Commit 4ffdb7d
feat(memory): path-aware episode/skill provenance + human-gated promote (#10)
* feat(memory): path-aware episode/skill provenance + human-gated promote
Fix the "episode dead-end": provenance tainted episodes by tool *name* only,
so read_file/search_files/multi_grep — used in essentially every coding
session — always produced an untrusted episode. Untrusted episodes are
excluded from recall, and nothing ever set UserApproved, so for normal work
the episode tier was written but never replayed (dead weight).
Provenance is now argument-aware via a single shared predicate
`memory.ToolCallTaints(name, argsJSON)`:
- Always untrusted: browser, http_batch, transcribe, and any MCP tool (__).
- Path-scoped (read_file, search_files, multi_grep): taint only when the
`path` argument resolves to a sensitive location (danger.ClassifyPath →
system_write/destructive, e.g. /etc, ~/.ssh, ~/.aws). Workspace and other
non-sensitive local reads stay trusted. Malformed/absent path → conservative
taint. This precisely re-targets the original "covers /etc/, $HOME/.ssh"
concern while letting ordinary sessions stay recallable.
The skills provenance (internal/skills/selfimprove.go) now delegates to the
same predicate, so learned skills stop being over-flagged for review too.
Reusing danger.ClassifyPath means DeriveProvenance keeps its (messages)
signature — no cwd plumbing into the async session-end call sites.
Escape hatch for genuinely-external episodes: new human-gated CLI
`odek memory list` / `odek memory promote <session_id>` sets UserApproved.
It is intentionally NOT an agent tool — a prompt-injected agent must not be
able to self-approve its own poisoned memory.
Also fixes two stale descriptions found while mapping the system: the
extract_on_end docs ("durable facts" → episode summary) and the
FormatEpisodeContext comment ("recency-based ranker, no LLM" → it uses the
configured, LLM-by-default ranker).
Tests: path-aware taint (workspace trusted, sensitive/system tainted, malformed
conservative, always-external), the ToolCallTaints table, episode
promote/pending lifecycle + persistence, skills mirror (guards the
llm.Message→LlmMessage Arguments plumbing), and the CLI command end to end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(memory): close provenance bypasses found by verification (D-01/D-02/D-03/D-05)
Remediates the findings from the AI Verification Protocol run on this PR. The
v1 taint check used a sensitive-path *denylist* (danger.ClassifyPath) over only
three tools, which left several bypasses:
- D-03 (regression): macOS /etc and /var are symlinks to /private/etc, /private
/var. ClassifyPath only matched /etc,/var, so read_file("/private/etc/...")
classified local_write and did NOT taint — a sensitive read that tainted
before this PR. Confirmed live.
- D-01: sibling whole-file readers (batch_read, json_query, head_tail, sort,
tr, diff, count_lines, checksum, word_count, file_info, glob, tree, base64)
were in no taint set at all, so reading /etc/shadow via batch_read produced a
TRUSTED, recallable episode.
- D-02: home credential files outside ClassifyPath's hardcoded subdir list
(e.g. ~/secrets.env) classified local_write and did not taint.
- D-05: session_search re-surfaces prior-session transcripts but never tainted.
Fix — flip the model from a sensitive-path denylist to a workspace-containment
allowlist, and broaden coverage:
- ToolCallTaints now taints a path-reading tool when ANY of its path arguments
(path / path_a / path_b / files[].path) resolves OUTSIDE the workspace trust
zone (cwd, the sandbox /workspace mount, or ~/.odek). Symlinks are resolved
so /etc -> /private/etc can't disguise an escape. Workspace reads stay
trusted (the original goal); everything else taints. Malformed args still
taint conservatively; absent/empty path defaults to the workspace.
- PathReadingTools expanded to every file-reading tool; session_search added to
AlwaysExternalTools. Provenance no longer depends on ClassifyPath.
- Defense in depth: ClassifyPath now strips the macOS /private prefix so
/private/etc|var classify the same as /etc|var for its other callers
(transcribe risk, write confinement).
Also adds the missing manager-wrapper tests (PromoteEpisode /
PendingReviewEpisodes were at 0% coverage — the report's coverage finding;
now 100%) and updates docs/SECURITY.md to describe the containment model
accurately (the report's doc-accuracy finding).
Verified: batch_read/json_query/diff of /etc, read_file of /private/etc and
~/secrets.env, and session_search all taint; workspace reads (relative + abs)
and the /workspace mount stay trusted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(memory): optional auto-approval of episode learnings (default off)
Add `memory.auto_approve_episodes` (default false). When enabled, untrusted
episodes are stamped approved at session end so they are recalled without a
manual `odek memory promote` — an explicit opt-in that trades the human review
gate for convenience in trusted/sandboxed deployments.
- New EpisodeProvenance.AutoApproved field, kept DISTINCT from UserApproved so
the audit trail shows the approval was automatic (policy), while Untrusted +
Sources are still recorded. Episode Search and PendingReview treat
AutoApproved as approved.
- The stamp is applied at write time in OnSessionEndWithProvenance only when
the flag is on and the episode is untrusted; trusted episodes are unaffected.
- Config plumbed through MemoryConfig (default false), NewMemoryManager merge,
and resolveMemory.
- docs/CONFIG.md and docs/SECURITY.md document it as a security trade-off that
disables the persistence-injection protection for episodes when on.
Tests: store-level recall of an AutoApproved episode + exclusion from pending;
the secure default is false; and the full OnSessionEnd stamping path with the
flag on (AutoApproved set, recallable, not pending) vs off (excluded, pending).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent d4d0bc5 commit 4ffdb7d
15 files changed
Lines changed: 821 additions & 63 deletions
File tree
- cmd/odek
- docs
- internal
- config
- danger
- memory
- skills
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
452 | 452 | | |
453 | 453 | | |
454 | 454 | | |
| 455 | + | |
455 | 456 | | |
456 | 457 | | |
457 | 458 | | |
| |||
477 | 478 | | |
478 | 479 | | |
479 | 480 | | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
480 | 485 | | |
481 | 486 | | |
482 | 487 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | | - | |
| 203 | + | |
| 204 | + | |
204 | 205 | | |
205 | 206 | | |
206 | 207 | | |
| |||
213 | 214 | | |
214 | 215 | | |
215 | 216 | | |
216 | | - | |
| 217 | + | |
217 | 218 | | |
218 | 219 | | |
219 | 220 | | |
220 | 221 | | |
221 | 222 | | |
| 223 | + | |
222 | 224 | | |
223 | 225 | | |
224 | 226 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
103 | | - | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
104 | 118 | | |
105 | 119 | | |
106 | 120 | | |
107 | | - | |
| 121 | + | |
108 | 122 | | |
109 | 123 | | |
110 | 124 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
649 | 649 | | |
650 | 650 | | |
651 | 651 | | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
657 | | - | |
658 | | - | |
659 | | - | |
660 | | - | |
661 | | - | |
662 | | - | |
663 | | - | |
664 | | - | |
665 | | - | |
666 | | - | |
667 | | - | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
668 | 668 | | |
669 | 669 | | |
670 | 670 | | |
| |||
850 | 850 | | |
851 | 851 | | |
852 | 852 | | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
853 | 856 | | |
854 | 857 | | |
855 | 858 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
148 | 157 | | |
149 | 158 | | |
150 | 159 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
| |||
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
247 | 300 | | |
248 | 301 | | |
249 | 302 | | |
| |||
0 commit comments