Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
286f27a
fix(loop): neutralise padded, modifier-separated and look-alike-brack…
jkyberneees Oct 12, 2026
57be8bb
fix(loop): argv0 skips array and append assignments
jkyberneees Oct 12, 2026
ff08b42
fix(loop): safe tools no longer withhold batch trust; compaction cut …
jkyberneees Oct 12, 2026
834222d
fix(loop): argv0 skips subscripted assignments
jkyberneees Oct 12, 2026
4532579
fix(loop): neutralise enclosed-letter, Armenian/Cherokee and bracket …
jkyberneees Oct 12, 2026
4e11717
fix(http_request): strip credentials on every https-to-http hop
jkyberneees Oct 12, 2026
b494046
fix(transcribe): never pass a protocol-shaped path to ffmpeg
jkyberneees Oct 12, 2026
4ff2679
fix(speak): write audio atomically so artifact symlinks are replaced …
jkyberneees Oct 12, 2026
3d17f32
fix(browser): wrap body-read error text as untrusted
jkyberneees Oct 12, 2026
e01a523
fix(list_tools): redact values after auth/pass/cookie-style MCP flags
jkyberneees Oct 12, 2026
757542f
fix(transcribe): restrict ffmpeg to file protocol and audio/video con…
jkyberneees Oct 12, 2026
60712c8
fix(list_tools): do not over-mask MCP switches that only mention a cr…
jkyberneees Oct 12, 2026
984b271
fix(speak): refuse a symlinked or non-directory artifacts root
jkyberneees Oct 12, 2026
0b5eeb9
fix(mcp): keep only structural pattern/format/$ref in the provider sc…
jkyberneees Oct 12, 2026
25fbc39
fix(subagent): digest denial reasons in subagent_denied events
jkyberneees Oct 12, 2026
802cbe9
fix(mcp): approval prompt shows the full description the model receives
jkyberneees Oct 12, 2026
85826ee
fix(mcpclient): bounded tools/list pagination, 64-char tool names, HT…
jkyberneees Oct 12, 2026
22112ef
fix(agent): open AGENTS.md with O_NOFOLLOW
jkyberneees Oct 12, 2026
5c3049d
test: regression tests for the runtime-area findings
jkyberneees Oct 12, 2026
58a5444
fix(mcpclient): keep the __ separator in shortened tool names; stop o…
jkyberneees Oct 12, 2026
36b0f9b
fix(mcp): tighten schema pattern and enum prose rules
jkyberneees Oct 12, 2026
945592d
fix(telegram): outbound media rejects credential-shaped files
jkyberneees Oct 12, 2026
329d6f2
fix(telegram): balance entities on every prose split path
jkyberneees Oct 12, 2026
207a40a
fix(telegram): plain-text fallback keeps code content
jkyberneees Oct 12, 2026
e8ff399
fix(telegram): convert CommonMark **bold** to MarkdownV2 bold
jkyberneees Oct 12, 2026
8cadec3
fix(telegram): bot client timeout covers the longest long-poll
jkyberneees Oct 12, 2026
c9a4713
fix(telegram): refuse uploads over the Bot API size limit before reading
jkyberneees Oct 12, 2026
65b8936
fix(schedule): scheduled Telegram delivery uses the stripped plain fa…
jkyberneees Oct 12, 2026
7a121d1
fix(telegram): daily token budget fails closed and keys on the UTC date
jkyberneees Oct 12, 2026
3848a81
fix(telegram): /plan_view cuts on a rune boundary; /plan_resume honou…
jkyberneees Oct 12, 2026
0bbf1e1
docs(telegram): media credential model, budget, upload and client tim…
jkyberneees Oct 12, 2026
fecd90f
test(telegram): cover StripMarkdown code handling and entity-balanced…
jkyberneees Oct 12, 2026
089c8a9
refactor(telegram): keep each doc comment on its own function
jkyberneees Oct 12, 2026
c52cc3b
fix(telegram): daily budget counter is written atomically; empty coun…
jkyberneees Oct 12, 2026
eac4280
fix(telegram): media directory-name credential rules skip ordinary me…
jkyberneees Oct 12, 2026
01d5bf9
fix(telegram): crossed emphasis never yields crossed MarkdownV2 entities
jkyberneees Oct 12, 2026
84ad33e
fix(session): redact tool-call arguments with key context
jkyberneees Oct 12, 2026
9dd6956
fix(session): redact system records on every save
jkyberneees Oct 12, 2026
e4fded3
fix(session): reads under ~/.odek taint the episode
jkyberneees Oct 12, 2026
063c49c
fix(memory): redact buffer lines before truncating
jkyberneees Oct 12, 2026
f2c7284
fix(memory): consolidation keeps a pin from any group member
jkyberneees Oct 12, 2026
d89f74a
fix(memory): sanitize pending-review ID and field on display
jkyberneees Oct 12, 2026
e0d0b0d
fix(memory): write the episode index before its summary file
jkyberneees Oct 12, 2026
5b0502f
test(memory): regression tests for the memory-area findings
jkyberneees Oct 12, 2026
0f913f1
fix(session): make tool-argument key-context redaction precise
jkyberneees Oct 12, 2026
15d4cb5
fix(serve): audit the first turn of a new session and drop audit writ…
jkyberneees Oct 12, 2026
96d08bc
fix(serve): retire pending approvals before announcing expiry or cancel
jkyberneees Oct 12, 2026
aa239fd
fix(serve): throttle WebSocket session lookup rejections and close ap…
jkyberneees Oct 12, 2026
13d8f6f
fix(serve): order audit writes against Delete and bound the pre-sessi…
jkyberneees Oct 12, 2026
ff1b7a1
fix(serve): honor an approval or clarify answer accepted at the timeo…
jkyberneees Oct 12, 2026
1da183e
fix(serve): rate limit @sess: rejections inside headless runs
jkyberneees Oct 12, 2026
3c3b18b
fix(subagent): staging root and task dir never followed through symlinks
jkyberneees Oct 12, 2026
896a759
fix(subagent): children inherit the parent's tool filter, sandbox pos…
jkyberneees Oct 12, 2026
54a01a0
fix(config): bare $VAR in project sandbox_env is flagged as host inte…
jkyberneees Oct 12, 2026
af2fe0e
fix(agent): CommonMark fence tracking for pillar stripping and fence …
jkyberneees Oct 12, 2026
1f941d1
fix(config): project config may only lower max_tool_parallel and skil…
jkyberneees Oct 12, 2026
b476f98
fix(skills): skill promote refuses symlinked SKILL.md and skill dirs
jkyberneees Oct 12, 2026
494e5fc
fix(subagent): cap staging copy at the artifact budget and drop parti…
jkyberneees Oct 12, 2026
f1a01af
fix(subagent): forward only directory- and content-bound approvals to…
jkyberneees Oct 12, 2026
2121396
fix(agent): fence tracker follows list-item containers
jkyberneees Oct 12, 2026
dab6ea4
fix(redact): one literal-value check for credential-named assignments
jkyberneees Oct 12, 2026
8fa0310
fix(render): escape the same zero-width marks as approval prompts
jkyberneees Oct 12, 2026
310da75
fix(audit): escape bidi, C1 and zero-width characters in 'odek audit …
jkyberneees Oct 12, 2026
fcddd46
fix(unreadscan): decode line-wrapped base64 and bound candidates by b…
jkyberneees Oct 12, 2026
fe955b3
fix(redact): linear-time credential redaction with bounded values
jkyberneees Oct 12, 2026
6295229
fix(redact): leave the closing quote of an enclosing JSON string outs…
jkyberneees Oct 12, 2026
d2cf05d
fix(unreadscan): dedupe decodes and scan every decode head before rem…
jkyberneees Oct 12, 2026
f82e636
fix(danger): classify substitutions in quoted arithmetic and substrin…
jkyberneees Oct 12, 2026
c808efd
fix(danger): match wildcard operands against credential and trust-anc…
jkyberneees Oct 12, 2026
ffd75ab
fix(danger): gate HOME/XDG_CONFIG_HOME overrides in front of startup-…
jkyberneees Oct 12, 2026
06108aa
fix(danger): catch pkill/killall inverse, empty, list and root select…
jkyberneees Oct 12, 2026
202a4d3
fix(danger): classify gpg key fetch, card, trust and preference verbs…
jkyberneees Oct 12, 2026
e1ba0a2
fix(danger): see jq env dumps behind --argfile and python-yq; treat .…
jkyberneees Oct 12, 2026
1093a11
fix(danger): scan environment, builtin-assigned and arithmetic code b…
jkyberneees Oct 12, 2026
3344b3e
fix(danger): rate sqlite readfile() and recursive readers over secret…
jkyberneees Oct 12, 2026
a38547d
fix(danger): compare account homes and rc/history parents case-folded
jkyberneees Oct 12, 2026
661a5ba
fix(danger): refuse shell/REPL history, fish config and rotated odek …
jkyberneees Oct 12, 2026
ef13f2a
fix(danger): classify zone-scoped IPv6 URL literals by their address
jkyberneees Oct 12, 2026
2d4a45a
docs(danger): document the new classifier and denylist coverage
jkyberneees Oct 12, 2026
90b7fe8
fix(danger): compute the pgrep-substitution broadcast check once per …
jkyberneees Oct 12, 2026
9c33587
fix(danger): back the read gate and ClassifyPath with one home histor…
jkyberneees Oct 12, 2026
705f70c
fix(danger): track HOME/XDG_CONFIG_HOME/ZDOTDIR across segments; stop…
jkyberneees Oct 12, 2026
379b2d4
fix(danger): let any /proc environ path component be a glob
jkyberneees Oct 12, 2026
e2f4d4c
fix(danger): treat a catch-all pgrep feeding kill through loops and n…
jkyberneees Oct 12, 2026
a00d9a1
fix(danger): compose GNU parallel and xargs placeholders and here-str…
jkyberneees Oct 12, 2026
4260676
docs(danger): document cross-segment startup overrides, parallel comp…
jkyberneees Oct 12, 2026
4931e47
fix(subagent): forward the parent's tool filter, not its surviving to…
jkyberneees Oct 12, 2026
b3a69f8
fix(loop): neutralise spaced forgeries of the untrusted-content tag
jkyberneees Oct 12, 2026
9ba4d0a
fix(introspect): mask dash-led values after credential flags
jkyberneees Oct 12, 2026
1355e8c
fix(telegram): apply media directory-name rules only below the workin…
jkyberneees Oct 12, 2026
291449a
docs: note UTC daily budget reset on upgrade; list new source files
jkyberneees Oct 12, 2026
c1a55a1
test: reword comments that referenced review findings
jkyberneees Oct 12, 2026
6f92ddc
fix(redact): linear credential scanning, brackets inside secrets, ski…
jkyberneees Oct 12, 2026
3a74b2a
fix(redact): leave Go source fields and secret-store references unred…
jkyberneees Oct 12, 2026
b27cb19
fix(redact): camelCase and PascalCase names ending in a secret word c…
jkyberneees Oct 12, 2026
793546c
fix(redact): quoted command-substitution references stay readable; do…
jkyberneees Oct 12, 2026
2700b6a
fix(danger): bound argv composer compositions by a total token/byte b…
jkyberneees Oct 12, 2026
2d8f07a
fix(danger): wildcards over /proc environ and home shell rc/history f…
jkyberneees Oct 12, 2026
d8b6609
fix(danger): HOME/XDG_CONFIG_HOME overrides for git are system_write
jkyberneees Oct 12, 2026
b1de105
fix(danger): recursive-reader secret roots judge output breadth, wild…
jkyberneees Oct 12, 2026
dbc96db
docs(security): document composer budget, glob/git/recursive-reader c…
jkyberneees Oct 12, 2026
ee79d9e
fix(loop): neutralise spaced-opener tag forgeries with spaced words
jkyberneees Oct 12, 2026
d88eeb0
fix(redact): letters-only secrets before punctuation are not treated …
jkyberneees Oct 12, 2026
7d632f2
perf(redact): judge unquoted credential values on a 256-byte window
jkyberneees Oct 12, 2026
0147ffe
fix(danger): narrow search patterns must be plain literals; empty sea…
jkyberneees Oct 12, 2026
167d01c
fix(danger): argv composer command cap fails closed
jkyberneees Oct 12, 2026
92e6e47
fix(danger): /proc/<pid>/task/<tid>/environ globs are secret reads
jkyberneees Oct 12, 2026
f9ac5e1
fix(danger): other accounts' shell rc/history files are secret reads
jkyberneees Oct 12, 2026
1e72277
docs(security): literal-pattern rule for narrow searches
jkyberneees Oct 12, 2026
834bfd4
chore: staticcheck fixes; skill promote resolves paths through os.Root
jkyberneees Oct 12, 2026
05b2d0a
test(telegram): wait out post-turn background writes before temp-dir …
jkyberneees Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ cmd/odek/
subagent.go Sub-agent command (--goal, --context, --task) + flag parsing/limits
subagent_tool.go delegate_tasks built-in tool (sub-agent spawning)
subagent_key.go FD-based API key handoff (parent → sub-agent, never via env)
subagent_posture.go Parent posture stamped into task files: configured tool allow/deny filter, container
settings and granted sandbox approval keys (children never exceed the parent)
subagent_registry.go Parent-side registry of in-flight sub-agent tasks (cancel, status)
subagent_artifacts.go Sub-agent result-artifact protocol (files auto-delivered to the parent)
subagent_artifact_registry.go Collation-time artifact-id registry backing artifact_read
Expand Down Expand Up @@ -123,6 +125,9 @@ internal/
command inside is classified; unpaired constructs are unknown
wrapper_grammar.go One option grammar for wrappers (timeout/nice/sudo/flock/script/nix/mise/…) incl. command-string options
denylist.go Denylist token-prefix matching at every command position
argv_compose.go Argument composition of xargs/parallel/find -exec stages (bounded; over budget ⇒ unknown)
secret_globs.go Globs that can expand to credential files, shell rc/history files or /proc environ
secret_recursive.go Recursive reads/greps over trees that contain secrets
secret_reads.go Secret-shaped env-var references and credential-file reads/writes → system_write
network_upload.go network_upload class: bodies from files/stdin, credentials, mutating methods, uploads, listeners
gh_adapter.go gh classified by command and verb (reads egress, mutations system_write, deletes destructive)
Expand All @@ -131,7 +136,10 @@ internal/
ledger_indirect.go Scripts delivered indirectly (pipes, substitutions, eval, find -exec, program-file options)
path_identity.go Symlink-resolving path targets for classification (snapshot, not an execution boundary)
sensitive_read.go IsSensitiveReadPath: paths @-references refuse (shares the home credential table with ClassifyPath)
arith_eval.go Arithmetic-evaluating builtins (let, declare, …): command substitutions in subscripts are classified
arith_eval.go Arithmetic-evaluating builtins (let, declare, …) and arithmetic expansions ($(( )), $[ ], ${s:off:len}, ${a[sub]}): command substitutions inside are classified even when single-quoted
secret_globs.go Wildcard operands matched segment-wise against odek anchors, /proc/<pid>/environ and the credential path table
secret_recursive.go Recursive readers (grep -r, rg, find -exec cat, tar c, cp -r, …) rooted at /proc/<pid>, /etc, / or a home
argv_compose.go xargs / GNU parallel / xe composition: static stdin, here-strings, ::: sources and -I/{} placeholders expanded into commands (classifier + denylist)
param_default.go ${name:-word} / ${name:=word} / ${name:+word} default words classified as the commands they can run
env_dump_tools.go jq/yq/ps environment disclosure → system_write
env_code.go PROMPT_COMMAND / PS0-PS4 / BASH_FUNC_ assignments: values classified as commands
Expand Down Expand Up @@ -166,7 +174,7 @@ internal/
session_scope.go (session-id → chat ownership check), send_fallback.go (plain-text resend when markup is rejected)
render/ Terminal output and narrator support
narrate/ LLM-powered emoji-rich progress messages
redact/ Secret redaction (20+ patterns)
redact/ Secret redaction (20+ patterns; credential_value.go judges unquoted credential values)
mcp/ MCP server handler (stdio JSON-RPC: tools/list, tools/call)
mcpclient/ MCP client (connect to external MCP servers); per-server limits (timeout/response bytes/result
chars/artifact roots) + odek-extension/v1 contract (contract.go), artifact-ref enforcement in CallTool
Expand Down
46 changes: 45 additions & 1 deletion cmd/odek/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"fmt"
"os"
"strings"
"unicode"
"unicode/utf16"

"github.com/BackendStack21/odek/internal/danger"
"github.com/BackendStack21/odek/internal/loop"
Expand Down Expand Up @@ -252,11 +254,53 @@ func auditCmd(args []string) error {
if err != nil {
return fmt.Errorf("audit: marshal: %w", err)
}
fmt.Println(string(out))
fmt.Println(escapeJSONForTerminal(string(out)))
return nil
}
}

// escapeJSONForTerminal rewrites the characters json.Marshal leaves raw but
// a terminal acts on or hides (C1 controls such as the 8-bit CSI, bidi and
// other format characters, zero-width marks, blank fillers, any non-printable
// rune) as \uXXXX escapes. Such runes can only occur inside JSON strings, so
// the result stays valid JSON that decodes to the same value.
func escapeJSONForTerminal(s string) string {
clean := true
for _, r := range s {
if jsonRuneNeedsEscape(r) {
clean = false
break
}
}
if clean {
return s
}
var b strings.Builder
for _, r := range s {
switch {
case !jsonRuneNeedsEscape(r):
b.WriteRune(r)
case r > 0xFFFF:
hi, lo := utf16.EncodeRune(r)
fmt.Fprintf(&b, `\u%04x\u%04x`, hi, lo)
default:
fmt.Fprintf(&b, `\u%04x`, r)
}
}
return b.String()
}

func jsonRuneNeedsEscape(r rune) bool {
if r < 0x80 {
return false
}
switch r {
case '\u034F', '\u115F', '\u1160', '\u180E', '\u3164', '\uFFA0':
return true
}
return !unicode.IsPrint(r) || unicode.Is(unicode.Cf, r) || danger.IsZeroWidthMark(r)
}

func auditList(store *session.Store, auditStore *session.AuditStore) error {
sessions, err := store.List(0)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion cmd/odek/browser_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ func (t *browserTool) doNavigate(rawURL string) (string, error) {

body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB cap
if err != nil {
return jsonError(fmt.Sprintf("cannot read response: %v", err))
msg := fmt.Sprintf("cannot read response: %v", err)
return jsonError(wrapUntrusted(t.toolCtx(), rawURL, msg))
}

html := string(body)
Expand Down
2 changes: 1 addition & 1 deletion cmd/odek/clarify.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

// clarifyTimeout is how long a principal-channel question waits before
// the tool returns an error and the loop continues.
const clarifyTimeout = 5 * time.Minute
var clarifyTimeout = 5 * time.Minute

func ttyClarifyAvailable() bool {
f, err := os.OpenFile(danger.TTYDevicePath(), os.O_RDWR, 0)
Expand Down
4 changes: 3 additions & 1 deletion cmd/odek/http_request_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ func (t *httpRequestTool) checkRedirect(req *http.Request, via []*http.Request)
// every credential header and re-gate a method-preserving redirect as an
// upload to the new host.
// A scheme downgrade (https to http) on the same host sends them in the clear.
// Every hop is compared with the ORIGINAL request: net/http re-copies the
// original headers on each hop, so a strip on one hop does not carry over.
if len(via) > 0 && (!strings.EqualFold(via[0].URL.Host, req.URL.Host) ||
(via[len(via)-1].URL.Scheme == "https" && req.URL.Scheme != "https")) {
(via[0].URL.Scheme == "https" && req.URL.Scheme != "https")) {
for name := range req.Header {
if danger.CredentialHeaderName(name) {
req.Header.Del(name)
Expand Down
39 changes: 35 additions & 4 deletions cmd/odek/introspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,23 @@ func toolEnabled(name string, enabledSet, disabledSet map[string]bool, whitelist
// --api-key, -token, --secret-file, --db-password, --credential, …
var credentialArgFlag = regexp.MustCompile(`(?i)^-{1,2}[a-z0-9_-]*(key|token|secret|password|passwd|credential)[a-z0-9_-]*$`)

// credentialArgFlagWord matches flags whose dash/underscore-separated
// component names a credential (--auth, --pass, --pat, --cookie, --creds,
// --dsn, --bearer, --session), the vocabulary credentialEnvName uses. Whole
// components only, so --path or --author stay readable.
var credentialArgFlagWord = regexp.MustCompile(`(?i)^-{1,2}(?:[a-z0-9]+[-_])*(?:auth|pass|pat|cookie|cookies|creds|dsn|bearer|session|apikey)(?:[-_][a-z0-9]+)*$`)

// nonCredentialArgFlag matches switches that merely mention a credential word
// (--no-auth, --pass-through, --auth-mode, --token-type): they take no secret.
var nonCredentialArgFlag = regexp.MustCompile(`(?i)^-{1,2}no[-_]|[-_](mode|through|type)$`)

func isCredentialArgFlag(a string) bool {
if nonCredentialArgFlag.MatchString(a) {
return false
}
return credentialArgFlag.MatchString(a) || credentialArgFlagWord.MatchString(a)
}

const redactedArgPlaceholder = "[redacted]"

// redactMCPServersView is the defense-in-depth pass for the TOOL face of
Expand Down Expand Up @@ -236,9 +253,13 @@ func redactCredentialArgs(args []string) []string {
envNext := false
for i, a := range args {
if redactNext {
out[i] = redactedArgPlaceholder
redactNext = false
continue
// A following long flag (--name) means the credential flag took
// no value; anything else, even a dash-led token, is the value.
if !looksLikeLongFlag(a) {
out[i] = redactedArgPlaceholder
continue
}
}
if envNext {
envNext = false
Expand All @@ -247,11 +268,11 @@ func redactCredentialArgs(args []string) []string {
continue
}
}
if eq := strings.IndexByte(a, '='); eq > 0 && credentialArgFlag.MatchString(a[:eq]) {
if eq := strings.IndexByte(a, '='); eq > 0 && isCredentialArgFlag(a[:eq]) {
out[i] = a[:eq+1] + redactedArgPlaceholder
continue
}
if credentialArgFlag.MatchString(a) {
if isCredentialArgFlag(a) {
redactNext = true
out[i] = a
continue
Expand All @@ -266,6 +287,16 @@ func redactCredentialArgs(args []string) []string {
return out
}

// looksLikeLongFlag reports whether a is shaped like `--name` or `--name=…`
// (a letter right after the double dash).
func looksLikeLongFlag(a string) bool {
if len(a) < 3 || a[0] != '-' || a[1] != '-' {
return false
}
c := a[2]
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}

// redactPositionalArg masks a credential embedded in a single argument that
// no preceding flag identified.
func redactPositionalArg(a string) string {
Expand Down
18 changes: 18 additions & 0 deletions cmd/odek/introspect_credflag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import "testing"

// A credential flag's value is masked even when it starts with a dash; only a
// following long flag means the credential flag took no value.
func TestRedactCredentialArgs_DashPrefixedValue(t *testing.T) {
for _, val := range []string{"-AbC123xyz", "-s3cr3t", "-"} {
got := redactCredentialArgs([]string{"--token", val})
if got[1] != redactedArgPlaceholder {
t.Errorf("value %q leaked: %v", val, got)
}
}
got := redactCredentialArgs([]string{"--password", "--verbose"})
if got[1] != "--verbose" {
t.Errorf("following flag masked: %v", got)
}
}
5 changes: 5 additions & 0 deletions cmd/odek/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,7 @@ func setupSandbox(tools []odek.Tool, cfg sandboxConfig) (containerName string, c
})

applySandboxToolBindings(tools, containerName)
recordChildSandbox(tools, cfg)
return containerName, cleanup, nil
}

Expand Down Expand Up @@ -2832,6 +2833,10 @@ func filterBuiltinTools(tools []odek.Tool, cfg config.ToolConfig, required map[s
for i, t := range filtered {
out[i] = t.(odekToolAdapter).tool
}
// delegate_tasks children inherit the filter itself (they can narrow it
// further, never widen it); with no filter configured they get their
// normal tool set.
recordChildToolFilter(tools, cfg)
return out
}

Expand Down
1 change: 1 addition & 0 deletions cmd/odek/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2275,6 +2275,7 @@ func setupTestHome(t *testing.T) string {
dir := t.TempDir()
os.Setenv("HOME", dir)
t.Cleanup(func() { os.Setenv("HOME", orig) })
confighuntResetSessionApprovals(t)
return dir
}

Expand Down
24 changes: 7 additions & 17 deletions cmd/odek/mcp_approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ func approveMCPToolsWithTTY(projectDir, serverName string, cfg mcpclient.ServerC
}

fmt.Fprintf(stdout, "\nMCP server %q wants to register tool %q\n", serverName, def.Name)
if reg := mcpclient.RegisteredToolName(serverName, def.Name); reg != serverName+"__"+def.Name {
fmt.Fprintf(stdout, " registered as %q (shortened to the %d-character provider limit)\n", reg, mcpclient.MaxRegisteredToolNameLen)
}
writeMCPToolContract(stdout, def, schemaHash, schemaSize)
fmt.Fprintf(stdout, "Approve? [y/N] ")

Expand Down Expand Up @@ -427,23 +430,7 @@ func hashServerLimits(h hash.Hash, cfg mcpclient.ServerConfig) {
}
}

// truncateDescription limits a tool description for the approval prompt.
func truncateDescription(desc string, max int) string {
if len(desc) <= max {
return desc
}
if max <= 3 {
return desc[:max]
}
return desc[:max-3] + "..."
}

const (
// mcpPromptDescriptionBytes bounds the description shown in the per-tool
// approval prompt. It exceeds the old 200-byte excerpt by far, so a
// payload appended after a benign opening is visible when a server
// rewrites an approved tool.
mcpPromptDescriptionBytes = 2048
// mcpPromptMaxParams bounds the parameter summary lines.
mcpPromptMaxParams = 32
// mcpPromptIndent prefixes continuation lines of multi-line fields so
Expand All @@ -461,7 +448,10 @@ const (
// danger.SanitizeInline.
func writeMCPToolContract(w io.Writer, def mcpclient.ToolDef, schemaHash string, schemaSize int) {
if strings.TrimSpace(def.Description) != "" {
desc := danger.SanitizeForDisplay(truncateDescription(def.Description, mcpPromptDescriptionBytes))
// The prompt shows exactly the description text the model receives
// (same rune cap as sanitizeMCPToolDoc), so a payload appended after
// a long benign opening is visible when a server rewrites a tool.
desc := danger.SanitizeForDisplay(capMCPDescription(def.Description))
fmt.Fprintf(w, " description:\n%s%s\n", mcpPromptIndent, indentMCPPrompt(desc))
}
fmt.Fprintf(w, " schema: sha256:%s (%d bytes)\n", schemaHash[:16], schemaSize)
Expand Down
Loading
Loading