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:
Martin VogelandMark Murawski committed 2026-10-20 19:26:31 +02:00
1 parent b9df26094f
commit 3e2de74e98
10 files changed
+227 -133

No files matched your search

+9
View File
@@ -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
+1 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1 -2
View File
@@ -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);
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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)));