mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Replace the PATH_ALLOW_BROAD environment variable with a recorded grant, the shape agreed in review: `codebase-memory-mcp allow-root --approve-broad <path>` records an explicit human approval for one root refused as too broad (CBM_WS_DENY_TOO_SHALLOW), mirroring --approve-sensitive end to end. - cbm_workspace_grant_add() takes approve_broad next to approve_sensitive. Each flag lifts only its own verdict, absolute denials stay unliftable, and a refusal names the flag that would lift it. - The approval is stored with a '^' marker, listed by allow-root --list as "(approved broad)", and honoured by cbm_workspace_root_allowed() only for CBM_WS_DENY_TOO_SHALLOW and only on an exact path match, keeping the exact-match rule of the original change. - cbm_workspace_root_allowed() returns to its previous signature. Every caller (MCP index_repository and index-job start, auto-index, daemon sessions, the HTTP UI) reads the same grant store, so stdio and daemon sessions decide identically and nothing depends on the environment a daemon was started with. - The too-broad refusal names `allow-root --approve-broad <path>`, a command a person runs. - cbm_workspace_classify_root() and cbm_workspace_verdict_is_overridable() are unchanged. The regression test keeps its name and its exact-match, sibling, absolute and sensitive checks, now against the grant store, and adds that a broad marker never lifts a sensitive verdict. Co-authored-by: Mark Murawski <github@kobaz.net> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
170 lines
8.9 KiB
C
170 lines
8.9 KiB
C
#ifndef CBM_FOUNDATION_WORKSPACE_H
|
|
#define CBM_FOUNDATION_WORKSPACE_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
|
|
/*
|
|
* Workspace boundary policy — classify a directory as an indexing root.
|
|
*
|
|
* This is the breadth half of the boundary: it rejects roots that are too broad
|
|
* or too sensitive to be indexed as a unit. It is NOT the authorization half.
|
|
* Whether a caller may index a given tree at all is answered by the user-level
|
|
* grant store, because that is the only input a caller cannot write. A verdict
|
|
* of CBM_WS_ALLOW here means "this path is not obviously too broad", never
|
|
* "this path is authorized".
|
|
*
|
|
* Every function takes an ALREADY-CANONICALIZED absolute path. Classifying
|
|
* before symlink resolution lets `/a/b/c/d/e/link -> /` present as a deep path
|
|
* and launder straight through, so callers must resolve first.
|
|
*
|
|
* home_dir and cache_dir are passed in rather than read from the environment so
|
|
* the policy is a pure function and can be tested without touching the host.
|
|
* Either may be NULL, which disables the checks that depend on it.
|
|
*/
|
|
|
|
typedef enum {
|
|
CBM_WS_ALLOW = 0,
|
|
/* Fewer path components than the platform minimum — e.g. "/etc", "/home".
|
|
* Lifted only for one exact path by a recorded broad approval
|
|
* (allow-root --approve-broad), never by a manifest or a tool call. */
|
|
CBM_WS_DENY_TOO_SHALLOW,
|
|
/* A filesystem root, a drive root, a UNC share root, or a path that is or
|
|
* contains the cbm cache directory. Never overridable. */
|
|
CBM_WS_DENY_ABSOLUTE,
|
|
/* The home directory itself, or a credential/system directory. Overridable
|
|
* by an explicit human action, never by a manifest or a tool call. */
|
|
CBM_WS_DENY_SENSITIVE,
|
|
} cbm_ws_verdict_t;
|
|
|
|
/* Classify canonical_path as a candidate indexing root. */
|
|
cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const char *home_dir,
|
|
const char *cache_dir);
|
|
|
|
/* Stable, human-facing reason for a verdict. Never NULL. */
|
|
const char *cbm_workspace_verdict_reason(cbm_ws_verdict_t verdict);
|
|
|
|
/* True when a sensitive approval (allow-root --approve-sensitive) may override
|
|
* this verdict. Absolute denials never are. A shallow path is not overridable
|
|
* in this sense either: only an exact broad approval lifts it, see
|
|
* cbm_workspace_grant_add. */
|
|
bool cbm_workspace_verdict_is_overridable(cbm_ws_verdict_t verdict);
|
|
|
|
/* Number of path components below the volume root, ignoring repeated and
|
|
* trailing separators. "/" is 0, "/etc" is 1, "C:/repos" is 1, "//srv/share" is
|
|
* 0 (the share root itself). Exposed for tests. */
|
|
int cbm_workspace_path_depth(const char *canonical_path);
|
|
|
|
/* Canonical containment check: is abs_path at or below root_path, after symlink
|
|
* and junction resolution? The definition currently lives in src/mcp/mcp.c; it is
|
|
* declared here so the MCP handler, the HTTP UI route and the CLI all reach the
|
|
* same implementation. Two entry points evaluating the same policy separately is
|
|
* what produced the UI/MCP divergence in the first place. */
|
|
bool cbm_path_within_root(const char *root_path, const char *abs_path);
|
|
|
|
/*
|
|
* ── Grant store ──────────────────────────────────────────────────────────────
|
|
*
|
|
* The authorization half. Grants live in a user-level file under the cache
|
|
* directory and are only ever written by an explicit human action (the CLI), so
|
|
* neither an indexed repository nor a tool caller can widen its own boundary.
|
|
*
|
|
* Enforcement is deliberately two-tier, because default-deny with an empty store
|
|
* would refuse every first run:
|
|
*
|
|
* - The breadth policy above is ALWAYS enforced. Out of the box that already
|
|
* refuses "/", "/etc", "$HOME", "~/.ssh" and friends — the paths the reports
|
|
* actually demonstrate — with no configuration at all.
|
|
* - Containment in a granted root is enforced ONLY once the store is non-empty
|
|
* or a root is configured. Declaring your first root is therefore what turns
|
|
* on true confinement, and it never silently loosens: adding a root can only
|
|
* narrow what was previously unrestricted.
|
|
*/
|
|
|
|
/* Absolute path of the grant file for a cache directory. */
|
|
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz);
|
|
|
|
/* Record canonical_path as an allowed root. approve_sensitive records an explicit
|
|
* human override for a CBM_WS_DENY_SENSITIVE path; approve_broad records one for a
|
|
* CBM_WS_DENY_TOO_SHALLOW path. Each flag lifts only its own verdict, and only
|
|
* for exactly this path. Absolute denials are never grantable. Returns false and
|
|
* fills err on refusal. */
|
|
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
|
const char *canonical_path, bool approve_sensitive, bool approve_broad,
|
|
char *err, size_t err_sz);
|
|
|
|
/* Newline-separated granted roots into out. True when at least one exists. */
|
|
bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz);
|
|
|
|
/* The whole decision, used by every entry point that accepts a repo path.
|
|
* canonical_path must already be canonicalized. configured_root is the legacy
|
|
* CBM_ALLOWED_ROOT / session policy, treated as an additional grant, or NULL.
|
|
* On refusal, err receives a message that names the command which would fix it. */
|
|
bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir,
|
|
const char *cache_dir, const char *configured_root, char *err,
|
|
size_t err_sz);
|
|
|
|
/* The home and cache directories the policy should be evaluated against. Shared
|
|
* so two callers cannot derive them differently and reach different verdicts for
|
|
* the same path. Either may return NULL. */
|
|
const char *cbm_workspace_home_dir(void);
|
|
const char *cbm_workspace_cache_dir(void);
|
|
|
|
#endif /* CBM_FOUNDATION_WORKSPACE_H */
|
|
|
|
/*
|
|
* ── Per-project request manifest (.cbmpathwhitelist) ─────────────────────────
|
|
*
|
|
* A repository may ship a manifest listing outside roots it would like indexed
|
|
* alongside it. The file REQUESTS; it never GRANTS. Both attackers in this
|
|
* project's threat model can write a file inside a repository — a malicious
|
|
* indexed repo, and an agent with project write access — so a repo-local file
|
|
* cannot be authoritative without handing them the boundary.
|
|
*
|
|
* Approval is recorded user-level and keyed to a SHA-256 of the manifest bytes,
|
|
* so editing the file (a `git pull` that widens the requests, say) lapses the
|
|
* approval and asks again. Without the hash, a repo approved once could widen
|
|
* itself forever. This is direnv's model, and it is the only shape that keeps the
|
|
* ergonomics of a checked-in file without making the file a permission slip.
|
|
*/
|
|
|
|
#define CBM_WS_MANIFEST_NAME ".cbmpathwhitelist"
|
|
|
|
enum { CBM_WS_MANIFEST_MAX_ENTRIES = 64 };
|
|
|
|
typedef struct {
|
|
/* Requested roots, verbatim from the file (already control-char screened). */
|
|
char entries[CBM_WS_MANIFEST_MAX_ENTRIES][1024];
|
|
int count;
|
|
/* SHA-256 hex of the raw file bytes; empty when there is no manifest. */
|
|
char digest[65];
|
|
bool present;
|
|
} cbm_ws_manifest_t;
|
|
|
|
/* Read <project_root>/.cbmpathwhitelist. Returns false only on a malformed file
|
|
* (an entry containing control characters, or more entries than the cap); a
|
|
* missing file is success with present=false. */
|
|
bool cbm_workspace_manifest_read(const char *project_root, cbm_ws_manifest_t *out);
|
|
|
|
/* True when this exact manifest content has been approved for this project. A
|
|
* changed manifest is a different digest and so is not approved. */
|
|
bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *project_root,
|
|
const cbm_ws_manifest_t *manifest);
|
|
|
|
/* Record approval for the manifest currently on disk. Each entry is judged in
|
|
* its resolved form (links followed, `..` collapsed), since that is the
|
|
* directory that would be indexed; an entry that is relative, does not exist
|
|
* or is not a directory is refused, as is one that would not be allowable as a
|
|
* root on its own — approving a manifest must not become a way around the
|
|
* breadth policy. err names the entry as written and, when it differs, the
|
|
* directory it resolves to. */
|
|
bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
|
|
const char *project_root, char *err, size_t err_sz);
|
|
|
|
/* True when candidate is at or below an APPROVED manifest entry of project_root.
|
|
* Entries are resolved and classified again here: approval binds to the
|
|
* manifest text, and where an entry leads is read at the time it is used. An
|
|
* entry that no longer resolves matches nothing. */
|
|
bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir,
|
|
const char *project_root, const char *candidate);
|