Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ In TUI chat mode there is no completion gate — the session stays open across t
- `settings.ts` owns the schema, validators (the per-repo file rejects credentials), file loaders, and the pure `resolveProvider` precedence function.
- `providers.ts` defines the `ProviderCatalogEntry` type and helpers for building TUI provider lists; `profiles.ts` handles profile-level selection logic.
- `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.
- 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.
- 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.
- 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.

### Inference credential recovery
Expand Down Expand Up @@ -407,7 +407,7 @@ tool call
- **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.
- **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`.
- **Tool-output URI** (`tool-output-uri-plugin.ts`) — Normalizes mistaken `read_file` blob URIs to `tool-output:///id` (corbits-only; interchange stays unpatched).
- **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.
- **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.
- **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.
- **Permission** (`permission-plugin.ts`) — Delegates consequential calls to the permission gate.
- **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.
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Intent defaults: `intent=implement` → director `builder`; `explore` → `explo

### Auto Mode

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.
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.

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:

Expand Down
2 changes: 1 addition & 1 deletion docs/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ line instead of a path dump.

## Configuration

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.
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.

## Optional Capabilities (plugins)

Expand Down
25 changes: 23 additions & 2 deletions src/agent/posix-tool-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export interface CorePosixToolPluginsArgs {
getShellOutputFeeds?: () => ShellOutputFeedMap | undefined;
/** Primary-only evidence archive; workers omit this getter. */
getEvidenceArchive?: () => CompactionArchive | undefined;
/**
* CL-9386: runtime secret-guard denylist for the active --config path.
* Entry points pass [config.globalSettingsPath]; workers inherit their
* parent's list. Omitted (tests, ad-hoc stacks) keeps the static denylist
* only — the default settings file stays covered either way.
*/
secretGuardExtraDeniedPaths?: readonly string[];
}

// Middleware order matches docs/ARCHITECTURE.md: path escape through truncation,
Expand Down Expand Up @@ -91,6 +98,7 @@ export function buildCorePosixToolPlugins(
getBackgroundShellRegistry,
getShellOutputFeeds,
getEvidenceArchive,
secretGuardExtraDeniedPaths,
} = args;
// Pre-gate sandboxes honor yolo mode so outside-workspace path tools and shell
// cwd are not hard-denied after the gate already auto-allows. Pass a live
Expand All @@ -101,6 +109,15 @@ export function buildCorePosixToolPlugins(
// One shared workspace-roots provider for every bound in this stack, so
// pathEscape and delete_file admit the same registered sibling worktrees.
const rootsProvider = createWorktreeRootsProvider(cwd);
// CL-1187 finding 2: the gate's shell legs (segmentGuard, auto-allow, auto
// policy) must treat the extras-denied paths as sensitive exactly like the
// secret-guard plugin below does. The gate is built before this stack and
// shared across stacks, so forward the list here — the single funnel every
// entry point (exec, TUI) and worker flows through — rather than wiring
// each runner's gate construction separately.
if (secretGuardExtraDeniedPaths !== undefined) {
permissionGate.setSensitiveExtraDeniedPaths?.(secretGuardExtraDeniedPaths);
}
const truncationOptions =
getBlobWriter !== undefined ||
getContextDir !== undefined ||
Expand All @@ -121,7 +138,11 @@ export function buildCorePosixToolPlugins(
evidenceArchivePathGuardPlugin(),
deleteFilePlugin(cwd, { allowOutside, rootsProvider }),
toolOutputUriPlugin(),
secretGuardPlugin(),
secretGuardPlugin(
secretGuardExtraDeniedPaths !== undefined
? { extraDeniedPaths: secretGuardExtraDeniedPaths }
: undefined,
),
permissionPlugin(permissionGate),
shellGuardPlugin(cwd, shellTimeout, shellEnv, {
allowOutsideCwd: allowOutside,
Expand All @@ -134,7 +155,7 @@ export function buildCorePosixToolPlugins(
? [evidenceArchiveSearchPlugin(getEvidenceArchive)]
: []),
readFileGuardPlugin(cwd, readFileGuard),
ripgrepPlugin(cwd),
ripgrepPlugin(cwd, {}, undefined, secretGuardExtraDeniedPaths ?? []),
// Verify wraps the line-range short-circuit (composeMiddleware runs plugins
// outer-to-inner in array order) so its before/after check still covers
// start_line/end_line edits instead of only substring-mode edit_file calls.
Expand Down
14 changes: 14 additions & 0 deletions src/agent/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ export interface AgentToolsetArgs {
getContextDir?: () => string | undefined;
// Per-project settings.env, merged into the run_shell tool's spawn environment.
shellEnv?: Record<string, string>;
/**
* CL-9386: runtime secret-guard denylist for the active --config path.
* Entry points pass [config.globalSettingsPath]; forwarded to the posix
* plugin stack and inherited by workers via the fleet deps below. Omitted
* keeps the static denylist only.
*/
secretGuardExtraDeniedPaths?: readonly string[];
// Called when a background run_shell (background: true) process exits. Hosts
// deliver the exit as a system message so the reactor re-enters on a later
// turn; omit it and background runs still start/collect but never notify.
Expand Down Expand Up @@ -424,6 +431,7 @@ export async function createAgentToolset(
getEvidenceArchive,
sessionMode = "orchestrator",
shellEnv,
secretGuardExtraDeniedPaths,
toolAvailability = { languageServerAvailable: true },
} = args;
let mcpServersSource = args.mcpServersSource ?? "none";
Expand Down Expand Up @@ -524,6 +532,9 @@ export async function createAgentToolset(
permissionGate,
...(shellTimeout !== undefined ? { shellTimeout } : {}),
extraToolPlugins,
...(secretGuardExtraDeniedPaths !== undefined
? { secretGuardExtraDeniedPaths }
: {}),
...(sessionBlobReader !== undefined
? { readFileGuard: { blobReader: sessionBlobReader } }
: {}),
Expand Down Expand Up @@ -573,6 +584,9 @@ export async function createAgentToolset(
gateAgentTools(inheritedMcpTools, gate),
...(shellTimeout !== undefined ? { shellTimeout } : {}),
...(shellEnv !== undefined ? { shellEnv } : {}),
...(secretGuardExtraDeniedPaths !== undefined
? { secretGuardExtraDeniedPaths }
: {}),
...(skillDirs.length > 0 ? { skillDirs } : {}),
...(extraToolPlugins.length > 0 ? { extraToolPlugins } : {}),
cwd,
Expand Down
Loading
Loading