Skip to content

feat(workspace): add PATH_ALLOW_BROAD override for too-shallow roots - #2017

Open
kobaz wants to merge 1 commit into
DeusData:mainfrom
kobaz:feat/path-allow-broad-too-shallow-override
Open

kobaz wants to merge 1 commit into
DeusData:mainfrom
kobaz:feat/path-allow-broad-too-shallow-override

Conversation

@kobaz

@kobaz kobaz commented Sep 2, 2026

Copy link
Copy Markdown

CBM_WS_DENY_TOO_SHALLOW ("path is too broad to index as one root") has no override today: cbm_workspace_verdict_is_overridable() only lifts CBM_WS_DENY_SENSITIVE, and allow-root --list already documents shallow/absolute refusals as "always-refused" rather than configurable. That default is right -- a bare top-level tree like "/etc" or "/home" is almost always a mistake -- but it leaves no way to say "I mean this one specific broad root, on purpose." That's a real case: a person who deliberately wants one combined project spanning several sibling repos under a shared parent has no path forward today short of physically restructuring their checkout.

PATH_ALLOW_BROAD names one exact canonical path that lifts a CBM_WS_DENY_TOO_SHALLOW verdict, and only that verdict:

  • exact match only, not a prefix -- naming one broad root must not quietly approve every root below it too, the same "/Users"-style breadth this depth rule exists to catch in the first place
  • process environment, not a recorded grant: it never appears in cbm_workspace_grant_list, and cbm_workspace_verdict_is_overridable() is untouched, so CBM_WS_DENY_ABSOLUTE and CBM_WS_DENY_SENSITIVE stay exactly as unliftable as before
  • read once per call site with getenv(), mirroring how CBM_ALLOWED_ROOT is already threaded into cbm_workspace_root_allowed() from each of its four callers, rather than read inside cbm_workspace_classify_root() itself -- that function is documented as a pure function of its arguments precisely so it stays host-independent and directly testable

Also updates the refusal message to name the fix, the same way the sensitive-root refusal already names allow-root --approve-sensitive.

Adds ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only alongside the existing too-shallow coverage, checking the exact-match requirement, the untouched sensitive/absolute paths, and that nothing is written to the grant store.

What does this PR do?

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (make -f Makefile.cbm test)
  • Lint passes (make -f Makefile.cbm lint-ci)
  • New behavior is covered by a test (reproduce-first for bug fixes)

CBM_WS_DENY_TOO_SHALLOW ("path is too broad to index as one root") has
no override today: cbm_workspace_verdict_is_overridable() only lifts
CBM_WS_DENY_SENSITIVE, and `allow-root --list` already documents
shallow/absolute refusals as "always-refused" rather than configurable.
That default is right -- a bare top-level tree like "/etc" or "/home"
is almost always a mistake -- but it leaves no way to say "I mean this
one specific broad root, on purpose." That's a real case: a person who
deliberately wants one combined project spanning several sibling repos
under a shared parent has no path forward today short of physically
restructuring their checkout.

PATH_ALLOW_BROAD names one exact canonical path that lifts a
CBM_WS_DENY_TOO_SHALLOW verdict, and only that verdict:

- exact match only, not a prefix -- naming one broad root must not
  quietly approve every root below it too, the same "/Users"-style
  breadth this depth rule exists to catch in the first place
- process environment, not a recorded grant: it never appears in
  cbm_workspace_grant_list, and cbm_workspace_verdict_is_overridable()
  is untouched, so CBM_WS_DENY_ABSOLUTE and CBM_WS_DENY_SENSITIVE stay
  exactly as unliftable as before
- read once per call site with getenv(), mirroring how CBM_ALLOWED_ROOT
  is already threaded into cbm_workspace_root_allowed() from each of
  its four callers, rather than read inside
  cbm_workspace_classify_root() itself -- that function is documented
  as a pure function of its arguments precisely so it stays
  host-independent and directly testable

Also updates the refusal message to name the fix, the same way the
sensitive-root refusal already names `allow-root --approve-sensitive`.

Adds ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only
alongside the existing too-shallow coverage, checking the exact-match
requirement, the untouched sensitive/absolute paths, and that nothing
is written to the grant store.

Signed-off-by: Mark Murawski <github@kobaz.net>
@kobaz
kobaz requested a review from DeusData as a code owner September 2, 2026 17:46
@DeusData

DeusData commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Actionable note ahead of the review verdict, so you are not blocked on me for it.

lint / lint is red and it is yours — unusually, this is not the false positive several contributors have hit this week. CI's own pinned formatter reports violations on the exact lines you added:

src/daemon/application.c:441:63: error: code should be clang-formatted
                                   getenv("PATH_ALLOW_BROAD"), boundary_error,

src/daemon/application.c:438-441. Running the formatter over that file should clear it.

(For context, since it has caught others: our gate requires the Homebrew LLVM clang-format; a distro or standalone clang-format-20 reports spurious whole-file drift on some large files. That is not what this is — these are your own new lines.)

On the change itself, I have verified the three boundary claims in your description and they hold:

  • the lift is guarded on verdict == CBM_WS_DENY_TOO_SHALLOW alone, so CBM_WS_DENY_SENSITIVE and CBM_WS_DENY_ABSOLUTE are untouched;
  • ws_paths_equal(canonical_path, allow_broad_root) is exact equality rather than a prefix test, so naming one broad root does not quietly approve everything beneath it;
  • the decision lives in cbm_workspace_root_allowed and not in cbm_workspace_classify_root, which stays a pure function of its arguments, and cbm_workspace_verdict_is_overridable really is untouched — the two occurrences in the diff are a context line and a comment saying it is deliberately excluded.

That is a carefully drawn boundary and it is the reason this is reviewable at all.

What I cannot decide on my own is whether we want a new environment variable that lifts a workspace security refusal. A new env var is a one-way door, and this one weakens a guard by design, however narrowly. That is a maintainer call and I have put it in front of ours; I will come back with an answer rather than leaving it open-ended.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData DeusData added enhancement New feature or request security Security vulnerabilities, hardening priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Sep 3, 2026
@DeusData DeusData mentioned this pull request Sep 3, 2026
2 tasks done

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

First, an apology: this has been open since 2 September with only a formatting note from me and no verdict. That is too long for a PR this small and this carefully argued, and it is on me, not you.

The need is real and I want it solved. "Too shallow" on POSIX means fewer than two path components, and that does not only catch /etc and /home — it also refuses /app, /code, /work, /src and /workspace, which is where a great many container images and build machines keep exactly one project. Your sibling-repos-under-a-shared-parent case is the same problem from the other side. Today the only answer is "restructure your checkout", and that is not a good answer.

You also got the hard parts right, and I want to say so specifically: exact match rather than prefix (so one allowance cannot quietly cover everything beneath it), leaving cbm_workspace_verdict_is_overridable() and the absolute/sensitive verdicts untouched, and keeping cbm_workspace_classify_root() a pure function by threading the value in from the callers instead of reading it inside. The test name says what it proves. All of that carries over unchanged to what I am about to ask for.

What I would like changed is the lever, not the rule. The comment on the verdict enum in workspace.h states the principle this boundary is built on: a refusal is "overridable by an explicit human action, never by a manifest or a tool call." An environment variable does not meet that bar here, for reasons specific to how this server runs:

  • It lives in the MCP client's config file — a manifest, and one that a coding agent with file access can edit. The grant store exists precisely so that widening the boundary takes a person running a command.
  • It leaves no record. You describe "never appears in cbm_workspace_grant_list" as a property; for a security boundary it is the drawback — allow-root --list would stop being the complete answer to "what can this installation index?".
  • In daemon mode getenv() in src/daemon/application.c reads the daemon's environment, fixed when it started, not the client's. The allowed root is carried per session (srv->allowed_root, with CBM_ALLOWED_ROOT only as the fallback) for exactly this reason; PATH_ALLOW_BROAD is not, so a user who sets it in their client config can find that a daemon started earlier never sees it — with nothing telling them why.
  • The new refusal text hands the bypass to whoever made the request. When that requester is an agent calling index_repository, "To index it anyway, set PATH_ALLOW_BROAD=…" is an instruction it may try to follow.

The shape I would merge: the same override, as a recorded grant —

codebase-memory-mcp allow-root --approve-broad <path>

mirroring --approve-sensitive end to end: parsed next to it in main.c (~line 1165), carried into cbm_workspace_grant_add() as a second explicit-approval flag, stored with the grant, and honoured in cbm_workspace_root_allowed() by the exact-match rule you already wrote — only for CBM_WS_DENY_TOO_SHALLOW, only for that exact canonical path. It then shows up in allow-root --list (ideally marked as a broad approval — the store already keeps an explicit-approval bit per grant for the sensitive case, match.exact_sensitive), it is revocable the same way, it behaves identically over stdio and through the daemon, and the refusal message can name the command — re-run allow-root with --approve-broad if that is intended — which a human runs in a terminal. CBM_WS_DENY_ABSOLUTE stays unliftable. Your test moves over almost as it is: grant without the flag → still refused; grant with it → allowed for the exact path, refused for a child and for a sibling; absolute and sensitive verdicts unaffected by it.

Two smaller things that apply either way:

  • lint / lint is still red from the formatting note on 2 September (the pinned clang-format on the lines you added). Note that main itself currently has an unrelated lint failure that a fix in flight (#2257) clears — once that lands, a rebase will show you only your own.
  • If any environment variable does survive in a later iteration, it needs the CBM_ prefix every other knob here carries; PATH_* reads like something the shell or a build system owns.

If you would rather not rework it, say so and I will carry it forward from your branch with you credited as the author of the design — but I would much prefer it to land as yours. Thank you for the care in this one; it shows.

@DeusData

Copy link
Copy Markdown
Owner

Hi @kobaz, a quick status note so nothing here is ambiguous. The review from 20 September still stands as the path forward: allow-root --approve-broad <path> as a recorded grant, reusing your exact-match rule and test. Our offer to carry it from your branch with you credited stands too; just say which you'd prefer.

Two mechanical things, whichever way it goes:

  • The branch now conflicts with main in src/mcp/mcp.c. The two cbm_workspace_root_allowed call sites have moved to around lines 11145 and 17690.
  • lint / lint is clang-format only: 11 wrap violations on your added lines, in mcp.c:12854-12856 and application.c:437-441. cppcheck is clean.

Thanks again for how carefully you drew this boundary.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority/normal Standard review queue; useful PR with ordinary maintainer urgency. security Security vulnerabilities, hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants