Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECU
- **Telegram plan file size cap** (`internal/telegram/plan.go`) — plan files larger than 1 MiB are rejected by `ReadPlan` and `MostRecentPlan`, and `ListPlans` only reads the first 8 KiB for preview. This prevents a prompt-injected agent from causing an OOM via a multi-hundred-megabyte plan file.
- **Telegram log file permissions** (`internal/telegram/log.go`) — Telegram log files are created with `0600`, and `os.Chmod` hardens existing files. Chat IDs and task snippets are no longer world-readable by default.
- **Telegram chat-scoped sessions and plans** (`internal/telegram/session.go`, `internal/telegram/plan.go`, `cmd/odek/telegram.go`) — `/sessions`, `/resume`, `/prune`, `/plans`, `/plan_view`, `/plan_delete`, and `/plan_resume` are scoped to the requesting chat. Sessions are filtered by the `tg-<chatID>` prefix, `ResumeSession` rejects cross-chat IDs, and plans live under `~/.odek/plans/chat<chatID>/` so one chat cannot list, read, delete, or resume another chat's sessions/plans.
- **odek self-invocation gate** (`internal/danger/classifier.go`) — any shell stage whose program basename is `odek` is classified as `system_write`. This prevents a prompt-injected agent from reaching the human-gated trust mutations (`odek memory promote`, `odek memory extended confirm`, `odek skill promote --force`) through the shell tool and flipping its own taint gates.
- **Session ID entropy + session-scoped auth tokens** (`internal/session/session.go`, `cmd/odek/serve.go`) — session IDs now carry 128 bits of randomness (16 bytes / 32 hex chars); each session stores a 256-bit `AuthToken` required by `GET/DELETE/POST /api/sessions/<id>`, `POST /api/cancel`, and WebSocket session-resume messages via `X-Session-Token` header, `session_token` cookie, or `auth_token` WS field. Per-IP rate limiting (60/min) on session lookups adds a brute-force backstop.
- **Skill/episode untrusted wrapper** (`internal/loop/loop.go` + `odek.go`) — skill context and retrieved session-episode context are passed through the caller-provided untrusted wrapper (the same nonce'd `<untrusted_content_*>` boundary used for tool output) before being injected into the model's system context. This prevents a compromised or tainted skill/episode from being treated as trusted system instructions.
- **`env` / `printenv` environment-dump gate** (`internal/danger/classifier.go`) — bare `env` and `printenv` invocations are classified as `system_write` because they can leak process-environment secrets that the redaction scanner does not recognise. `env VAR=value <cmd>` still classifies `<cmd>` normally.
Expand Down
6 changes: 6 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,12 @@ These commands are now scoped to the requesting chat:

This removes the cross-chat session/plan disclosure path while keeping the CLI and admin flows functional.

### 39d. odek self-invocations classified as `system_write`

Human-gated trust mutations such as `odek memory promote <session>`, `odek memory extended confirm <id>`, and `odek skill promote <name> --force` are intentionally exposed only through the CLI, not as agent tools. Because the shell tool would otherwise resolve a bare `odek` command through `commandName` and let it fall through to an interactive prompt (or auto-allow depending on policy), a prompt-injected agent could invoke these commands itself and flip its own taint gates.

The danger classifier now treats any shell stage whose program basename is `odek` as `system_write`, so every self-invocation requires explicit operator approval and cannot be used to bypass the memory/skill provenance gates from inside an agent session.

### 40. `/api/resources` result limit cap

The `/api/resources?q=...&limit=N` autocomplete endpoint previously accepted any positive `limit` value. It is now capped to 100 results both in the HTTP handler and in `Registry.Search`. This prevents a prompt-injected or attacker-forged request from forcing an unbounded directory walk and returning a multi-megabyte JSON response.
Expand Down
8 changes: 8 additions & 0 deletions internal/danger/classifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,14 @@ func classifyCommand(tokens []string) RiskClass {
return SystemWrite
}

// odek self-invocations can reach human-gated trust mutations (`odek memory
// promote`, `odek memory extended confirm`, `odek skill promote --force`).
// Treating the whole binary as system_write prevents a prompt-injected agent
// from using the shell tool to flip its own taint gates.
if first == "odek" {
return SystemWrite
}

// Blocked
if isBlocked(tokens) {
return Blocked
Expand Down
21 changes: 21 additions & 0 deletions internal/danger/classifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,27 @@ func TestClassify_ShellRCTargets(t *testing.T) {
}
}

func TestClassify_OdekSelfInvocation(t *testing.T) {
tests := []struct {
cmd string
want RiskClass
}{
{"odek memory promote abc123", SystemWrite},
{"odek memory extended confirm xyz", SystemWrite},
{"odek skill promote evil-skill --force", SystemWrite},
{"./odek memory list", SystemWrite},
{"/usr/local/bin/odek --version", SystemWrite},
}
for _, tt := range tests {
t.Run(tt.cmd, func(t *testing.T) {
got := Classify(tt.cmd)
if got != tt.want {
t.Errorf("Classify(%q) = %s, want %s", tt.cmd, got, tt.want)
}
})
}
}

func TestClassify_PythonDashC(t *testing.T) {
got := Classify("python -c 'print(1)'")
if got != CodeExecution {
Expand Down
Loading