Skip to content

Commit 887ae48

Browse files
committed
Persist yolo as the user-global skip-permissions default
1 parent 8b70ad0 commit 887ae48

11 files changed

Lines changed: 158 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Corbits Code defaults to **auto mode** (`auto = true`). Workspace file writes/ed
101101

102102
Wrappers such as `bash -c '…'`, `sh`/`zsh -c`, `xargs`, and transparent prefixes (`env`, `nice`, `timeout`) are peeled so the same rules apply to the inner command. Unparseable wrappers fall through to ask rather than auto-allow.
103103

104-
Catastrophic patterns (`rm -rf /`, `sudo`, `curl | bash`, force-push, open-ended `find`/`rg`/`grep -r`, …) are always denied by authorization, independent of auto mode. `--dangerously-skip-permissions` (and mid-session `/yolo` in the TUI) is a separate escape hatch that bypasses the permission gate (not secret-guard path denies or authz hard blocks).
104+
Catastrophic patterns (`rm -rf /`, `sudo`, `curl | bash`, force-push, open-ended `find`/`rg`/`grep -r`, …) are always denied by authorization, independent of auto mode. `--dangerously-skip-permissions` still forces this process; `/yolo` persists as the user-global default. Both bypass the permission gate (not secret-guard path denies or authz hard blocks).
105105

106106
Details live in `docs/PRODUCT.md` (safety model) and `docs/ARCHITECTURE.md` (permission gate and auto-shell policy).
107107

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ In TUI chat mode there is no completion gate — the session stays open across t
7272
- `--config <path>` replaces the global settings file as the provider source (useful for CI per-run injection). A provider must be defined in a settings file; there is no env fallback.
7373
- `settings.ts` owns the schema, validators (the per-repo file rejects credentials), file loaders, and the pure `resolveProvider` precedence function.
7474
- `providers.ts` defines the `ProviderCatalogEntry` type and helpers for building TUI provider lists; `profiles.ts` handles profile-level selection logic.
75-
- `loadConfig` is async (it reads settings files). Parses a leading `exec`/`run` subcommand, flags `--cwd`, `--config`, `--provider`, `--model`, `--force`, `--dangerously-skip-permissions` (TUI mid-session twin: `/yolo`), `--auto` / `--no-auto` (auto mode defaults on); collects positional arguments as the optional initial task for the TUI or the required prompt for exec.
75+
- `loadConfig` is async (it reads settings files). Parses a leading `exec`/`run` subcommand, flags `--cwd`, `--config`, `--provider`, `--model`, `--force`, `--dangerously-skip-permissions` (forces this process; TUI `/yolo` persists as the user-global default), `--auto` / `--no-auto` (auto mode defaults on); collects positional arguments as the optional initial task for the TUI or the required prompt for exec.
7676
- Both settings files are on the secret-guard denylist for path-keyed tools, so the agent cannot `read_file` its own credentials. Shell commands that reference them still require explicit operator approval.
7777

7878
### TUI Runner (`src/tui/runner.ts`)
@@ -368,7 +368,7 @@ tool call
368368
- **classify** — Read-only tools (`read_file`, `search_files`, `grep`, `list_dir`) are tier `allow`; everything else is tier `ask`. Builds approval requests: shell yields one request for the full command the model asked to run (security still splits under the gate); file tools keyed on the target path; other tools keyed on tool name.
369369
- **command** — Splits chained commands for security classification and derives command-shape approval scopes. Multi-segment chains only offer an exact-command persist pattern (a prefix like `npm *` must not cover `npm i && rm -rf /` later).
370370
- **auto-shell-policy** — Constrains `run_shell` even when auto mode would otherwise rubber-stamp it. Before matching, `expandShellSubjects` peels `bash`/`sh`/`zsh -c`, `xargs` utility tails, and transparent prefixes (`env`, `nice`, `timeout`, …) so rules see the real payload; an unparseable wrapper (variable expansion or command substitution) sets an opaque flag that forces `ask`. Effects: `deny` blocks outright (file mutations through ad-hoc tooling — output redirection, `tee`, `sed -i`/`perl -i`, interpreter inline programs or heredocs — which must instead go through `write_file`/`edit_file`); `ask` declines to auto-allow and falls through to the operator prompt (recursive `rm`, dependency installs and remote runners: npm/yarn/pnpm/bun, pip, cargo, go, brew, npx/bunx, …, force or uncontained `git worktree` ops, shell that references a sensitive path such as `.env` or a private key, and opaque wrappers). Contained non-force `git worktree add`/`remove`/`prune` and read-only `list` auto-allow (sibling destinations like `../corbits-dispatch-wts/…` included; absolute outside, `~`, globs, and credential basenames still ask). Deny beats ask when multiple subjects match. Quoted spans are stripped before pattern matching so a quoted `>` or install word in an argument is not flagged, and program names are matched only in command position. Adding a table category is a one-line rule append in `AUTO_SHELL_RULES`.
371-
- **gate** — Evaluates a call: `skipPermissions` allows everything; `allow`-tier passes; for `ask`-tier, checks persisted approvals, otherwise requests operator approval. Shell security classifies each chain segment (`||` / `&&` / `|` / `;` / newlines), but the operator is prompted once for the full command block — any unapproved segment fails the whole block, and execution always runs the unsplit original. Safe pipeline tails and pure shell no-ops (`true` / `false` / `:` and bare control-flow keywords stranded by chain-splitting) skip without a prompt. In a non-interactive run an unresolved `ask` becomes a denial. In auto mode: non-shell built-ins in `AUTO_ALLOWED_TOOLS` (writes/edits/deletes, `manage_tasks`, `task`, …) auto-allow when not path-restricted; for `run_shell` the gate consults the auto-shell policy — a `deny` rule fails the call, an `ask` rule skips the auto-allow shortcut and proceeds to the normal approval flow, and anything unmatched is auto-allowed. Paths outside the workspace and writes under the session state root (`~/.corbits/projects/...` and legacy `.agent-state`) still ask under auto mode. Under `--dangerously-skip-permissions` (or mid-session `/yolo`, which calls `setSkipPermissions`), the gate auto-allows those same cases, and pre-gate sandboxes (path-escape, shell session cwd retention, `list_dir` / `delete_file` workspace bounds) honor `getSkipPermissions()` live so outside-workspace access is not hard-denied after the gate already allowed it — without rebuilding the plugin stack. Secret-guard path denies and authorization hard blocks still apply. Mutating MCP and unknown built-ins are not blanket-allowed outside skip. Newly granted scopes are appended in memory and persisted.
371+
- **gate** — Evaluates a call: `skipPermissions` allows everything; `allow`-tier passes; for `ask`-tier, checks persisted approvals, otherwise requests operator approval. Shell security classifies each chain segment (`||` / `&&` / `|` / `;` / newlines), but the operator is prompted once for the full command block — any unapproved segment fails the whole block, and execution always runs the unsplit original. Safe pipeline tails and pure shell no-ops (`true` / `false` / `:` and bare control-flow keywords stranded by chain-splitting) skip without a prompt. In a non-interactive run an unresolved `ask` becomes a denial. In auto mode: non-shell built-ins in `AUTO_ALLOWED_TOOLS` (writes/edits/deletes, `manage_tasks`, `task`, …) auto-allow when not path-restricted; for `run_shell` the gate consults the auto-shell policy — a `deny` rule fails the call, an `ask` rule skips the auto-allow shortcut and proceeds to the normal approval flow, and anything unmatched is auto-allowed. Paths outside the workspace and writes under the session state root (`~/.corbits/projects/...` and legacy `.agent-state`) still ask under auto mode. Under `--dangerously-skip-permissions` (forces this process) or `/yolo` (persists as the user-global default via `setSkipPermissions`), the gate auto-allows those same cases, and pre-gate sandboxes (path-escape, shell session cwd retention, `list_dir` / `delete_file` workspace bounds) honor `getSkipPermissions()` live so outside-workspace access is not hard-denied after the gate already allowed it — without rebuilding the plugin stack. Secret-guard path denies and authorization hard blocks still apply. Mutating MCP and unknown built-ins are not blanket-allowed outside skip. Newly granted scopes are appended in memory and persisted.
372372

373373
- **matcher** — Approval pattern matching via `@intx/authz` `matchPattern` (`*` wildcards). Exact-command grants store a backslash before each metacharacter; those patterns match by equality after unescape (the package has no escape syntax).
374374
- **authz-grants** — Maps stored approvals into `@intx/authz` `GrantRule`s and evaluates them with `evaluateGrants` (allow-only; Corbits cwd/provider-model filters applied first). Exact-escaped grants bypass the package path and use equality.

‎docs/IMPLEMENTATION.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Intent defaults: `intent=implement` → director `build`; explore/plan → same-
166166

167167
### Auto Mode
168168

169-
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. Skip-permissions (`--dangerously-skip-permissions`) has a mid-session TUI toggle: `/yolo [on|off|toggle]` (bare `/yolo` also toggles) wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins.
169+
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` still forces this process. `/yolo [on|off|toggle]` (bare `/yolo` also toggles) persists as the user-global default and wires `getSkipPermissions`/`setSkipPermissions` so the gate and pre-gate sandboxes honor the change on the next tool call without rebuilding plugins. Secret-guard and authz still apply.
170170

171171
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:
172172

@@ -315,7 +315,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts`
315315
| `--model <id>` | provider default | Select a model for the active provider |
316316
| `--profile <name>` | — | Settings profile |
317317
| `--force` | false | Override an existing run state |
318-
| `--dangerously-skip-permissions` | false | Auto-allow anything not denied by the authorization layer (gate + pre-gate workspace sandboxes; secret-guard / authz hard denies remain). Mid-session TUI twin: `/yolo [on\|off\|toggle]` via `setSkipPermissions` |
318+
| `--dangerously-skip-permissions` | false | Auto-allow anything not denied by the authorization layer (gate + pre-gate workspace sandboxes; secret-guard / authz hard denies remain). This launch flag still forces this process; `/yolo [on\|off\|toggle]` persists as the user-global default via `setSkipPermissions` |
319319
| `--auto` | true (default) | Force auto mode on (workspace writes + unconstrained shell without prompts) |
320320
| `--no-auto` | false | Start with auto mode off (ask on every consequential action); no in-session key toggles it |
321321
| `--help`, `-h` | — | Show help (exit 0 via `CliHelpError`) |

‎docs/PRODUCT.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ $ corbits exec "Add JWT auth to the API"
6767
$ corbits run "Add JWT auth to the API"
6868
```
6969

70-
Same directors, tools, permissions, MCP, plugins, and hooks as the TUI — without the OpenTUI shell. The exec bootstrap is a deliberate fork of the TUI path (not a shared factory yet); see `docs/ARCHITECTURE.md` “Exec Runner” for intentional deltas (no workflow controller; single primary send; non-interactive permission gate). Compaction continuation matches TUI so long runs do not stall after compact. Streams assistant text to stdout for scripts and CI. Non-interactive by default: actions that need operator approval are denied unless `--dangerously-skip-permissions` is set (or auto mode covers them). In the TUI, `/yolo` is the mid-session twin of that flag. `ask_operator` reads a single line from stdin when available.
70+
Same directors, tools, permissions, MCP, plugins, and hooks as the TUI — without the OpenTUI shell. The exec bootstrap is a deliberate fork of the TUI path (not a shared factory yet); see `docs/ARCHITECTURE.md` “Exec Runner” for intentional deltas (no workflow controller; single primary send; non-interactive permission gate). Compaction continuation matches TUI so long runs do not stall after compact. Streams assistant text to stdout for scripts and CI. Non-interactive by default: actions that need operator approval are denied unless `--dangerously-skip-permissions` is set (or auto mode covers them). In the TUI, `/yolo` persists as the user-global default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply. `ask_operator` reads a single line from stdin when available.
7171

7272
Local multi-model capability checks use this path (`bun run eval:capability`); see `evals/capability/README.md`.
7373

@@ -97,7 +97,7 @@ is the direct, explicit resume path.
9797

9898
## Slash Commands (TUI)
9999

100-
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (mid-session twin of `--dangerously-skip-permissions`; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. Plugins can register additional commands.
100+
The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/mcp`, and `/yolo` (persists as the user-global skip-permissions default; `--dangerously-skip-permissions` still forces this process; secret-guard and authz still apply; `/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/<name>` command per available workflow. Plugins can register additional commands.
101101

102102
**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request-review`, `/create-issue`, `/scribe`, `/interview`, `/ast-grep`. Each one is a Skywalker recipe — the slash sends the skill body to the primary, which then `task(agent="<director>")`. `/scribe` → shakespeare; `/implement` spawns build / greybeard / critique as the recipe specifies; `/plan` → plan director (eng change plan: files, AC, non-goals, risks, ordered steps; does not implement); `/review` is a code-review action. `/create-issue` remains the tracker command: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). Dispatch is not a default slash — it stays `use_skill` only, along with git-rebase, linear-issue-workflow, style, philosophy, typescript, and opsh (`user-invocable: false`). Draper and emil are not slashes; they remain closed directors via `task(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `use_skill`. Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone.
103103

@@ -124,7 +124,7 @@ The exact turn thresholds are model-family-dependent (tighter for models with ob
124124

125125
**What the user sees:** In a non-interactive `corbits exec` run, a consequential action that needs approval returns a tool error explaining that approval is unavailable.
126126

127-
**Recovery:** Re-run interactively (TUI), pre-approve via persisted approvals, narrow the action, re-run with `--dangerously-skip-permissions`, or use `/yolo` mid-session in the TUI.
127+
**Recovery:** Re-run interactively (TUI), pre-approve via persisted approvals, narrow the action, re-run with `--dangerously-skip-permissions`, or use `/yolo` in the TUI (persists as the user-global default).
128128

129129
### Resume after interruption
130130

‎src/config.test.ts‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,90 @@ describe("loadConfig", () => {
510510
}
511511
});
512512

513+
test("seeds dangerouslySkipPermissions from global settings without the CLI flag", async () => {
514+
const cwd = await emptyCwd();
515+
try {
516+
const globalPath = join(cwd, "global.json");
517+
await writeFile(
518+
globalPath,
519+
JSON.stringify({
520+
defaultProvider: "fireworks",
521+
providers: {
522+
fireworks: {
523+
baseURL: "https://api.fireworks.ai/inference",
524+
apiKey: "test-key",
525+
models: ["accounts/fireworks/routers/kimi-k2p6-turbo"],
526+
},
527+
},
528+
dangerouslySkipPermissions: true,
529+
}),
530+
);
531+
const config = await loadConfig(["--cwd", cwd, "do something"], {
532+
globalSettingsPath: globalPath,
533+
});
534+
expect(config.dangerouslySkipPermissions).toBe(true);
535+
} finally {
536+
await rm(cwd, { recursive: true, force: true });
537+
}
538+
});
539+
540+
test("CLI --dangerously-skip-permissions still wins over settings false", async () => {
541+
const cwd = await emptyCwd();
542+
try {
543+
const globalPath = join(cwd, "global.json");
544+
await writeFile(
545+
globalPath,
546+
JSON.stringify({
547+
defaultProvider: "fireworks",
548+
providers: {
549+
fireworks: {
550+
baseURL: "https://api.fireworks.ai/inference",
551+
apiKey: "test-key",
552+
models: ["accounts/fireworks/routers/kimi-k2p6-turbo"],
553+
},
554+
},
555+
dangerouslySkipPermissions: false,
556+
}),
557+
);
558+
const config = await loadConfig(
559+
["--cwd", cwd, "--dangerously-skip-permissions", "do something"],
560+
{ globalSettingsPath: globalPath },
561+
);
562+
expect(config.dangerouslySkipPermissions).toBe(true);
563+
} finally {
564+
await rm(cwd, { recursive: true, force: true });
565+
}
566+
});
567+
568+
test("exec inherits persisted skip-permissions without the CLI flag", async () => {
569+
const cwd = await emptyCwd();
570+
try {
571+
const globalPath = join(cwd, "global.json");
572+
await writeFile(
573+
globalPath,
574+
JSON.stringify({
575+
defaultProvider: "fireworks",
576+
providers: {
577+
fireworks: {
578+
baseURL: "https://api.fireworks.ai/inference",
579+
apiKey: "test-key",
580+
models: ["accounts/fireworks/routers/kimi-k2p6-turbo"],
581+
},
582+
},
583+
dangerouslySkipPermissions: true,
584+
}),
585+
);
586+
const config = await loadConfig(["exec", "--cwd", cwd, "ship it"], {
587+
globalSettingsPath: globalPath,
588+
});
589+
assertConfigured(config);
590+
expect(config.command).toBe("exec");
591+
expect(config.dangerouslySkipPermissions).toBe(true);
592+
} finally {
593+
await rm(cwd, { recursive: true, force: true });
594+
}
595+
});
596+
513597
test("reads provider and model from a --config settings file", async () => {
514598
const cwd = await emptyCwd();
515599
try {

‎src/config/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,9 @@ export async function loadConfig(
589589
})
590590
: await loadSettings(options.globalSettingsPath ?? globalSettingsPath());
591591

592+
dangerouslySkipPermissions =
593+
dangerouslySkipPermissions || settings?.dangerouslySkipPermissions === true;
594+
592595
// OAuth profiles live in home-level auth stores, not in settings files. They
593596
// are merged in only for the real default settings path: an explicit --config
594597
// or test override selects a controlled provider set that should not pull in

0 commit comments

Comments
 (0)