mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(workspace): record broad approvals with allow-root --approve-broad
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>
This commit is contained in:
1 parent
b9df26094f
commit
3e2de74e98
10 files changed
+227
-133
No files matched your search
@@ -237,6 +237,15 @@ root because they are too broad or too sensitive to index as a unit:
|
||||
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
|
||||
`.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.
|
||||
|
||||
A single top-level directory can still be indexed as one root on purpose, for
|
||||
example a container or build machine that keeps its project in `/work`, `/app` or
|
||||
`/src`, by approving that exact path in a terminal:
|
||||
`codebase-memory-mcp allow-root --approve-broad /work`. The approval is recorded
|
||||
like any other allowed root, listed as `(approved broad)` by `allow-root --list`,
|
||||
and covers no other top-level directory; as with any recorded root, indexing is
|
||||
then confined to the recorded roots. Filesystem, drive and share roots cannot be
|
||||
approved.
|
||||
|
||||
Two limits are worth stating plainly. This constrains *scope*, not
|
||||
*sensitivity*: inside a root that is allowed, every file the process can read may
|
||||
be indexed and later returned. And the credential list is a denylist, so it
|
||||
|
||||
@@ -521,8 +521,7 @@ static bool application_session_workspace_allowed(const cbm_daemon_application_s
|
||||
bool allowed =
|
||||
root && root[0] &&
|
||||
cbm_workspace_root_allowed(root, cbm_workspace_home_dir(), cbm_workspace_cache_dir(),
|
||||
cbm_mcp_server_allowed_root(session->mcp),
|
||||
getenv("PATH_ALLOW_BROAD"), boundary_error,
|
||||
cbm_mcp_server_allowed_root(session->mcp), boundary_error,
|
||||
sizeof(boundary_error));
|
||||
if (!allowed) {
|
||||
cbm_log_warn("daemon.workspace.skipped", "operation", operation, "detail",
|
||||
|
||||
+109
-60
@@ -383,8 +383,18 @@ enum { WS_LINE_MAX = 4096 };
|
||||
/* A line may carry a leading '!' meaning "a person explicitly approved this
|
||||
* sensitive root". The marker is stored rather than inferred so re-classifying
|
||||
* later (a new credential name is added to the list, say) cannot silently
|
||||
* upgrade an ordinary grant into a sensitive one. */
|
||||
* upgrade an ordinary grant into a sensitive one. A leading '^' is the same
|
||||
* kind of record for a root refused as too broad (allow-root --approve-broad).
|
||||
* Neither character can begin a canonical absolute path, so a marked line can
|
||||
* never be misread as an ordinary grant. */
|
||||
static const char WS_SENSITIVE_MARK = '!';
|
||||
static const char WS_BROAD_MARK = '^';
|
||||
|
||||
typedef enum {
|
||||
WS_GRANT_ORDINARY = 0,
|
||||
WS_GRANT_SENSITIVE, /* explicit approval of a CBM_WS_DENY_SENSITIVE root */
|
||||
WS_GRANT_BROAD, /* explicit approval of a CBM_WS_DENY_TOO_SHALLOW root */
|
||||
} ws_grant_kind_t;
|
||||
|
||||
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
|
||||
if (!cache_dir || !cache_dir[0] || !out || out_sz == 0) {
|
||||
@@ -397,7 +407,8 @@ bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
|
||||
/* Walk the grant file, invoking visit for each entry. visit returns true to stop.
|
||||
* Returns the number of entries seen. */
|
||||
static int ws_grant_walk(const char *cache_dir,
|
||||
bool (*visit)(const char *root, bool sensitive, void *ctx), void *ctx) {
|
||||
bool (*visit)(const char *root, ws_grant_kind_t kind, void *ctx),
|
||||
void *ctx) {
|
||||
char store[WS_LINE_MAX];
|
||||
if (!cbm_workspace_grant_path(cache_dir, store, sizeof(store))) {
|
||||
return 0;
|
||||
@@ -416,13 +427,18 @@ static int ws_grant_walk(const char *cache_dir,
|
||||
if (len == 0 || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
bool sensitive = line[0] == WS_SENSITIVE_MARK;
|
||||
const char *root = sensitive ? line + 1 : line;
|
||||
ws_grant_kind_t kind = WS_GRANT_ORDINARY;
|
||||
if (line[0] == WS_SENSITIVE_MARK) {
|
||||
kind = WS_GRANT_SENSITIVE;
|
||||
} else if (line[0] == WS_BROAD_MARK) {
|
||||
kind = WS_GRANT_BROAD;
|
||||
}
|
||||
const char *root = kind == WS_GRANT_ORDINARY ? line : line + 1;
|
||||
if (!root[0]) {
|
||||
continue;
|
||||
}
|
||||
seen++;
|
||||
if (visit && visit(root, sensitive, ctx)) {
|
||||
if (visit && visit(root, kind, ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -434,14 +450,21 @@ typedef struct {
|
||||
const char *candidate;
|
||||
bool contained; /* candidate is at or below some granted root */
|
||||
bool exact_sensitive; /* candidate is itself a grant marked sensitive */
|
||||
bool exact_broad; /* candidate is itself a grant marked broad */
|
||||
} ws_match_t;
|
||||
|
||||
static bool ws_match_visit(const char *root, bool sensitive, void *ctx) {
|
||||
static bool ws_match_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
|
||||
ws_match_t *m = ctx;
|
||||
if (cbm_path_within_root(root, m->candidate)) {
|
||||
m->contained = true;
|
||||
if (sensitive && ws_paths_equal(root, m->candidate)) {
|
||||
m->exact_sensitive = true;
|
||||
/* Exact equality, never a prefix: an approval names one root, and must
|
||||
* not quietly cover every root below it. */
|
||||
if (kind != WS_GRANT_ORDINARY && ws_paths_equal(root, m->candidate)) {
|
||||
if (kind == WS_GRANT_SENSITIVE) {
|
||||
m->exact_sensitive = true;
|
||||
} else {
|
||||
m->exact_broad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; /* visit every entry: a later one may be the exact match */
|
||||
@@ -453,10 +476,15 @@ typedef struct {
|
||||
size_t used;
|
||||
} ws_list_t;
|
||||
|
||||
static bool ws_list_visit(const char *root, bool sensitive, void *ctx) {
|
||||
static bool ws_list_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
|
||||
ws_list_t *l = ctx;
|
||||
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n",
|
||||
sensitive ? "(approved) " : "", root);
|
||||
const char *label = "";
|
||||
if (kind == WS_GRANT_SENSITIVE) {
|
||||
label = "(approved) ";
|
||||
} else if (kind == WS_GRANT_BROAD) {
|
||||
label = "(approved broad) ";
|
||||
}
|
||||
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n", label, root);
|
||||
if (n > 0 && (size_t)n < l->out_sz - l->used) {
|
||||
l->used += (size_t)n;
|
||||
}
|
||||
@@ -472,9 +500,49 @@ bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz) {
|
||||
return ws_grant_walk(cache_dir, ws_list_visit, &l) > 0;
|
||||
}
|
||||
|
||||
/* The allow-root flag that records an explicit approval for a refused verdict,
|
||||
* or NULL when nothing can lift it (absolute denials). */
|
||||
static const char *ws_verdict_approval_flag(cbm_ws_verdict_t verdict) {
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
|
||||
return "--approve-broad";
|
||||
}
|
||||
if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
return "--approve-sensitive";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* True when a refused verdict carries the explicit approval that lifts it. Each
|
||||
* flag lifts only its own verdict: --approve-sensitive never widens breadth and
|
||||
* --approve-broad never widens sensitivity; absolute denials lift for neither.
|
||||
* Refuses by default and names the flag, rather than asking a question whose
|
||||
* answer we cannot authenticate. */
|
||||
static bool ws_grant_verdict_approved(cbm_ws_verdict_t verdict, bool approve_sensitive,
|
||||
bool approve_broad, char *err, size_t err_sz) {
|
||||
bool approved = false;
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
|
||||
approved = approve_broad;
|
||||
} else if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
approved = approve_sensitive;
|
||||
}
|
||||
if (approved) {
|
||||
return true;
|
||||
}
|
||||
if (err) {
|
||||
const char *flag = ws_verdict_approval_flag(verdict);
|
||||
if (flag) {
|
||||
snprintf(err, err_sz, "%s; re-run with %s if that is intended",
|
||||
cbm_workspace_verdict_reason(verdict), flag);
|
||||
} else {
|
||||
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
const char *canonical_path, bool approve_sensitive, char *err,
|
||||
size_t err_sz) {
|
||||
const char *canonical_path, bool approve_sensitive, bool approve_broad,
|
||||
char *err, size_t err_sz) {
|
||||
if (err && err_sz) {
|
||||
err[0] = '\0';
|
||||
}
|
||||
@@ -486,33 +554,22 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
|
||||
cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_path, home_dir, cache_dir);
|
||||
if (verdict != CBM_WS_ALLOW) {
|
||||
if (!cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
if (err) {
|
||||
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!approve_sensitive) {
|
||||
/* Refuse by default and name the flag, rather than asking a question
|
||||
* whose answer we cannot authenticate. */
|
||||
if (err) {
|
||||
snprintf(err, err_sz, "%s; re-run with --approve-sensitive if that is intended",
|
||||
cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (verdict != CBM_WS_ALLOW &&
|
||||
!ws_grant_verdict_approved(verdict, approve_sensitive, approve_broad, err, err_sz)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sensitive = verdict == CBM_WS_DENY_SENSITIVE;
|
||||
bool broad = verdict == CBM_WS_DENY_TOO_SHALLOW;
|
||||
|
||||
/* Already present is success, not a duplicate error. A sensitive approval
|
||||
/* Already present is success, not a duplicate error. An explicit approval
|
||||
* is narrower: an old ordinary grant (or an ordinary ancestor) establishes
|
||||
* containment, but does not record the exact human-approved exception that
|
||||
* cbm_workspace_root_allowed requires. Append that exact marked entry once. */
|
||||
ws_match_t existing = {canonical_path, false, false};
|
||||
ws_match_t existing = {canonical_path, false, false, false};
|
||||
(void)ws_grant_walk(cache_dir, ws_match_visit, &existing);
|
||||
if (existing.contained && (!sensitive || existing.exact_sensitive)) {
|
||||
if (existing.contained && (!sensitive || existing.exact_sensitive) &&
|
||||
(!broad || existing.exact_broad)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -530,7 +587,13 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
(void)fprintf(f, "%s%s\n", sensitive ? "!" : "", canonical_path);
|
||||
char mark[2] = {'\0', '\0'};
|
||||
if (sensitive) {
|
||||
mark[0] = WS_SENSITIVE_MARK;
|
||||
} else if (broad) {
|
||||
mark[0] = WS_BROAD_MARK;
|
||||
}
|
||||
(void)fprintf(f, "%s%s\n", mark, canonical_path);
|
||||
bool ok = fclose(f) == 0;
|
||||
if (!ok && err) {
|
||||
snprintf(err, err_sz, "cannot write %s", store);
|
||||
@@ -539,8 +602,8 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
|
||||
bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir,
|
||||
const char *cache_dir, const char *configured_root,
|
||||
const char *allow_broad_root, char *err, size_t err_sz) {
|
||||
const char *cache_dir, const char *configured_root, char *err,
|
||||
size_t err_sz) {
|
||||
if (err && err_sz) {
|
||||
err[0] = '\0';
|
||||
}
|
||||
@@ -551,7 +614,7 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
|
||||
return false;
|
||||
}
|
||||
|
||||
ws_match_t match = {canonical_path, false, false};
|
||||
ws_match_t match = {canonical_path, false, false, false};
|
||||
int grants = ws_grant_walk(cache_dir, ws_match_visit, &match);
|
||||
|
||||
/* A configured root behaves as an additional grant so existing
|
||||
@@ -596,37 +659,23 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
|
||||
return true;
|
||||
}
|
||||
/* An explicit human approval recorded for exactly this path is the only thing
|
||||
* that lifts a sensitive refusal. Absolute refusals cannot be lifted at all. */
|
||||
* that lifts a sensitive or a too-broad refusal, each by its own marker: a
|
||||
* broad approval says "index this whole tree on purpose", which is not a
|
||||
* statement about credentials, and the reverse. Exact match, never a prefix,
|
||||
* so one broad approval cannot cover every root below it — the "/Users"-style
|
||||
* breadth the depth rule exists to catch. Absolute refusals cannot be lifted. */
|
||||
if (verdict == CBM_WS_DENY_SENSITIVE && match.exact_sensitive) {
|
||||
return true;
|
||||
}
|
||||
/* PATH_ALLOW_BROAD is a separate, narrower lever from the grant store above:
|
||||
* an operator has named this exact root as one they have deliberately chosen
|
||||
* to index whole, accepting that it may span multiple unrelated projects --
|
||||
* the tradeoff CBM_WS_DENY_TOO_SHALLOW normally refuses on their behalf. It
|
||||
* must match canonical_path exactly, the same way an exact sensitive grant
|
||||
* does above: a prefix match would let one broad allowance quietly cover
|
||||
* every root below it too, which is the "/Users"-style breadth this rule
|
||||
* exists to catch. It is environment, not a recorded grant, so it is not
|
||||
* listed by cbm_workspace_grant_list and cannot lift CBM_WS_DENY_ABSOLUTE or
|
||||
* CBM_WS_DENY_SENSITIVE. */
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW && allow_broad_root && allow_broad_root[0] &&
|
||||
ws_paths_equal(canonical_path, allow_broad_root)) {
|
||||
if (verdict == CBM_WS_DENY_TOO_SHALLOW && match.exact_broad) {
|
||||
return true;
|
||||
}
|
||||
if (err) {
|
||||
if (cbm_workspace_verdict_is_overridable(verdict)) {
|
||||
const char *flag = ws_verdict_approval_flag(verdict);
|
||||
if (flag) {
|
||||
snprintf(err, err_sz,
|
||||
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root "
|
||||
"--approve-sensitive %s",
|
||||
canonical_path, cbm_workspace_verdict_reason(verdict), canonical_path);
|
||||
} else if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
|
||||
/* No allow-root command exists for this one -- the grant store is a
|
||||
* human-approved list of real project roots, and this path is being
|
||||
* refused for being the opposite of that. PATH_ALLOW_BROAD is the
|
||||
* deliberate, narrower escape hatch: set it to this exact path. */
|
||||
snprintf(err, err_sz, "%s: %s. To index it anyway, set PATH_ALLOW_BROAD=%s",
|
||||
canonical_path, cbm_workspace_verdict_reason(verdict), canonical_path);
|
||||
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root %s %s",
|
||||
canonical_path, cbm_workspace_verdict_reason(verdict), flag, canonical_path);
|
||||
} else {
|
||||
snprintf(err, err_sz, "%s: %s", canonical_path, cbm_workspace_verdict_reason(verdict));
|
||||
}
|
||||
|
||||
+16
-15
@@ -25,7 +25,9 @@
|
||||
|
||||
typedef enum {
|
||||
CBM_WS_ALLOW = 0,
|
||||
/* Fewer path components than the platform minimum — e.g. "/etc", "/home". */
|
||||
/* 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. */
|
||||
@@ -42,8 +44,10 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
|
||||
/* Stable, human-facing reason for a verdict. Never NULL. */
|
||||
const char *cbm_workspace_verdict_reason(cbm_ws_verdict_t verdict);
|
||||
|
||||
/* True when an explicit human action may override this verdict. Absolute denials
|
||||
* and shallow paths are never overridable; sensitive ones are. */
|
||||
/* 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
|
||||
@@ -81,11 +85,13 @@ bool cbm_path_within_root(const char *root_path, const char *abs_path);
|
||||
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; absolute denials and shallow
|
||||
* paths are never grantable. Returns false and fills err on refusal. */
|
||||
* 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, char *err,
|
||||
size_t err_sz);
|
||||
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);
|
||||
@@ -93,15 +99,10 @@ 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.
|
||||
* allow_broad_root is PATH_ALLOW_BROAD (or NULL): an operator-set exact match
|
||||
* for canonical_path that lifts a CBM_WS_DENY_TOO_SHALLOW verdict only. It is
|
||||
* process environment, not a recorded grant, so it does not appear in
|
||||
* cbm_workspace_grant_list and does not widen CBM_WS_DENY_ABSOLUTE or
|
||||
* CBM_WS_DENY_SENSITIVE. On refusal, err receives a message that names the
|
||||
* command which would fix it. */
|
||||
* 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,
|
||||
const char *allow_broad_root, char *err, size_t err_sz);
|
||||
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
|
||||
|
||||
+11
-4
@@ -1175,21 +1175,27 @@ static void print_help(void) {
|
||||
|
||||
/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help).
|
||||
* Returns -1 if no subcommand matched, otherwise the exit code. */
|
||||
/* `allow-root [--approve-sensitive] <path>` — record an indexing root.
|
||||
/* `allow-root [--approve-sensitive | --approve-broad] <path>` — record an
|
||||
* indexing root.
|
||||
*
|
||||
* Enrollment lives here, in a command a person types, and deliberately nowhere
|
||||
* else: the whole point of the grant store is that neither an indexed repository
|
||||
* nor a tool caller can widen its own boundary. A confirmation delivered through
|
||||
* the MCP surface would be answered by the same agent that may have been
|
||||
* influenced, so it would not be a human decision at all. */
|
||||
* influenced, so it would not be a human decision at all. --approve-broad is the
|
||||
* same kind of record for one exact root refused as too broad ("/work", "/app"),
|
||||
* so it is listed, revocable and seen identically by stdio and daemon sessions. */
|
||||
static int main_run_allow_root(int argc, char **argv) {
|
||||
const char *path = NULL;
|
||||
bool approve_sensitive = false;
|
||||
bool approve_broad = false;
|
||||
bool list_only = false;
|
||||
bool approve_manifest = false;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--approve-sensitive") == 0) {
|
||||
approve_sensitive = true;
|
||||
} else if (strcmp(argv[i], "--approve-broad") == 0) {
|
||||
approve_broad = true;
|
||||
} else if (strcmp(argv[i], "--list") == 0) {
|
||||
list_only = true;
|
||||
} else if (strcmp(argv[i], "--approve-manifest") == 0) {
|
||||
@@ -1221,7 +1227,8 @@ static int main_run_allow_root(int argc, char **argv) {
|
||||
}
|
||||
if (!path && !list_only) {
|
||||
(void)fprintf(stderr,
|
||||
"usage: codebase-memory-mcp allow-root [--approve-sensitive] <path>\n"
|
||||
"usage: codebase-memory-mcp allow-root [--approve-sensitive | "
|
||||
"--approve-broad] <path>\n"
|
||||
" codebase-memory-mcp allow-root --approve-manifest <project>\n"
|
||||
" codebase-memory-mcp allow-root --list\n");
|
||||
return EXIT_FAILURE;
|
||||
@@ -1264,7 +1271,7 @@ static int main_run_allow_root(int argc, char **argv) {
|
||||
|
||||
char err[CBM_SZ_1K];
|
||||
if (!cbm_workspace_grant_add(cache_dir, cbm_workspace_home_dir(), canonical, approve_sensitive,
|
||||
err, sizeof(err))) {
|
||||
approve_broad, err, sizeof(err))) {
|
||||
(void)fprintf(stderr, "refused: %s\n", err[0] ? err : "not an allowable root");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
+3
-6
@@ -12214,8 +12214,7 @@ static char *index_status_project_key(cbm_mcp_server_t *srv, const char *args, c
|
||||
srv->allowed_root_policy_set ? srv->allowed_root : getenv("CBM_ALLOWED_ROOT");
|
||||
if (repo_path && repo_path[0] &&
|
||||
!cbm_workspace_root_allowed(repo_path, cbm_workspace_home_dir(), cbm_workspace_cache_dir(),
|
||||
allowed_root, getenv("PATH_ALLOW_BROAD"), error_out,
|
||||
error_size)) {
|
||||
allowed_root, error_out, error_size)) {
|
||||
safe_free(repo_path);
|
||||
return NULL;
|
||||
}
|
||||
@@ -12318,8 +12317,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
|
||||
char boundary_err[CBM_SZ_1K];
|
||||
if (repo_path && repo_path[0] &&
|
||||
!cbm_workspace_root_allowed(repo_path, cbm_workspace_home_dir(), cbm_workspace_cache_dir(),
|
||||
allowed_root, getenv("PATH_ALLOW_BROAD"), boundary_err,
|
||||
sizeof(boundary_err))) {
|
||||
allowed_root, boundary_err, sizeof(boundary_err))) {
|
||||
index_args_free(repo_path, mode_str, name_override);
|
||||
return cbm_mcp_text_result(boundary_err, true);
|
||||
}
|
||||
@@ -20828,8 +20826,7 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
|
||||
srv->allowed_root_policy_set ? srv->allowed_root : getenv("CBM_ALLOWED_ROOT");
|
||||
char boundary_err[CBM_SZ_1K];
|
||||
if (!cbm_workspace_root_allowed(srv->session_root, cbm_workspace_home_dir(),
|
||||
cbm_workspace_cache_dir(), allowed_root,
|
||||
getenv("PATH_ALLOW_BROAD"), boundary_err,
|
||||
cbm_workspace_cache_dir(), allowed_root, boundary_err,
|
||||
sizeof(boundary_err))) {
|
||||
cbm_log_warn("autoindex.skip", "reason", "workspace_boundary", "detail", boundary_err);
|
||||
return;
|
||||
|
||||
@@ -1713,8 +1713,7 @@ static void handle_index_start(cbm_http_server_t *server, cbm_http_conn_t *c,
|
||||
}
|
||||
if (!cbm_workspace_root_allowed(canonical_root, cbm_workspace_home_dir(),
|
||||
cbm_workspace_cache_dir(), getenv("CBM_ALLOWED_ROOT"),
|
||||
getenv("PATH_ALLOW_BROAD"), boundary_err,
|
||||
sizeof(boundary_err))) {
|
||||
boundary_err, sizeof(boundary_err))) {
|
||||
yyjson_doc_free(doc);
|
||||
char escaped[1024];
|
||||
cbm_json_escape(escaped, (int)sizeof(escaped), boundary_err);
|
||||
|
||||
@@ -2594,8 +2594,9 @@ TEST(daemon_application_sensitive_root_blocks_auto_index_but_preserves_controls)
|
||||
app_wait_for_active_jobs(application, 0);
|
||||
|
||||
char grant_error[APP_TEST_PATH_CAP];
|
||||
bool approved = ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved =
|
||||
ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved_initialized =
|
||||
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
|
||||
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
|
||||
@@ -2720,8 +2721,9 @@ TEST(daemon_application_sensitive_root_blocks_watch_but_preserves_controls) {
|
||||
bool ordinary_released = ordinary_watched && cbm_watcher_watch_count(watcher) == 0;
|
||||
|
||||
char grant_error[APP_TEST_PATH_CAP];
|
||||
bool approved = ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved =
|
||||
ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
|
||||
grant_error, sizeof(grant_error));
|
||||
bool approved_initialized =
|
||||
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
|
||||
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
|
||||
|
||||
+1
-1
@@ -2045,7 +2045,7 @@ static int issue403_initialize_count_calls(const char *session_root, bool approv
|
||||
char err[1024];
|
||||
bool approved =
|
||||
!approve_sensitive || cbm_workspace_grant_add(cache, cbm_workspace_home_dir(), session_root,
|
||||
true, err, sizeof(err));
|
||||
true, false, err, sizeof(err));
|
||||
cbm_config_t *cfg = approved ? cbm_config_open(cache) : NULL;
|
||||
cbm_mcp_server_t *srv = cfg ? cbm_mcp_server_new(NULL) : NULL;
|
||||
int calls = -2;
|
||||
|
||||
+70
-39
@@ -75,11 +75,11 @@ TEST(ws_posix_top_level_trees_are_too_shallow) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* PATH_ALLOW_BROAD is deliberately not part of the grant store's overridability:
|
||||
* cbm_workspace_classify_root and cbm_workspace_verdict_is_overridable must stay
|
||||
* exactly as strict as the test above already checked. Only the top-level
|
||||
* decision, cbm_workspace_root_allowed, knows about it, and only as an exact
|
||||
* match. */
|
||||
/* A recorded broad approval (allow-root --approve-broad) is the only thing that
|
||||
* lifts CBM_WS_DENY_TOO_SHALLOW, and only for that exact path. Classification
|
||||
* and cbm_workspace_verdict_is_overridable stay exactly as strict as the test
|
||||
* above checks; the approval is a grant, so it is listed like any other human
|
||||
* decision, and it lifts no other verdict. */
|
||||
TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
|
||||
char cache[256];
|
||||
char *created = th_mktempdir("cbm_ws_allow_broad");
|
||||
@@ -87,29 +87,58 @@ TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char err[1024];
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/srv", "/home/dev", cache, NULL, NULL, err,
|
||||
sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "PATH_ALLOW_BROAD"));
|
||||
char listed[4096];
|
||||
/* Refused out of the box, and the refusal names the command a person runs. */
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /usr"));
|
||||
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_root_allowed("/srv", "/home/dev", cache, NULL, "/srv", err, sizeof(err)));
|
||||
|
||||
/* A grant for the root does not extend to a sibling: PATH_ALLOW_BROAD names
|
||||
* one path, not a prefix. */
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/opt", "/home/dev", cache, NULL, "/srv", err,
|
||||
sizeof(err)));
|
||||
|
||||
/* Sensitive and absolute refusals are a different verdict and stay refused
|
||||
* even when PATH_ALLOW_BROAD happens to name that exact path. */
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/home/dev", "/home/dev", cache, NULL, "/home/dev",
|
||||
err, sizeof(err)));
|
||||
ASSERT_FALSE(
|
||||
cbm_workspace_root_allowed("/", "/home/dev", cache, NULL, "/", err, sizeof(err)));
|
||||
|
||||
/* Environment, not a grant: it never shows up in the recorded list. */
|
||||
char listed[64];
|
||||
/* Neither an ordinary grant nor a sensitive approval lifts breadth; a broad
|
||||
* approval lifts neither an absolute nor a sensitive verdict. A refused grant
|
||||
* records nothing. */
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", false, false, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-broad"));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", true, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/", false, true, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, HOME, false, true, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
ASSERT_FALSE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
|
||||
/* The explicit approval is recorded and marked. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
|
||||
|
||||
#ifndef _WIN32
|
||||
/* Containment resolves real paths, so the rest needs a top-level directory
|
||||
* that exists on every POSIX host: "/usr". A repeated approval is recognized
|
||||
* rather than appended again. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
|
||||
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
|
||||
|
||||
/* It lifts exactly that path. A configured root of "/" puts "/dev" inside the
|
||||
* declared boundary too, so the only thing still refusing it is that the
|
||||
* approval names another path. */
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed("/dev", HOME, cache, "/", err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /dev"));
|
||||
#endif
|
||||
|
||||
/* Each marker lifts only its own verdict: a broad mark on a credential
|
||||
* directory, which grant_add would never write, still does not lift it. */
|
||||
char secret[1024];
|
||||
snprintf(secret, sizeof(secret), "%s/.ssh", cache);
|
||||
ASSERT_EQ(cbm_mkdir(secret), 0);
|
||||
char store[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
|
||||
FILE *f = cbm_fopen(store, "a");
|
||||
ASSERT_NOT_NULL(f);
|
||||
(void)fprintf(f, "^%s\n", secret);
|
||||
ASSERT_EQ(fclose(f), 0);
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(secret, HOME, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
@@ -214,10 +243,10 @@ TEST(ws_sensitive_root_explicit_approval_is_preserved) {
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char err[1024];
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
|
||||
th_cleanup(root);
|
||||
th_cleanup(cache);
|
||||
@@ -236,14 +265,14 @@ TEST(ws_sensitive_approval_upgrades_existing_ordinary_exact_grant) {
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char err[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
|
||||
/* A repeated explicit approval must recognize the marked exact grant rather
|
||||
* than append another exception. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
|
||||
|
||||
char listed[4096];
|
||||
char expected[4096];
|
||||
@@ -271,13 +300,15 @@ TEST(ws_sensitive_approval_adds_exact_exception_under_ordinary_ancestor) {
|
||||
snprintf(sensitive, sizeof(sensitive), "%s/private-project", ancestor);
|
||||
ASSERT_EQ(cbm_mkdir(sensitive), 0);
|
||||
char err[1024];
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, false, err, sizeof(err)));
|
||||
ASSERT_FALSE(cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
|
||||
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
|
||||
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
|
||||
ASSERT_TRUE(
|
||||
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
|
||||
|
||||
char listed[4096];
|
||||
char expected[4096];
|
||||
@@ -719,7 +750,7 @@ TEST(ws_manifest_ordinary_entries_unchanged) {
|
||||
|
||||
char err[1024];
|
||||
/* The grant store names a root the sdk is not under. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, false, err, sizeof(err)));
|
||||
|
||||
ws_manifest_write_line(base, sdk);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
|
||||
Reference in new issue
Block a user