diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 3e9956d60..04afa5b3e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 diff --git a/src/foundation/workspace.c b/src/foundation/workspace.c index 952e9aba0..c658d111c 100644 --- a/src/foundation/workspace.c +++ b/src/foundation/workspace.c @@ -392,8 +392,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) { @@ -406,7 +416,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; @@ -425,13 +436,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; } } @@ -443,14 +459,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 */ @@ -462,10 +485,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; } @@ -481,9 +509,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'; } @@ -495,33 +563,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; } @@ -539,7 +596,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); @@ -560,7 +623,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 @@ -605,17 +668,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 and shallow 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; } + 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); + "%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)); } diff --git a/src/foundation/workspace.h b/src/foundation/workspace.h index c4674696d..a956529ab 100644 --- a/src/foundation/workspace.h +++ b/src/foundation/workspace.h @@ -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); diff --git a/src/main.c b/src/main.c index 3e658c801..d3f431d52 100644 --- a/src/main.c +++ b/src/main.c @@ -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] ` — record an indexing root. +/* `allow-root [--approve-sensitive | --approve-broad] ` — 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] \n" + "usage: codebase-memory-mcp allow-root [--approve-sensitive | " + "--approve-broad] \n" " codebase-memory-mcp allow-root --approve-manifest \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; } diff --git a/tests/test_daemon_application.c b/tests/test_daemon_application.c index a923043ca..4faea87a9 100644 --- a/tests/test_daemon_application.c +++ b/tests/test_daemon_application.c @@ -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); diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 80a320ca5..3587554fb 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -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; diff --git a/tests/test_workspace.c b/tests/test_workspace.c index cba81e76a..be93b8e29 100644 --- a/tests/test_workspace.c +++ b/tests/test_workspace.c @@ -75,6 +75,74 @@ TEST(ws_posix_top_level_trees_are_too_shallow) { PASS(); } +/* 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"); + ASSERT_NOT_NULL(created); + snprintf(cache, sizeof(cache), "%s", created); + + char err[1024]; + 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")); + + /* 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(); +} + /* Legitimately shallow project roots must survive: these are the false positives * a blanket depth rule would cause, which is why Windows counts drive-relative. */ TEST(ws_legitimate_shallow_roots_are_allowed) { @@ -177,7 +245,7 @@ TEST(ws_sensitive_root_explicit_approval_is_preserved) { char err[1024]; 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_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); @@ -197,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_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_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]; @@ -232,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_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_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, err, sizeof(err))); + ASSERT_TRUE( + cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err))); char listed[4096]; char expected[4096]; @@ -774,7 +844,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))); @@ -918,6 +988,7 @@ SUITE(workspace) { RUN_TEST(ws_volume_roots_are_absolutely_denied); RUN_TEST(ws_non_absolute_paths_are_denied); RUN_TEST(ws_posix_top_level_trees_are_too_shallow); + RUN_TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only); RUN_TEST(ws_legitimate_shallow_roots_are_allowed); RUN_TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine); RUN_TEST(ws_credential_directories_are_sensitive_at_any_depth);