Skip to content

Commit efa6e79

Browse files
fix(mcp): validate write project resolution
1 parent 4f6b30e commit efa6e79

9 files changed

Lines changed: 1480 additions & 128 deletions

File tree

DOCS.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -583,13 +583,13 @@ This envelope is used consistently by `/sync/push` validation/control failures a
583583

584584
## MCP Project Resolution
585585

586-
Engram resolves the project at MCP tool call time from the **server process working directory** (cwd), not at MCP startup and not from arbitrary LLM arguments. This eliminates project drift caused by agents supplying different names for the same repo.
586+
Engram resolves the project at MCP tool call time. The default source is the **server process working directory** (cwd), not MCP startup state, but some write tools have stronger context: `mem_session_start(directory=...)` resolves from the provided directory, and `mem_save` may use a validated explicit `project` or an existing `session_id` project before falling back to cwd detection. The explicit field is treated as a **validated selection**, not a free-form creation hint. This eliminates project drift caused by agents supplying different names for the same repo.
587587

588588
### Detection algorithm
589589

590590
| Case | Condition | Source | Project |
591591
|------|-----------|--------|---------|
592-
| 1 | `.engram/config.json` exists at the repo root, or at cwd outside git | `config` | `project_name` from config |
592+
| 1 | nearest `.engram/config.json` exists within the enclosing git root, or at cwd outside git | `config` | `project_name` from config |
593593
| 2 | cwd is a git root with `origin` remote | `git_remote` | repo name from remote URL |
594594
| 3 | cwd is inside a git repo (subdirectory) | `git_root` | git root's directory basename |
595595
| 4 | cwd has exactly one git-repo child | `git_child` | child repo name (warning included) |
@@ -617,13 +617,28 @@ Exceptions:
617617
- `mem_current_project` returns detection fields directly (`project`, `project_source`, `project_path`, `cwd`, `available_projects`, optional `warning` / `error_hint`) and does not wrap them in `result`.
618618
- `mem_doctor` returns the same JSON report shape as `engram doctor --json`; it uses read-project resolution before running diagnostics but does not wrap the report in the common MCP envelope.
619619

620-
### Write tools (cwd-detected project, limited recovery override)
620+
### Write tools (explicit/session/cwd project resolution)
621621

622-
`mem_session_start`, `mem_session_end`, `mem_session_summary`, and `mem_capture_passive` auto-detect project from cwd. Any `project` argument the LLM sends is ignored.
622+
`mem_session_start` resolves from its explicit `directory` argument when supplied; otherwise it auto-detects from cwd. `mem_session_end`, `mem_session_summary`, and `mem_capture_passive` auto-detect project from cwd. Any `project` argument the LLM sends to these tools is ignored.
623623

624624
`mem_update` uses ID-based updates and auto-detects project only for response envelope metadata. Its public schema does not expose `project`; raw legacy clients may still send a non-empty `project` argument, and the handler tolerates it as an observation project update for compatibility.
625625

626-
`mem_save` and `mem_save_prompt` also auto-detect project from cwd by default, but they accept one narrow recovery override: after a previous `ambiguous_project` error, the agent may retry with `project=<one of available_projects>` and `project_choice_reason=user_selected_after_ambiguous_project`. Without that exact reason, LLM-supplied `project` is ignored so routine writes cannot drift across project names.
626+
`mem_save` resolves writes by precedence: validated explicit `project`, project already associated with `session_id`, repo/cwd detection (nearest `.engram/config.json` within the enclosing git root, git remote/root/child), then directory-basename fallback.
627+
628+
Guardrails:
629+
- Invalid explicit `project` names fail loudly instead of silently falling back.
630+
- Valid-looking explicit `project` names are accepted only when backed by known context: an existing local project in the store, a matching existing session project, the nearest resolvable `.engram/config.json`, or exact ambiguous-project recovery after the user selected one available project.
631+
- An unbacked explicit `project` fails loudly and does not create a new bucket.
632+
- If a non-empty `session_id` is supplied and no session exists, `mem_save` fails with a structured error and does not write.
633+
- If both explicit `project` and `session_id` are supplied, they must resolve to the same normalized project or `mem_save` fails with a structured error and does not write.
634+
- `project_choice_reason=user_selected_after_ambiguous_project` is only honored when cwd resolution is actually ambiguous. On a non-ambiguous cwd, stale recovery flags do not override explicit-project precedence or session mismatch validation.
635+
- If ambiguous-project recovery is active, `project` must exactly match one of the previously returned `available_projects`; invented or normalized guesses are rejected.
636+
- Exact ambiguous-project choices can still fail with `project_name_collision` when multiple available names collapse to the same stored project bucket after normalization. Rename or disambiguate the colliding projects before retrying.
637+
- Ordinary explicit `mem_save(project=...)` calls can also fail with `project_name_collision` when the raw explicit name collapses into an existing config-backed, session-backed, or store-backed project bucket, such as `foo--bar` colliding with `foo-bar`.
638+
639+
For monorepos, detection now honors the **nearest** `.engram/config.json` at or below the enclosing git root. That lets `repo/backend/.engram/config.json` and `repo/frontend/.engram/config.json` behave as independent projects without letting `~/.engram/config.json` leak into nested workspaces.
640+
641+
`mem_save_prompt` keeps the older cwd/default behavior by default and only uses `project` for the narrow ambiguous-project recovery override: after a previous `ambiguous_project` error, the agent may retry with `project=<one of available_projects>` and `project_choice_reason=user_selected_after_ambiguous_project`.
627642

628643
### Read tools (optional project override)
629644

@@ -729,6 +744,7 @@ Save comprehensive end-of-session summary:
729744
## Instructions
730745
## Discoveries
731746
## Accomplished
747+
## Next Steps
732748
## Relevant Files
733749
```
734750

@@ -900,7 +916,7 @@ All project names are normalized on write and read: **lowercase**, **trimmed**,
900916
### Auto-detection
901917

902918
MCP tools resolve project names at call time using the shared detection chain:
903-
1. `.engram/config.json` `project_name` at the repo root, or at cwd outside git
919+
1. Nearest `.engram/config.json` `project_name` within the enclosing git root, or at cwd outside git
904920
2. Git remote origin URL (extracts repo name)
905921
3. Git repository root directory name
906922
4. Single git-repo child of cwd
@@ -990,7 +1006,7 @@ Instead of a separate LLM service, the agent itself compresses observations. The
9901006
**Two levels:**
9911007

9921008
- **Per-action** (`mem_save`): Structured summaries (What/Why/Where/Learned)
993-
- **Session summary** (`mem_session_summary`): Comprehensive end-of-session summary (Goal/Instructions/Discoveries/Accomplished/Files)
1009+
- **Session summary** (`mem_session_summary`): Comprehensive end-of-session summary (Goal/Instructions/Discoveries/Accomplished/Next Steps/Files)
9941010

9951011
### No Raw Tool-Call Auto-Capture
9961012

docs/AGENT-SETUP.md

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ Engram works with **any MCP-compatible agent**. Pick your agent below.
2727

2828
### Project auto-detection (important)
2929

30-
**Do not pass `project` to write tools during normal operation.** Engram auto-detects the project from the server's working directory (cwd) using `.engram/config.json`, git remote URL, repo root name, or directory basename. Agents that include `project` in `mem_save` or similar calls will have that argument ignored unless they are using the explicit ambiguous-project recovery flow below.
30+
`mem_save` resolves its write project in this order: validated explicit `project`, existing `session_id` association, repo `.engram/config.json`/cwd detection, then directory-basename fallback. Use an explicit `project` when you intentionally want to target a known project; invalid or unbacked names fail loudly instead of silently falling back.
31+
32+
Other write tools still primarily use cwd/repo detection unless their schema says otherwise. Start the MCP server from the repo or add `.engram/config.json` when you want deterministic default writes.
3133

3234
To lock write tools to the canonical project for a repo, add `.engram/config.json` at the repo root:
3335

@@ -37,7 +39,9 @@ To lock write tools to the canonical project for a repo, add `.engram/config.jso
3739
}
3840
```
3941

40-
When present, `project_name` is used for writes from the repo and its subdirectories and overrides lower-confidence cwd/git detection. This is a write lock only: read tools can still use an explicit `project` filter when you need to query another existing project. Empty or invalid `project_name` values fail writes loudly instead of falling back silently.
42+
When present, `project_name` is the default auto-detected target for writes from the repo and its subdirectories and overrides lower-confidence cwd/git detection. It is NOT an unbreakable lock against an explicit `mem_save(project=...)`, but explicit project writes are still validated against known context before they are accepted. Read tools can still use an explicit `project` filter when you need to query another existing project. Empty or invalid `project_name` values fail writes loudly instead of falling back silently.
43+
44+
For monorepos, prefer subproject configs such as `backend/.engram/config.json` and `frontend/.engram/config.json`. Engram uses the **nearest** config under the enclosing git root, so backend/frontend can resolve as separate projects while still blocking `$HOME/.engram/config.json` ancestor leakage.
4145

4246
**Recommended first call:** `mem_current_project` — confirms which project Engram detected before you start writing. Returns `project_source` (how it was detected) and `available_projects` (if cwd is ambiguous).
4347

@@ -63,7 +67,7 @@ The first write fails with an error like:
6367
}
6468
```
6569

66-
Ask the user to choose exactly one value from `available_projects`, then retry only `mem_save` or `mem_save_prompt` with both recovery fields:
70+
Ask the user to choose exactly one value from `available_projects`. For ambiguous-project recovery, retry `mem_save` with BOTH fields:
6771

6872
```json
6973
{
@@ -72,7 +76,7 @@ Ask the user to choose exactly one value from `available_projects`, then retry o
7276
}
7377
```
7478

75-
On success, Engram writes to the selected project and reports the recovery source:
79+
On success, `mem_save` writes to the selected project and reports the recovery source:
7680

7781
```json
7882
{
@@ -82,36 +86,63 @@ On success, Engram writes to the selected project and reports the recovery sourc
8286
}
8387
```
8488

89+
If the exact choices normalize to the same stored project bucket, Engram returns `project_name_collision` instead of writing. Ask the user to rename or disambiguate the colliding projects before retrying.
90+
8591
### Ambiguous-project recovery rules
8692

87-
This is a narrow rescue path, not a free-form project override:
93+
Normal `mem_save` precedence:
94+
95+
- explicit `project`
96+
- existing `session_id` project
97+
- repo `.engram/config.json` / cwd detection
98+
- directory-basename fallback
8899

89-
- Recovery is accepted only after cwd detection failed with `ambiguous_project`.
90-
- `project_choice_reason` must be exactly `user_selected_after_ambiguous_project`.
91-
- `project`, after trimming surrounding whitespace, must exactly match one of the reported `available_projects`.
92-
- Normalized variants and guesses are rejected: if `available_projects` contains `foo--bar`, retry with `foo--bar`, not `foo-bar`.
93-
- Empty or whitespace-only choices are rejected.
94-
- In all non-ambiguous cases, `.engram/config.json`/git/cwd detection remains authoritative and the explicit `project` field is ignored.
100+
Additional rules:
101+
102+
- `project`, after trimming surrounding whitespace, must be a name, not a path.
103+
- Empty, whitespace-only, path-like, or control-character names are rejected.
104+
- Names are normalized the same way the store normalizes projects.
105+
- Invalid explicit `project` names fail loudly.
106+
- Valid-looking explicit `project` names are accepted only when backed by known context: an existing local project in the store, a matching existing session project, the nearest resolvable repo/subproject `.engram/config.json`, or exact ambiguous-project recovery.
107+
- Unbacked explicit `project` values are rejected; `mem_save(project=...)` is a validated selection, not an arbitrary project-creation path.
108+
- If `session_id` is provided and no session exists, `mem_save` fails loudly instead of falling back to cwd/config detection.
109+
- If both explicit `project` and `session_id` are supplied, they must match after normalization or the write is rejected.
110+
- `project_choice_reason=user_selected_after_ambiguous_project` is only valid when cwd detection is actually ambiguous; stale flags on a non-ambiguous cwd do not override explicit `project` precedence or session mismatch checks.
111+
- When ambiguous-project recovery is active, `project` must exactly match one of `available_projects`; invented or normalized guesses are rejected.
112+
- Exact choices may still fail with `project_name_collision` when two available names collapse to the same normalized storage bucket, such as `foo--bar` and `foo-bar`.
113+
- Ordinary explicit `mem_save(project=...)` calls may also fail with `project_name_collision` when the raw explicit name collapses into an existing config-backed, session-backed, or store-backed project bucket, such as `foo--bar` versus `foo-bar`.
114+
115+
`mem_save_prompt` keeps the older cwd/default behavior. Its `project` field is only for ambiguous-project recovery together with `project_choice_reason=user_selected_after_ambiguous_project`.
95116

96117
Mental model:
97118

98119
```text
99-
mem_save fails with ambiguous_project
120+
normal mem_save call
100121
101-
Engram returns available_projects
122+
explicit project wins when valid
102123
103-
agent asks the user to choose one exact value
124+
otherwise existing session project wins
104125
105-
agent retries with project + project_choice_reason
126+
otherwise repo/cwd detection picks the default target
127+
```
128+
129+
Ambiguous recovery:
130+
131+
```text
132+
write fails with ambiguous_project
106133
107-
Engram validates the choice came from ambiguity
134+
user chooses one exact value from available_projects
108135
109-
Engram saves to the selected project
136+
agent retries with project + project_choice_reason
137+
138+
Engram validates the exact choice and writes to that repo
110139
```
111140

141+
If validation returns `project_name_collision`, do not guess. Ask the user to disambiguate the project names first.
142+
112143
Alternatives: `cd` into the target repo before starting the MCP server, or add repo `.engram/config.json`.
113144

114-
**Read tools** (`mem_search`, `mem_context`, `mem_get_observation`, `mem_stats`, `mem_timeline`) accept an optional `project` override validated against the store. Omit it to auto-detect.
145+
**Read tools** (`mem_search`, `mem_context`, `mem_stats`, `mem_timeline`, `mem_doctor`) accept an optional `project` override validated against the store. Omit it to auto-detect. `mem_get_observation` is ID-based and does not accept a `project` override.
115146

116147
---
117148

@@ -127,7 +158,7 @@ engram setup opencode
127158

128159
This does three things:
129160
1. Copies the plugin to `~/.config/opencode/plugins/engram.ts` (session tracking, Memory Protocol, compaction recovery)
130-
2. Adds the `engram` MCP server entry to your `opencode.json` with `--tools=agent` (14 agent-facing tools)
161+
2. Adds the `engram` MCP server entry to your `opencode.json` with `--tools=agent` (15 agent-facing tools)
131162
3. Adds `opencode-subagent-statusline` to your `tui.json` or `tui.jsonc` so OpenCode shows sub-agent activity in the sidebar/home footer
132163

133164
The plugin auto-starts the HTTP server if needed for session tracking. If your environment blocks background processes, run it manually:
@@ -138,7 +169,7 @@ engram serve &
138169

139170
> **Windows**: OpenCode uses `~/.config/opencode/` on Windows too (it does not read `%APPDATA%\opencode\`). `engram setup opencode` writes to `~/.config/opencode/plugins/` and `~/.config/opencode/opencode.json`. To run the server in the background: `Start-Process engram -ArgumentList "serve" -WindowStyle Hidden` (PowerShell) or just run `engram serve` in a separate terminal.
140171
141-
**Alternative: Manual MCP-only setup** (no plugin, all 18 tools by default):
172+
**Alternative: Manual MCP-only setup** (no plugin, all 19 tools by default):
142173

143174
Add to your `opencode.json` (global: `~/.config/opencode/opencode.json` on all platforms, or project-level):
144175

@@ -179,7 +210,7 @@ engram setup claude-code
179210

180211
During setup, you'll be asked whether to add engram's agent-profile MCP tools to `~/.claude/settings.json` `permissions.allow`. The setup writes entries for both the durable user-level MCP server id (`mcp__engram__...`) and the plugin-scoped server id used by older Claude Code plugin installs, so re-running setup repairs stale or incomplete allowlists without adding startup delay.
181212

182-
**Option C: Bare MCP** — all 18 tools by default, no session management:
213+
**Option C: Bare MCP** — all 19 tools by default, no session management:
183214

184215
Add to your `.claude/settings.json` (project) or `~/.claude/settings.json` (global):
185216

docs/ARCHITECTURE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Engram trusts the **agent** to decide what's worth remembering — not a firehos
4242
```
4343
Session starts → Agent works → Agent saves memories proactively
4444
45-
Session ends → Agent writes session summary (Goal/Discoveries/Accomplished/Files)
45+
Session ends → Agent writes session summary (Goal/Discoveries/Accomplished/Next Steps/Files)
4646
4747
Next session starts → Previous session context is injected automatically
4848
```
@@ -69,6 +69,9 @@ Next session starts → Previous session context is injected automatically
6969
| `mem_capture_passive` | Extract learnings from text output |
7070
| `mem_merge_projects` | Merge project name variants into canonical name (admin) |
7171
| `mem_current_project` | Detect project from cwd — never errors, recommended first call |
72+
| `mem_doctor` | Run read-only operational diagnostics for project detection and store health |
73+
| `mem_judge` | Record a verdict for a pending memory conflict surfaced by `mem_save` |
74+
| `mem_compare` | Persist a semantic relation verdict between two existing observations |
7275

7376
---
7477

@@ -126,7 +129,7 @@ engram/
126129
├── internal/
127130
│ ├── store/store.go # Core: SQLite + FTS5 + all data ops
128131
│ ├── server/server.go # HTTP REST API (port 7437)
129-
│ ├── mcp/mcp.go # MCP stdio server (18 tools)
132+
│ ├── mcp/mcp.go # MCP stdio server (19 tools)
130133
│ ├── setup/setup.go # Agent plugin installer (go:embed)
131134
│ ├── cloud/ # Optional cloud runtime (Postgres + dashboard)
132135
│ │ ├── cloudserver/ # /sync API + dashboard mount + auth/session bridge

docs/PLUGINS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ claude --plugin-dir ./plugin/claude-code
8282

8383
| Feature | Bare MCP | Plugin |
8484
|---------|----------|--------|
85-
| MCP tools available | 18 default (`engram mcp`) | 14 agent-profile tools (`engram mcp --tools=agent`) |
85+
| MCP tools available | 19 default (`engram mcp`) | 15 agent-profile tools (`engram mcp --tools=agent`) |
8686
| Session tracking (auto-start) |||
8787
| Auto-import git-synced memories |||
8888
| Compaction recovery |||

0 commit comments

Comments
 (0)