Skip to content

feat(mcp): route project-qualified paths in the POSIX tools - #1421

Open
phernandez wants to merge 3 commits into
1398-eval-test-fixesfrom
1415-posix-project-routing
Open

feat(mcp): route project-qualified paths in the POSIX tools#1421
phernandez wants to merge 3 commits into
1398-eval-test-fixesfrom
1415-posix-project-routing

Conversation

@phernandez

Copy link
Copy Markdown
Member

Why

Implements #1415: projects become mount points in the POSIX tool namespace. Motivated by measured agent behavior in the #1398 A/B runs — tool outputs return project-prefixed identifiers, inputs demanded a separate project arg, agents faithfully quote outputs and omit the arg (shell affordances prime cwd-thinking), and the stateless default was silently wrong. In run 5, both surfaces' agents wrote a perfect relation into the wrong project.

Stacked on #1416 (1398-eval-test-fixes).

What changed

  • One shared resolver (mcp/project_context.py): first path/identifier segment naming an active project routes there with the remainder — inputs now accept exactly what outputs produce. The CLI verbs inherit it through the shared layer.
  • Explicit param wins only on agreement: project param + path prefix disagreeing refuses naming both; a workspace-qualified explicit param keeps its workspace (review catch — the agree-branch initially discarded it).
  • ls / lists active projects as the root directory — in-band discovery, the mount-point view.
  • Multi-project unqualified refusal: a path matching no project errors with the copyable project list (no project 'x' — active projects: …); single-project configs resolve unqualified paths unchanged.
  • Collision rule documented: first-segment-matches-a-project shadows a same-named folder; reachable only in single-project configs where there's no ambiguity.
  • Known scope gap (review, tracked on Project-qualified paths: projects as mount points in the tool namespace #1415): hosted/factory-mode surfaces with empty local config don't refuse yet.

Validated by the eval it came from (run at-b0442a6f5cd7, Sonnet 5)

run 5 (before) run 6 (after)
posix pass rate 5/12 10/12 — equal to rich
rich pass rate 10/12 10/12
wrong-project failures 2 rich + 6 posix zero, both surfaces

The entire wrong-scope failure class vanished. curate-connect — which agents had "solved" into the wrong project on every prior run — passed on both surfaces. Posix's metadata-search went 0/3 → 3/3 (agents brute-force it at ~2× rich's token cost, which is the remaining efficiency gap: grep/ls/find have no structured frontmatter predicates). Where the surfaces overlap cleanly, posix stays 17–24% cheaper (manual 23.8k vs 31.3k; curate 74.7k vs 89.9k). Aggregate tokens/completed: rich 72.9k, posix 88.0k (+21%, driven by the metadata brute-forcing and navigational chattiness — 13.8 vs 7.8 mean calls).

Remaining failures are genuine task behavior (decoy discrimination on both surfaces, one formatting miss, one incomplete status update, one token-budget stop) — none are scope errors.

Verification

  • ruff / format / ty — clean; tests/mcp 1030 passed; tests/cli 933 passed (21 new resolver tests + 16 routing tests; additive-only test diffs)
  • Live A/B run above; full artifacts with provenance in the run dir

🤖 Generated with Claude Code

https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T18:56:38.581513Z c940782 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfd26bafa9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/mcp/project_context.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6224e1626a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/mcp/project_context.py
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6224e1626a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/mcp/project_context.py Outdated
Comment on lines +1191 to +1192
if "/" in candidate:
detected = await detect_project_from_identifier_prefix(candidate, config, context=context)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prefer advertised mounts over workspace-qualified parsing

When a cloud/factory session advertises a project whose permalink is also an accessible workspace slug, and the next path segment names a project in that workspace (for example, /team is a mount while workspace team contains project docs), this lookup performs workspace-qualified discovery before consulting addressable_projects. Consequently, cat("team/docs/x") or ls("team/docs") routes to project docs in workspace team instead of directory docs in the advertised project team, potentially reading the wrong project's data. Fresh evidence after the prior fix is that session-mount matching remains only in the detected is None fallback, so check the advertised first-segment mount before attempting workspace-qualified parsing.

AGENTS.md reference: AGENTS.md:L337-L343

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c57ccefcf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if _session_routes_to_cloud():
project_list = await _session_project_list(context=context)
projects = (
AddressableProject(name=item.name, permalink=item.permalink)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep advertised mounts bound to their workspace

In a factory/cloud session scoped to a non-default workspace, if that workspace and an accessible default workspace both contain a project with the same permalink, this discards the project's external_id and workspace and turns the advertised mount into a bare name. The later resolve_workspace_project_identifier call resolves duplicate bare names using the cached/default workspace, so on a first call cat("docs/x") can read the default workspace even though ls("/") advertised docs from the session workspace. Preserve an unambiguous workspace-qualified identifier or external ID in the route.

AGENTS.md reference: AGENTS.md:L393-L395

Useful? React with 👍 / 👎.

Comment on lines +1251 to +1252
if detected is None and "/" in candidate:
detected = await detect_project_from_identifier_prefix(candidate, config, context=context)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recognize workspace-qualified project roots

When acme is an accessible non-current workspace with project docs and no mount is named acme, ls("acme/docs") reaches this branch, but detect_project_from_identifier_prefix recognizes workspace routes only when _split_workspace_identifier_segments finds a third nonempty path segment. With one current mount this falls through to the default and lists /acme/docs inside the current project; with several mounts it refuses. Treat the two-segment <workspace>/<project> form as that project's root with an empty remainder.

AGENTS.md reference: AGENTS.md:L393-L395

Useful? React with 👍 / 👎.

Comment on lines +1123 to +1125
projects = (
AddressableProject(name=name, permalink=generate_permalink(name))
for name in ConfigManager().config.projects

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle multi-segment project permalinks in mounts

ProjectInfoRequest and ProjectService accept project names containing /, and generate_permalink preserves that separator. In a multi-project local config containing Research/2026, the root advertises /research/2026, but the resolver compares only the first segment (research) against the full stored permalink (research/2026), so the advertised mount cannot be entered. Either reject slash-containing project names at the project boundary or route using the complete mount permalink.

AGENTS.md reference: AGENTS.md:L337-L343

Useful? React with 👍 / 👎.

phernandez and others added 3 commits September 1, 2026 13:51
Projects become mount points (#1415): every posix verb accepts
<project>/path — exactly the prefixed identifiers tool outputs and
stored permalinks produce — resolved by one shared helper the CLI
inherits. Explicit project params win only on agreement; disagreement
refuses naming both. ls with no project lists active projects as the
root directory. In multi-project configs an unqualified path that
matches no project refuses with the copyable project list; single
project configs keep resolving unqualified paths unchanged.

Motivated by measured agent behavior in the #1398 A/B runs: agents
faithfully quote prefixed output identifiers, omit the project arg
(shell affordances prime cwd thinking), and the stateless default was
silently wrong — both surfaces wrote a perfect relation into the wrong
project. Known scope gap for cloud/factory-mode surfaces noted in
review; follow-up tracked on #1415.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
The mount view and the routing resolver read different sources, so a
cloud tenant could advertise /research in ls '/' while routing asked the
local config, found nothing, refused nothing, and fell through to the
default project. In a team workspace that default is one shared mutable
is_default flag, so an unqualified call could silently read or write
another member's project.

Root cause of why the existing multi-project refusal never fired in the
hosted server: BasicMemoryConfig always materializes a placeholder 'main'
project, so config.projects is never empty and len(config.projects) > 1
was never a usable signal for a cloud session.

Both surfaces now read one addressable_projects() set — config locally,
the session's project listing in factory/cloud mode, memoized per MCP
request. The refusal counts that same set, so anything ls '/' advertises
is addressable, asserted as a property over the advertised list rather
than a hand-picked example. Nothing consults is_default.

Single-project workspaces still resolve unqualified references, and the
local path adds no new call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
Workspace-qualified parsing ran before the advertised-project lookup, so
when a project's permalink was also an accessible workspace slug, the
next segment naming a project in that workspace won. With /team
advertised and workspace team holding project docs, cat('team/docs/x')
read workspace team's docs project instead of the docs directory in the
advertised team project — another project's data, from a name ls /
promised.

The advertised list is a promise, so it now claims the first segment
first; workspace-qualified spellings resolve only when no mount matches.

The collision this creates is pinned rather than swallowed: while a
project's permalink equals a workspace slug, that workspace's other
projects lose their qualified path spelling. They stay addressable
through the project parameter, and pairing the two now raises a prefix
conflict instead of quietly agreeing, which is what teaches the escape.

Two side effects worth having: mount-prefixed paths skip workspace
discovery entirely, and ls 'research' and ls 'research/notes' now agree
on the project spelling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014pmKq6bqCi6Zp6BTHuZjrp
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez force-pushed the 1415-posix-project-routing branch from 1c57cce to c940782 Compare September 1, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant