Skip to content
Merged
9 changes: 9 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,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
Expand Down
149 changes: 109 additions & 40 deletions src/foundation/workspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
}
}
Expand All @@ -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 */
Expand All @@ -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;
}
Expand All @@ -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';
}
Expand All @@ -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;
}

Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down
20 changes: 13 additions & 7 deletions src/foundation/workspace.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 11 additions & 4 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1179,21 +1179,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) {
Expand Down Expand Up @@ -1225,7 +1231,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;
Expand Down Expand Up @@ -1268,7 +1275,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;
}
Expand Down
10 changes: 6 additions & 4 deletions tests/test_daemon_application.c
Original file line number Diff line number Diff line change
Expand Up @@ -2643,8 +2643,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);
Expand Down Expand Up @@ -2769,8 +2770,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);
Expand Down
2 changes: 1 addition & 1 deletion tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading