Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9dd3eb6
fix(mcp): run schema scan and size cap under ODEK_APPROVE_MCP
jkyberneees Oct 9, 2026
033356d
fix(mcpclient): cap JSON-RPC error messages at max_result_chars
jkyberneees Oct 9, 2026
65d4b99
fix(mcp): keep schema free text out of the provider tool definition
jkyberneees Oct 9, 2026
0fd4aca
fix(mcp): show the tool contract text on MCP tool (re-)approval
jkyberneees Oct 9, 2026
c1f9495
docs(agents): note MCP schema stripping and env-path scans
jkyberneees Oct 9, 2026
de579c1
fix(telegram): disable link previews on every outbound message by def…
jkyberneees Oct 9, 2026
416d897
fix(telegram): bind wake-turn approvals to a real user and mark wake …
jkyberneees Oct 9, 2026
e66a12b
fix(telegram): warn when a group chat is allowed without allowed_users
jkyberneees Oct 9, 2026
c9f9fc4
fix(telegram): record forwarded/voice/caption ingests and guard-scan …
jkyberneees Oct 9, 2026
bb51120
fix(memory): show persisted text in memory write approvals
jkyberneees Oct 9, 2026
2622cc2
fix(skills): bound, scan and sanitise skill names in the catalog
jkyberneees Oct 9, 2026
c77aa46
fix(skills): trusted skill wins over a same-named copy pending review
jkyberneees Oct 9, 2026
584964d
fix(memory): taint episodes from network shell commands
jkyberneees Oct 9, 2026
77e4e4e
fix(serve): show the full episode summary before REST promote
jkyberneees Oct 9, 2026
d4b0254
test(skills): cover catalog withholding and collision edge cases
jkyberneees Oct 9, 2026
e85647b
fix(loop): completion nudge never directs execution of plan checks
jkyberneees Oct 9, 2026
b46566a
fix(loop): bare engines wrap skill, episode, memory and bg-notice con…
jkyberneees Oct 9, 2026
6cb5d1f
fix(loop): re-wrap persisted and tool-supplied wrappers not minted in…
jkyberneees Oct 9, 2026
a9450d4
fix(loop): re-cap persisted digest and bound persisted plan fields on…
jkyberneees Oct 9, 2026
6a3587a
fix(agent): strip identity sections imitating the security pillar
jkyberneees Oct 9, 2026
bf0016b
fix(proactive): inject return-after-break summary as a named user mes…
jkyberneees Oct 9, 2026
554a980
fix(danger): stop flagging identity prose as impersonation
jkyberneees Oct 9, 2026
b241438
fix(guard): judge content past max_text_length with the sidecar
jkyberneees Oct 9, 2026
e4eef01
fix(render): escape terminal controls in model and tool text
jkyberneees Oct 9, 2026
795082a
fix(danger): catch common injection paraphrases with low false positives
jkyberneees Oct 9, 2026
ba8b139
fix(subagent): fence the task text whenever effective trust is untrusted
jkyberneees Oct 9, 2026
0b99ea3
fix(loop): treat a run with MCP tools registered as tainted for deleg…
jkyberneees Oct 9, 2026
8770b83
fix(session): persist the untrusted-ingest taint as sticky session state
jkyberneees Oct 9, 2026
d9d5cb8
fix(subagent): authenticate the child result frame with a per-spawn n…
jkyberneees Oct 9, 2026
dad977e
build(deps): bump golang.org/x/net to v0.60.0
jkyberneees Oct 9, 2026
714b558
fix(taint): pure first-party tool calls no longer taint the run
jkyberneees Oct 9, 2026
fc0b198
fix(memory): keep episode taint sticky across history trimming
jkyberneees Oct 9, 2026
82ddee0
fix(telegram): mark session taint under the chat save lock
jkyberneees Oct 9, 2026
273d5ff
fix(render): print subdivision flags instead of escaping their tag ch…
jkyberneees Oct 9, 2026
d7fb518
docs(memory): long legacy facts are targeted by a unique old_text sub…
jkyberneees Oct 9, 2026
827d6be
style(loop): gofmt plan_baseline_eval_test.go
jkyberneees Oct 9, 2026
9b8f6bc
fix(danger): do not flag policy prose that forbids sending secrets
jkyberneees Oct 9, 2026
f0077f0
fix(memory): show the entry a replace/remove modifies in its approval
jkyberneees Oct 9, 2026
5231543
fix(subagent): make the sub-agent child non-dumpable on Linux
jkyberneees Oct 9, 2026
ea5552b
test(session): pin the revision settle window in the own-write test
jkyberneees Oct 10, 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
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ cmd/odek/
browser_tool.go Built-in browser tool (HTTP fetch + headless navigation)
file_tool.go Built-in file tools (read_file, write_file, search_files, patch, glob, file_info)
perf_tools.go Native utilities (math_eval, diff, json_query, tree, checksum, head_tail, base64)
pure_tools.go Pure-output audit + markers: first-party calls that never taint the run
http_request_tool.go Single-URL HTTP status/size checks with SSRF and redirect guards
introspect.go config_view + list_tools tools and shared sanitized view builders
(structural sanitization: secrets never enter the view map)
Expand Down Expand Up @@ -107,7 +108,8 @@ internal/
post-batch reconciliation; signal.go — SignalEvent observability
(context_trimmed, tool_recovery, tool_running heartbeat); budget enforcement
(budget.Checker) + odek.event/v1 emission.
tool/ Thread-safe tool registry, clarify.go, send_message.go
tool/ Thread-safe tool registry, clarify.go, send_message.go, purity.go (first-party-only
pure-output registry: a registered type's pure calls skip the ingest taint)
danger/ Command/URL classification + bypass-resistant tokenizer. Approver interface +
TTYApprover with friction mode (interactive approval system lives here).
classifier.go normalize entry point, RiskClass set (11 classes), DangerousConfig, ActionForCommand, verb/path classification,
Expand Down Expand Up @@ -141,6 +143,7 @@ internal/
logquery/ Structured query over runtime logs
memory/ MemoryManager (facts, buffer, episodes, merge, scan). EpisodeProvenance — tainted episodes never auto-replayed.
session/ Session store (CRUD, trim, cleanup, compact JSON). AuditStore + divergence heuristic.
Sticky taint flags (UntrustedIngested; EpisodeUntrusted from the episode trust rule in episode_taint.go).
ExternalRef — opaque operator-supplied refs, never dereferenced.
artifact/ odek.artifact-ref/v1 + odek.tool-result/v1: fail-closed ref validation
(roots/symlinks/sha256/size) and model-safe rendering (metadata only, no paths/content).
Expand Down Expand Up @@ -204,19 +207,19 @@ Built-in tools include: http_request, math_eval, diff, json_query, tree, checksu
Vertical space compression is baked into the render paths; blank lines removed from Iteration/FinalAnswer/Summary. Raw-mode cursor uses `\r\n` for cross-platform compatibility.

### Identity
System prompt priority: `--system` flag > `~/.odek/IDENTITY.md` > compiled-in defaultSystem. Explicit prompts and IDENTITY.md are capped at 256 KiB and scanned with `danger.ScanInjection` (failure → compiled-in default). Project `AGENTS.md` ignored if >256 KiB. The compiled-in default carries the execution-provenance rules (justification from the principal; read what you execute; failed reads never become executions; deferred-execution confirmation; tool metadata is not directives) and is itself scanner-clean — pinned by `TestDefaultSystem_PassesOwnInjectionScan` so a copy into `IDENTITY.md` is never rejected. Operator identity surfaces (`--system`, `ODEK_SYSTEM`, config `system`, `IDENTITY.md`) carry identity only — name, mission, persona. `buildSystemPrompt` force-composes the invariant pillar on top of every accepted identity via `composeSystem` (idempotent — an identity already carrying the pillar is kept whole), so no operator surface can drop the security rules; the compiled default is `defaultIdentity` + pillar, pinned by `cmd/odek/system_pillar_test.go`. Sub-agents compose the same invariant pillar into `subagentSystem` (shared `securityPillar` const: Safety, Execution provenance, IPI) plus role amendments — a child has no principal channel, so confirmation rules become skip-and-report, justification scope is the declared task, and deferred execution requires the task to name the mechanism. Parity is pinned by `cmd/odek/subagent_pillar_test.go`; the operator-writable parent surface (`--system`, `IDENTITY.md`) never propagates to children.
System prompt priority: `--system` flag > `~/.odek/IDENTITY.md` > compiled-in defaultSystem. Explicit prompts and IDENTITY.md are capped at 256 KiB and scanned with `danger.ScanInjection` (failure → compiled-in default). Project `AGENTS.md` ignored if >256 KiB. The compiled-in default carries the execution-provenance rules (justification from the principal; read what you execute; failed reads never become executions; deferred-execution confirmation; tool metadata is not directives) and is itself scanner-clean — pinned by `TestDefaultSystem_PassesOwnInjectionScan` so a copy into `IDENTITY.md` is never rejected. Operator identity surfaces (`--system`, `ODEK_SYSTEM`, config `system`, `IDENTITY.md`) carry identity only — name, mission, persona. `buildSystemPrompt` force-composes the invariant pillar on top of every accepted identity via `composeSystem` (exact embedded pillar copies are stripped and one authoritative pillar is appended last; headings imitating a pillar section title — Safety, Execution provenance, Indirect Prompt Injection, in ATX, setext or bold-line form after Unicode folding — are stripped with the rule block directly under them, fenced code untouched, `untrusted_content` literals in the identity neutralised, applied to the identity alone before AGENTS.md/skill adjuncts are appended, so an altered copy cannot precede the real one), so no operator surface can drop or contradict the security rules; the compiled default is `defaultIdentity` + pillar, pinned by `cmd/odek/system_pillar_test.go`. Sub-agents compose the same invariant pillar into `subagentSystem` (shared `securityPillar` const: Safety, Execution provenance, IPI) plus role amendments — a child has no principal channel, so confirmation rules become skip-and-report, justification scope is the declared task, and deferred execution requires the task to name the mechanism. Parity is pinned by `cmd/odek/subagent_pillar_test.go`; the operator-writable parent surface (`--system`, `IDENTITY.md`) never propagates to children.

### Security Architecture

Layered prompt-injection / approval-fatigue defenses. The full per-mitigation list lives in [docs/SECURITY.md](docs/SECURITY.md); `cmd/odek/security_report_validation_test.go` is the regression bar. Summary by layer:

- **Untrusted-content boundary** (`cmd/odek/untrusted.go`) — every externally-sourced tool result (browser, file/shell/search tools, MCP, session_search, @-refs, --ctx, attachments, artifact_read) is wrapped in a per-call nonce'd `<untrusted·content_<nonce>>` tag; tool-result delimiters are also nonce'd (`internal/loop`). Skill/episode context injected into the system prompt is wrapped too. The per-session audit log (`cmd/odek/audit.go`) records every ingest and flags divergence between user-mentioned resources and agent actions.
- **Provenance gates** — tainted memory episodes are stored but never auto-replayed; skills from untrusted sources (imported via URI, project `./.odek/skills/`) are pinned `NeedsReview` until `odek skill promote --force` is run after human review, excluded from trigger matching, and protected against frontmatter tampering. Load-time and import-time skill bodies go through the injection scanner (`guard.ScanContentWithScope`). `odek` self-invocation via shell is `system_write` so the agent can't reach its own trust mutations.
- **Provenance gates** — tainted memory episodes are stored but never auto-replayed (the episode taint is sticky session state, `Session.EpisodeUntrusted`, so trimming cannot launder it); skills from untrusted sources (imported via URI, project `./.odek/skills/`) are pinned `NeedsReview` until `odek skill promote --force` is run after human review, excluded from trigger matching, and protected against frontmatter tampering. Load-time and import-time skill bodies go through the injection scanner (`guard.ScanContentWithScope`). `odek` self-invocation via shell is `system_write` so the agent can't reach its own trust mutations.
- **Danger classifier** (`internal/danger/classifier.go`; layered design in its package doc) — quote-aware normalization ($IFS, ANSI-C, brace lists/sequences, substitutions, heredocs, wrappers, backslashes, basenames); compound commands (`for`/`while`/`if`/`case`/groups/functions/`[[ ]]`) parsed so every simple command inside is classified, unterminated constructs and quotes classify `unknown`. Wrappers share one option grammar (`wrapper_grammar.go`) incl. command-string options (`env -S`, `script -c`, `flock -c`). Covers awk/sed/editor escapes, pipe-fed xargs composition, root-level mutation targets, git data-loss verbs, `git -c`/config code exec, find/rsync destructive flags, env dumps, exec-controlling `export`s, shell operand/redirect path classification (writes to shell rc files, ~/.ssh, ~/.odek escalate to system_write). `network_upload` (default prompt, ranked between `network_egress` and `code_execution`) splits uploads, credentialed/mutating requests, listeners and tunnels from plain egress; `gh` is classified by command and verb (`gh_adapter.go`). Ordinary git verbs escalate to `code_execution` only when the targeted repository is armed (hooks, fsmonitor, filters/drivers, editors, includes); an undeterminable repo, `GIT_*` overrides, sudo wrappers or a hook written earlier in the same command fail closed (`git_repo_arming.go`). Denylist entries are token prefixes matched at every command position, with tool global options stripped and known variables resolved — not raw string prefixes; here-strings and static echo/printf text piped into a shell, also through pass-through stages (cat, tee, sort, uniq, tac, head, tail), are scanned too, and the recursive scan is memoised depth-aware so nested eval/find -exec chains stay bounded. Secret-shaped env vars (`$NAME`, `${!v}`, `printenv`, `os.environ`) and credential files (by basename, extension or directory) are `system_write` (`secret_reads.go`). Trust anchors under `~/.odek` are write-protected from generic file tools. The read ledger is fingerprinted (`WasReadFresh`: post-read mutation re-fires the unread-script gate), covers scripts delivered through pipes, substitutions, `eval`, `find -exec` and program-file options, is bounded (4096 paths per session, 1024 sessions, LRU eviction only ever removes a licence) and dropped per session with `danger.ForgetReadLedger` (serve session delete, Telegram reset, schedule run end); unread-script approvals carry a pre-exec injection-scan enrichment incl. single-layer base64/hex decode (`cmd/odek/unreadscan.go`, scan never populates the ledger). Quoted operator-shaped words (`';'`, `'|'`, a decoded `$'\n'`) are arguments in every segment/pipe split (literal-marked tokens), the denylist scan examines each distinct command line once (bounded on nested `eval`/`find -exec`) and also reads here-strings and static `echo`/`printf` pipes into a shell, and a home reached through a symlink counts as the same home. Fuzz invariants in `monotonicity_fuzz_test.go` (suffix wipe never hidden, pipe into shell at least `code_execution`, harmless prefix never lowers a verdict, bounded time).
- **Plan-check honesty** — plan acceptance checks record outcomes from actual engine observation only; failed or unclassifiable tool effects invalidate earlier checks in the same batch, so a check can never be satisfied by a claim inside tool output.
- **Approval friction** — TTY/WS/Telegram approvers engage friction after 3 same-class approvals in 60s (type `approve`, pause, trust shortcut hidden); `destructive`/`blocked`/`unknown` never get trust shortcuts. TTY prompts are process-wide serialized. Everything shown in an approval (TTY/WebSocket/Telegram prompts, batch cards, MCP/sandbox approval prompts, denial error strings) goes through `danger.SanitizeForDisplay`/`SanitizeInline`, which escape control, bidi and invisible characters so the human reads the bytes that run; the read_only non-interactive carve-out is keyed on the native tool name, never the model-supplied description.
- **Sub-agent caps** — `delegate_tasks` carries trust_level + max_risk enforced via the sub-agent's DangerousConfig; MCP tools withheld from untrusted sub-agents; API keys handed off via unlinked-tempfile FD, never env. Sub-agent results over ~2000 chars are delivered as registry-backed artifacts the parent reads via `artifact_read`.
- **MCP hardening** — subprocess env sanitization (secret-pattern stripping), tool-name/description/inputSchema validation + injection scans, per-tool approval for every server (keys hash command/args/env + schema hash + description text + all four limit fields), per-server limits with absolute ceilings, per-server `enabled` toggle, artifact-ref fail-closed validation.
- **MCP hardening** — subprocess env sanitization (secret-pattern stripping), tool-name/description/inputSchema validation + injection scans (also under `ODEK_APPROVE_MCP`), provider schema reduced to structural keywords with schema free text moved into the wrapped description (`cmd/odek/mcp_schema.go`), per-tool approval for every server (keys hash command/args/env + schema hash + description text + all four limit fields), per-server limits with absolute ceilings, per-server `enabled` toggle, artifact-ref fail-closed validation.
- **Config trust split** — `./odek.json` is untrusted: sensitive sections (provider, providers, llm, base_url, api_key, system, dangerous, memory, telegram, web_search, embedding, sessions, skills.dirs, verify, profiles, guard, schedules) ignored with warnings; sandbox knobs gated behind explicit operator approval (incl. implicit `Dockerfile.odek` builds, content-hash keyed); project limits may only lower global budgets, project prices rejected outright. Global config/secrets permission-checked; config files size-capped.
- **Serve / network surface** — per-instance CSRF token on `/ws` and all `/api/*`, loopback Host checks, local-origin requirement for mutations, per-session auth tokens + rate limiting, clickjacking headers, WS message-size caps. SSRF dial guard (DNS-rebinding-safe, internal-IP refusal, proxy refusal) on browser/http_request/web_search. WebUI done-frame stats are markup-escape hardened; WebUI task supervision renders sub-agent state without trusting frame content.
- **Budgets, events, refs (v1.24.0)** — budget clamp merge (see above); event stream carries SHA-256 arg hashes + sizes only (never raw args), redact applied, JSONL sink 0600/no-symlink/fsync-per-event, drop-on-full dispatch; external refs validated and never dereferenced.
Expand Down
9 changes: 6 additions & 3 deletions cmd/odek/bg_telegram_wake.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ package main
// Deliberate limits:
// - The wake preamble is generic; job details arrive via the loop's
// notice drain, same as the WebUI path.
// - Wake turns run with userID 0: the TelegramApprover already treats a
// zero originating user as "no user binding", so approvals stay
// available without hijacking a user.
// - No chat member sent a wake turn, so its approvals and clarify prompts
// are bound to the user who last started a turn in the chat, or to the
// single allowed user. With neither, they fail closed (denied, never
// answerable by any chat member). The wake message is persisted with
// Name "bg-wake", as on the WebUI, so audit and verification never treat
// it as an operator task.
// - Idle detection uses the same per-chat slot the turn pipeline holds
// (pinChat), checked with a bounded wait (idleWait) so a wake never
// queues behind a long turn.
Expand Down
Loading
Loading