Skip to content

Commit cfd9d2e

Browse files
corbits-builderTheGreatAxios
authored andcommitted
fix(secret-guard): runtime-denylist the active --config path holding skip
1 parent 9dc2f49 commit cfd9d2e

12 files changed

Lines changed: 152 additions & 17 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ In TUI chat mode there is no completion gate — the session stays open across t
8888
- `settings.ts` owns the schema, validators (the per-repo file rejects credentials), file loaders, and the pure `resolveProvider` precedence function.
8989
- `providers.ts` defines the `ProviderCatalogEntry` type and helpers for building TUI provider lists; `profiles.ts` handles profile-level selection logic.
9090
- `loadConfig` is async (it reads settings files). Parses a leading `exec`/`run` subcommand, flags `--cwd`, `--config`, `--provider`, `--model`, `--dangerously-skip-permissions` and its `--yolo` alias (both force only this process and never persist), `--auto` / `--no-auto` (auto mode defaults on); collects positional arguments as the optional initial task for the TUI or the required prompt for exec. Authorization precedence remains catastrophic authorization denial → skip → normal tier/grant/auto, so `--auto --yolo` behaves as yolo. TUI `/yolo` persists to the active settings file: with the default settings source this is the machine-wide default file, while explicit `--config <path>` selects that path as the active source. An ordinary TUI started without the same `--config` does not modify a custom settings file.
91-
- The default user-global settings path, the per-repo `.corbits/settings.json`, and the project/global grant store (`.corbits/permissions.json`) are on the static secret-guard denylist for path-keyed tools, so the agent cannot `read_file` credentials there or persist standing auto-approvals. An arbitrary path selected with `--config <path>` is not added to that denylist at runtime. In normal and auto modes, shell commands that reference a statically protected path require explicit operator approval; yolo/skip-permissions allows those shell references after catastrophic authorization checks, while path-keyed access to statically protected paths remains hard-denied.
91+
- The default user-global settings path, the per-repo `.corbits/settings.json`, and the project/global grant store (`.corbits/permissions.json`) are on the static secret-guard denylist for path-keyed tools, so the agent cannot `read_file` credentials there or persist standing auto-approvals. The active settings source (`config.globalSettingsPath`, including a `--config` override pointing inside the workspace) is runtime-denylisted with the same force: path-keyed reads and writes are hard-denied even under skip-permissions, so a `/yolo`-persisted skip there cannot be silently leveraged, and workers inherit the denylist. In normal and auto modes, shell commands that reference a statically protected path require explicit operator approval; yolo/skip-permissions allows those shell references after catastrophic authorization checks, while path-keyed access to statically protected paths remains hard-denied.
9292
- Credential-surface ownership: each auth store module enumerates its own files (`*_AUTH_FILENAME` / `MCP_AUTH_DIRNAME`), the data-only registry in `src/auth/credential-surface.ts` turns them into denylist patterns, `secret-guard-plugin.ts` owns matching (lexical plus realpath), and `@mention` resolution consumes the resolved check — never the registry directly. A new `*-auth.json` token store is denied only once its store module exports its filename and the registry lists it; the coverage test scans store sources for `*-auth.json` literals (registered dirnames get an includes-check instead) and fails the build until both exist.
9393

9494
### Inference credential recovery
@@ -407,7 +407,7 @@ tool call
407407
- **Path Escape** (`path-escape-plugin.ts`) — Canonicalizes path-like arguments against `cwd` and blocks `..` escapes, except into a root the permission layer's worktree-roots provider allowlists (e.g. a sibling git worktree of the same repo). `tool-output://` and `archive:///` refs pass through unresolved. Runs first so later plugins see resolved paths.
408408
- **Evidence archive** (`evidence-archive-search-plugin.ts`, `evidence-archive-path-guard.ts`) — Primary-session compaction evidence is a first-class search/read surface on `search_files` / `read_file` / `grep` via `archive:///` refs. Dump paths (`evidence-archive/`, `tool-output/archive-*`) stay blocked so the on-disk sidecar is not the retrieval API. Blob keys reject `/` so they cannot nest under `tool-output`.
409409
- **Tool-output URI** (`tool-output-uri-plugin.ts`) — Normalizes mistaken `read_file` blob URIs to `tool-output:///id` (corbits-only; interchange stays unpatched).
410-
- **Secret Guard** (`secret-guard-plugin.ts`) — Hard-denies path-keyed tool calls (`read_file`, `write_file`, …) that would put a sensitive file into (or write it from) the model context. Runs before the permission plugin, so the path-arg deny holds even under `--dangerously-skip-permissions`. Shell commands that _reference_ a sensitive path (tokenized so `cat .env`, `bun --env-file=.env run …`, and quote/env-assignment forms are detected) are not hard-denied here. Normal mode requires operator approval via the permission gate, and auto mode forces the same ask through the auto-shell policy (`sensitive-path` rule). Yolo/skip-permissions bypasses that prompt after catastrophic authorization checks, but path-keyed access to statically protected secret paths remains hard-denied. Shell detection is best-effort: token matching defeats quoting and env-assignment/redirection forms but not dynamic path construction (variable indirection, `printf` assembly). Tool-result secret scrub still redacts credential-shaped output.
410+
- **Secret Guard** (`secret-guard-plugin.ts`) — Hard-denies path-keyed tool calls (`read_file`, `write_file`, …) that would put a sensitive file into (or write it from) the model context. Runs before the permission plugin, so the path-arg deny holds even under `--dangerously-skip-permissions`. An operator-chosen `--config` path cannot be covered by static patterns, so entry points pass the resolved active settings path as `extraDeniedPaths` (exact match, lexical plus realpath). Shell commands that _reference_ a sensitive path (tokenized so `cat .env`, `bun --env-file=.env run …`, and quote/env-assignment forms are detected) are not hard-denied here. Normal mode requires operator approval via the permission gate, and auto mode forces the same ask through the auto-shell policy (`sensitive-path` rule). Yolo/skip-permissions bypasses that prompt after catastrophic authorization checks, but path-keyed access to statically protected secret paths remains hard-denied. Shell detection is best-effort: token matching defeats quoting and env-assignment/redirection forms but not dynamic path construction (variable indirection, `printf` assembly). Tool-result secret scrub still redacts credential-shaped output.
411411
- **Authorization** (`run-shell-authz.ts`, enforced by the permission gate) — Denies catastrophic shell command patterns by regex, and hard-blocks shell `find`, head-position `rg`, and recursive `grep -r` (they can walk huge trees and OOM the host). Bounded `grep`/`search_files` tools remain practical alternatives (timeout + output caps); the patterns match those three command shapes only — an `ls -R`, `fd`, or scripted `os.walk` is just as unbounded and is not caught, so the block message tells the model not to substitute one. The gate hard-denies these at the top of its verdict path — before auto-allow, prompting, grants, and skipPermissions — so no mode or stored grant can admit them.
412412
- **Permission** (`permission-plugin.ts`) — Delegates consequential calls to the permission gate.
413413
- **Shell Guard** (`shell-guard-plugin.ts`) — Corbits Code-only replacement for stock `run_shell` (interchange stays unpatched): 120s foreground default (`settings.shell.timeoutMs` overrides that default only; a positive per-call `timeout` is the bound with no ceiling — `maxTimeoutMs` does not clamp it), background `run_shell` has no default (only a per-call timeout arms a timer), 512KB display cap with head+tail retention (the process keeps running when the cap is hit), process-group kill on timeout, abort, and plugin dispose (live children tracked in the plugin and reaped by `posixTools.dispose`), and `background: true` — the call returns a `shell_id` at once (registry in `src/shell/background-shell.ts`), the process group keeps running past the turn, completion is process-exit (stdio-close is not required), delivered on a later turn via `buildShellBackgroundMessage`, and `shell_collect` retrieves or cancels with a 300s wait cap (schema advertised by `advertiseShellGuardTimeout` only when `shell_collect` is mounted; evaluated by the permission chain at start time like any shell call). Foreground expiry is exit 124 + `timedOut:true` plus a nudge to retry with `background:true`. Also applies a 10s wall-clock budget to `grep`/`search_files`. Ripgrep detached spawns are not tracked.

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ Intent defaults: `intent=implement` → director `builder`; `explore` → `explo
174174

175175
### Auto Mode
176176

177-
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). It is toggled only via those CLI flags — there is currently no in-session key bound to it. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call. `--dangerously-skip-permissions` and its `--yolo` alias force skip-permissions for this process only; skip-permissions wins when combined with auto, so `--auto --yolo` runs in yolo mode. `/yolo [on|off|toggle]` (bare `/yolo` also toggles) persists the skip-permissions setting to the active settings file and wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins. The active source defaults to the user-global `~/.corbits/settings.json`, making that default machine-wide. Explicit `--config <path>` selects that file instead, so `/yolo` writes the custom file; a later ordinary TUI launch without the same `--config` uses the user-global source and does not modify the custom file. Secret-guard and authz still apply. `loadConfig` tracks `skipPermissionsFromSettings` (true only when the effective value came from persisted settings, not either CLI flag) so `runTUI` can show a startup notice and `exec` can print an equivalent stderr warning for the otherwise-silent persisted setting.
177+
Auto mode defaults **on** (`config.auto = true` from `loadConfig`; pass `--no-auto` to start off, or `--auto` to force on). It is toggled only via those CLI flags — there is currently no in-session key bound to it. The permission gate reads the flag (`getAuto`/`setAuto` in `src/permission/gate.ts`) on the next tool call. `--dangerously-skip-permissions` and its `--yolo` alias force skip-permissions for this process only; skip-permissions wins when combined with auto, so `--auto --yolo` runs in yolo mode. `/yolo [on|off|toggle]` (bare `/yolo` also toggles) persists the skip-permissions setting to the active settings file and wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins. The active source defaults to the user-global `~/.corbits/settings.json`, making that default machine-wide. Explicit `--config <path>` selects that file instead, so `/yolo` writes the custom file; a later ordinary TUI launch without the same `--config` uses the user-global source and does not modify the custom file. Secret-guard and authz still apply — and the active file itself is runtime-denylisted for path-keyed tools, reads and writes, even under skip-permissions, so the persisted skip cannot be silently leveraged; the startup notice remains as disclosure, not the enforcement. `loadConfig` tracks `skipPermissionsFromSettings` (true only when the effective value came from persisted settings, not either CLI flag) so `runTUI` can show a startup notice and `exec` can print an equivalent stderr warning for the otherwise-silent persisted setting.
178178

179179
When auto is on, the gate auto-allows workspace file tools in `AUTO_ALLOWED_TOOLS` and any `run_shell` that does not match the auto-shell policy. The policy (`autoShellRuleForCall` / `AUTO_SHELL_RULES` in `src/permission/auto-shell-policy.ts`) peels wrappers via `expandShellSubjects` (`bash`/`sh`/`zsh -c`, `xargs`, transparent prefixes), then applies:
180180

‎docs/PRODUCT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ line instead of a path dump.
145145

146146
## Configuration
147147

148-
Providers and models are configured in the active settings file, which defaults to `~/.corbits/settings.json` (providers + credentials), with a selection-only per-repo `.corbits/settings.json` override. Select at launch with `--provider` / `--model`, or use `--config <path>` to select an alternate active file for provider definitions and TUI settings persistence such as `/yolo`. `--config` composes with, rather than replaces, credentials for codex/xai OAuth-profile providers, which live in separate home-level auth stores (`~/.corbits/codex-auth.json`, `xai-auth.json`) and are merged into the catalog regardless of `--config`. Credentials are read only from these settings files and the OAuth auth stores — there is no environment-variable override and `.env` files are not loaded, so a stale or exported key can't shadow the configured provider. Static secret-guard protection denies path-keyed read access to the default user-global `~/.corbits/settings.json` and per-repo `.corbits/settings.json`. An arbitrary active path selected with `--config <path>` is not added to that denylist at runtime.
148+
Providers and models are configured in the active settings file, which defaults to `~/.corbits/settings.json` (providers + credentials), with a selection-only per-repo `.corbits/settings.json` override. Select at launch with `--provider` / `--model`, or use `--config <path>` to select an alternate active file for provider definitions and TUI settings persistence such as `/yolo`. `--config` composes with, rather than replaces, credentials for codex/xai OAuth-profile providers, which live in separate home-level auth stores (`~/.corbits/codex-auth.json`, `xai-auth.json`) and are merged into the catalog regardless of `--config`. Credentials are read only from these settings files and the OAuth auth stores — there is no environment-variable override and `.env` files are not loaded, so a stale or exported key can't shadow the configured provider. Static secret-guard protection denies path-keyed read access to the default user-global `~/.corbits/settings.json` and per-repo `.corbits/settings.json`. A `--config` alternate file gets the same denial at runtime: the active settings source is denylisted for the agent's file tools even when it lives inside the workspace, so `/yolo`-persisted skip-permissions there cannot be silently leveraged.
149149

150150
## Optional Capabilities (plugins)
151151

‎src/agent/posix-tool-plugins.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export interface CorePosixToolPluginsArgs {
5252
getShellOutputFeeds?: () => ShellOutputFeedMap | undefined;
5353
/** Primary-only evidence archive; workers omit this getter. */
5454
getEvidenceArchive?: () => CompactionArchive | undefined;
55+
/**
56+
* CL-9386: runtime secret-guard denylist for the active --config path.
57+
* Entry points pass [config.globalSettingsPath]; workers inherit their
58+
* parent's list. Omitted (tests, ad-hoc stacks) keeps the static denylist
59+
* only — the default settings file stays covered either way.
60+
*/
61+
secretGuardExtraDeniedPaths?: readonly string[];
5562
}
5663

5764
// Middleware order matches docs/ARCHITECTURE.md: path escape through truncation,
@@ -91,6 +98,7 @@ export function buildCorePosixToolPlugins(
9198
getBackgroundShellRegistry,
9299
getShellOutputFeeds,
93100
getEvidenceArchive,
101+
secretGuardExtraDeniedPaths,
94102
} = args;
95103
// Pre-gate sandboxes honor yolo mode so outside-workspace path tools and shell
96104
// cwd are not hard-denied after the gate already auto-allows. Pass a live
@@ -121,7 +129,11 @@ export function buildCorePosixToolPlugins(
121129
evidenceArchivePathGuardPlugin(),
122130
deleteFilePlugin(cwd, { allowOutside, rootsProvider }),
123131
toolOutputUriPlugin(),
124-
secretGuardPlugin(),
132+
secretGuardPlugin(
133+
secretGuardExtraDeniedPaths !== undefined
134+
? { extraDeniedPaths: secretGuardExtraDeniedPaths }
135+
: undefined,
136+
),
125137
permissionPlugin(permissionGate),
126138
shellGuardPlugin(cwd, shellTimeout, shellEnv, {
127139
allowOutsideCwd: allowOutside,

‎src/agent/tools.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,13 @@ export interface AgentToolsetArgs {
213213
getContextDir?: () => string | undefined;
214214
// Per-project settings.env, merged into the run_shell tool's spawn environment.
215215
shellEnv?: Record<string, string>;
216+
/**
217+
* CL-9386: runtime secret-guard denylist for the active --config path.
218+
* Entry points pass [config.globalSettingsPath]; forwarded to the posix
219+
* plugin stack and inherited by workers via the fleet deps below. Omitted
220+
* keeps the static denylist only.
221+
*/
222+
secretGuardExtraDeniedPaths?: readonly string[];
216223
// Called when a background run_shell (background: true) process exits. Hosts
217224
// deliver the exit as a system message so the reactor re-enters on a later
218225
// turn; omit it and background runs still start/collect but never notify.
@@ -424,6 +431,7 @@ export async function createAgentToolset(
424431
getEvidenceArchive,
425432
sessionMode = "orchestrator",
426433
shellEnv,
434+
secretGuardExtraDeniedPaths,
427435
toolAvailability = { languageServerAvailable: true },
428436
} = args;
429437
let mcpServersSource = args.mcpServersSource ?? "none";
@@ -524,6 +532,9 @@ export async function createAgentToolset(
524532
permissionGate,
525533
...(shellTimeout !== undefined ? { shellTimeout } : {}),
526534
extraToolPlugins,
535+
...(secretGuardExtraDeniedPaths !== undefined
536+
? { secretGuardExtraDeniedPaths }
537+
: {}),
527538
...(sessionBlobReader !== undefined
528539
? { readFileGuard: { blobReader: sessionBlobReader } }
529540
: {}),
@@ -573,6 +584,9 @@ export async function createAgentToolset(
573584
gateAgentTools(inheritedMcpTools, gate),
574585
...(shellTimeout !== undefined ? { shellTimeout } : {}),
575586
...(shellEnv !== undefined ? { shellEnv } : {}),
587+
...(secretGuardExtraDeniedPaths !== undefined
588+
? { secretGuardExtraDeniedPaths }
589+
: {}),
576590
...(skillDirs.length > 0 ? { skillDirs } : {}),
577591
...(extraToolPlugins.length > 0 ? { extraToolPlugins } : {}),
578592
cwd,

‎src/exec/runner.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,9 @@ export async function runExec(config: Config): Promise<ExecResult> {
660660
skillDirs,
661661
telemetry: liveTelemetry,
662662
isCodex: isCodexProviderName(config.providerName),
663+
// CL-9386: the active settings source (including a --config override)
664+
// is model-unreadable/unwritable, like the default settings file.
665+
secretGuardExtraDeniedPaths: [config.globalSettingsPath],
663666
...(shellTimeout !== undefined ? { shellTimeout } : {}),
664667
...(toolWatchdog !== undefined ? { toolWatchdog } : {}),
665668
...(localSettingsForMode?.env !== undefined

0 commit comments

Comments
 (0)