Skip to content

feat: add model-gated auto-approve mode - #39015

Open
mayanksingh09 wants to merge 2 commits into
anomalyco:devfrom
advicehub:auto-approve-pr
Open

feat: add model-gated auto-approve mode#39015
mayanksingh09 wants to merge 2 commits into
anomalyco:devfrom
advicehub:auto-approve-pr

Conversation

@mayanksingh09

@mayanksingh09 mayanksingh09 commented Jul 26, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #37564

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Adds an opt-in TUI mode where a small model reviews each consequential action before it runs. Safe, clearly authorized actions go through; anything unsafe or ambiguous falls back to the normal prompt. It's the inverse of --auto, which approves everything without looking.

It's behind a beta flag and does nothing until you set it:

{ "experimental": { "auto_approve": true } }

Then <tab> to Auto-approve. Because the shipped ruleset is *: allow, the mode installs a temporary in-memory overlay that turns allow into ask for bash/edit/task/webfetch/websearch/external_directory/skill while it's active. Reads are never escalated. deny is never softened, "always" grants still apply, and subagent sessions are never auto-approved — a subagent's prompt is written by a model, so it can't count as your authorization. Anything that errors or times out falls back to asking you.

How did you verify your code works?

Typecheck across all 30 packages; 263 tests in packages/opencode and 84 in packages/tui, including terminal frame-capture tests for the classifier row; all three test:httpapi modes at 210/210. Overlay tests pin the important one directly: with bash: {"rm -rf *": "deny"} configured, rm -rf / still denies while the overlay is on.

Checked live against openai/gpt-5.6-luna: "run ls -1d */ | wc -l in /" auto-approves, rm -rf /var/log asks, and a blanket "you can run any command" also asks — a standing grant isn't treated as authorization, since that's what an injection would plant.

Screenshots / recordings

auto-approve

Reviewer setup

gh pr checkout 39015
bun install --frozen-lockfile
OPENCODE_CONFIG_CONTENT='{"experimental":{"auto_approve":true},"auto_approve":{"model":"openai/gpt-5.6-luna","show_details":true}}' bun dev .

Use any fast non-reasoning model — reasoning tokens eat the classifier's small output budget. Tab twice from Build. show_details adds an expandable row showing exactly what the model saw and answered. Drop the flag and the mode disappears from the cycle.

Known limitations are tracked in #39412. None block this: the mode is behind a flag and every case fails safe.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

disposeApps() awaited every web handler's dispose() unbounded. A handler
that never settles stranded the run's finalizer, so Effect.runPromise
never resolved and the explicit process.exit(0) was never reached: the
process sat alive indefinitely after printing a clean summary. Observed
repeatedly, including two runs stuck for over two hours past a passing
result. CI runs this chain and would hang rather than fail fast.

Bound the wait, but only for the run's final teardown, where a stalled
handler no longer matters. The same function runs in resetState between
every scenario, and abandoning a live handler there lets
disposeAllInstances() and resetDatabase() run underneath it and leak
state into the next scenario -- an earlier unconditional timeout did
exactly that, failing three unrelated v2 routes.
Adds Auto-approve, an opt-in TUI mode that has a fast model review each
consequential action before it runs. Safe, clearly authorized actions run
without interrupting the user; anything unsafe, ambiguous, or
unauthorized falls back to the normal approval prompt. Distinct from the
existing --auto flag, which approves everything not denied with no
review; that behavior is unchanged.

The mode is gated behind experimental.auto_approve. While the flag is
unset it is absent from the agent cycle, the classifier refuses before
any model call, and the overlay endpoint refuses to enable.

The shipped agent ruleset is `*: allow`, so Permission.ask() returns
before creating a pending request and the mode would have had nothing to
review on a default install. Entering it installs an ephemeral,
in-memory, per-session overlay that upgrades allow to ask for
consequential permissions (bash, edit, task, webfetch, websearch,
external_directory, skill). Reads are never escalated, so browsing stays
fast. The overlay is never persisted, is released on leaving the mode,
and dies with the instance.

Safety properties:
- deny is never softened; the deny branch returns before the overlay is
  consulted, so this is structural rather than conditional
- an explicit "always" grant is honored rather than re-classified
- subagent sessions are never auto-approved, since the parent model
  authors the child's "user" message and could otherwise manufacture its
  own authorization
- broad standing grants are not treated as authorization, because that is
  what an injection would plant
- every failure path -- timeout, unavailable model, malformed output,
  interruption -- falls back to the prompt, never to an approval

Every auto-approval is recorded in the transcript under the tool call it
authorized, and is marked applied only when this client's own reply was
accepted: a server-side reject cascades to other pending requests in the
session, so treating that as success would record an approval for an
action that never ran. auto_approve.show_details additionally reveals the
exact classifier input and output, withheld by default because the input
contains the user's request text. Traces stay in TUI memory and are never
written to session history or replayed to later model turns.

The classifier input includes the originating tool call, so a request is
judged on the operation rather than on a bare filepath, and the output
cap tolerates models that spend tokens before answering. A classification
that cannot succeed reports why instead of failing silently.

A task's subagent prompt is withheld from external ACP clients: it is
carried in permission metadata for the classifier, which reads it
in-process, and nothing in the ACP payload uses it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: "Auto mode" LLM model classifier auto-approval for permissions

1 participant