Skip to content

feat(server): OpenCode 2 approvals, questions and every runtime mode - #14407

Merged
juliusmarminge merged 3 commits into
t3code/codex-turn-mappingfrom
v2/oc2-5-approvals
Oct 1, 2026
Merged

juliusmarminge merged 3 commits into
t3code/codex-turn-mappingfrom
v2/oc2-5-approvals

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Layer 4 left OpenCode 2 in Full access only: other modes were refused, permission asks were auto-rejected, and question forms were auto-answered. This layer maps every runtime mode onto OpenCode 2 session rules, and shows real approvals and questions.

Runtime modes → session permissions

Rules use the v2 action names and are built by one function, sessionRules(policy, paths, grants), so later layers can append their own rules (layer 9: [...modeRules, ...mcpRules]).

Mode Session rules
Full access * * allow
Supervised, Auto shell ask, edit ask, external_directory ask
Auto-accept edits shell ask, edit allow, external_directory ask
Plan mode (any mode) adds edit * deny, then the plan agent's own edit/external_directory allows for ~/.opencode/plan/*
Always subagent * deny last (until layer 6)
  • Rule order: last match wins. From the 2.0.18 binary, Permission.evaluate is rules.findLast(match(action) && match(resource)) ?? {effect:"ask"} over [...agentRules, ...sessionRules, ...savedGrants]. Session rules therefore override the agent's own rules. That is why T3's old * * allow let the plan agent write workspace files.
  • The agent's own path allows are kept. These are its saved tool output, shell output, config dir and plan dir. They're read from GET /api/agent and appended after the blanket rules, only for the modes that narrow Full access. Without them, reading a truncated shell output would ask (seen live), and plan mode could not write its plan.
  • A fresh server lists no agents for its first moments, as it does with models. The read retries an empty list for up to 5 seconds, and an empty answer is never cached (seen live; covered by an adapter test).
  • No deny on shell or read. DESIGN risk 2: the free tier refuses such sessions. T3 has no read-only runtime mode, so nothing needs "ask plus automatic reject". Auto has no OpenCode reviewer, so it asks like Supervised, matching 1.x.
  • Mode switches mid-thread. supportsRuntimeModeSwitchInSession is now true, so a mode change detaches nothing. The next turn resumes the same session and rewrites its rules via session.update (diffed, so it's skipped when nothing changed). startTurn also rewrites them, which covers plan-mode toggles.
  • Snapshot. supportedRuntimeModes is now all four modes; the Full-access-only refusal is gone.

Approvals

  • permission.asked becomes an approval request on the asking session's thread and running turn. The kind comes from the tool (command, file-change or file-read, via the 1.x helper), and the prompt shows the resources.
  • Subagent requests land on the parent thread's turn. Children are mapped from session.created{parentID}; verified live, see below. Layer 6 builds subagents on this.
  • Decisions:
    • Approve sends once.
    • Decline sends reject with a message, which fails only that tool call.
    • Cancel sends reject without a message, which OpenCode treats as "stop" and ends the whole execution. That matched Cancel, so it was kept.
  • Always stays session-scoped (fix(server): Grok's "Always allow this session" no longer saves a project-wide grant #13796). OpenCode 2's always is not session-scoped. Permission.reply saves it with PermissionSaved.add({projectID, …}).
    • Verified live: after always on echo * in one session, a brand-new session in the same directory ran echo … without asking. GET /api/permission/saved?location[directory]=… then listed the grant.
    • So T3 never sends always. "Allow <save pattern> this session" (for example "Allow echo * this session") adds {action, resource: save, effect: allow} to this session's own rules and replies once.
    • The grant lives in T3's in-memory state for the session, and is dropped when T3 unloads the thread. The option appears only when OpenCode supplies save patterns.
  • A reject ends the attempt cleanly. The reject message never reaches the model: a declined shell call reads only as "Unable to execute command", so the model retried (the spike saw this 3×).
    • So a decline first steers a synthetic note into the session (POST /session/{id}/synthetic, delivery: "steer", resume: false), then rejects.
    • Live on big-pickle: with the note, 3 of 3 runs stopped after one ask. Without it, 2 of 3 runs asked again.
  • Nothing is left stuck:
    • An answer that can't be delivered after one retry fails the turn and interrupts the session.
    • A request whose execution ended is closed with the turn.
    • permission.replied, form.replied and form.cancelled from other clients settle T3's copy.
    • A request raised by a run that a Stop left behind is rejected or cancelled at once.
  • Restart and reconnect. Pending requests don't survive a server restart (pendingRequestsSurviveRestart: false, so the orchestrator expires them). When a runtime first loads a session, it lists GET session/{id}/permission and GET session/{id}/form. It stops anything still open, because T3 no longer shows it. It then skips that stopped run's end, so the next turn isn't ended by it.

Questions

  • form.created (from the question tool) becomes a structured user-input request:
    • fields map to questions;
    • options keep their value;
    • custom maps to allowCustomAnswer;
    • multiselect maps to multiSelect, and its answer goes back as string[].
  • Answers go to session.form.reply; Cancel or Decline goes to session.form.cancel.
  • The question tool call itself is not shown as an item; the form is the item, as in 1.x.
  • when, hidden, external and number/boolean fields aren't supported. V2's model has no conditional, hidden, link or typed fields. Such a form is cancelled at once, and the turn fails with the reason (for example "OpenCode asked for a link to open, which T3 Code can't show"). It never hangs. The question tool only produces string and multiselect fields.
  • The layer 4 auto-answer and auto-reject are gone.

Verification

  • Replay fixtures through the whole orchestrator. OrchestratorReplayFixtures.integration.test.ts: all 103 pass. The two new ones, converted from the spike recordings:
    • opencode2_permission (permission.ndjson, Supervised): approve once, then decline the command and the model's two retries. It asserts four resolved requests, one run and three failed shell items, and the session-scoped option.
    • opencode2_question (question.ndjson): the form becomes a question with options and a custom answer, the answer goes back, and the reply uses it.
    • Both fail against the layer 4 adapter.
    • The fixture harness now lets consecutive answer steps wait for the next request instead of an idle thread. Every other fixture passes unchanged.
  • OpenCode2OrchestratorV2.integration.test.ts: 7 of 7 pass. New tests:
    • a Supervised session is created with ask rules;
    • mode change: Full access, then Auto-accept edits, then Full access, on one session, with session.update each time;
    • plan mode denies edits except the plan dir, and the deny goes away when plan mode is switched off.
    • All three fail against the layer 4 adapter.
  • OpenCode2AdapterV2.test.ts: 33 of 33 pass. New direct tests cover failures a live server can't produce on demand:
    • leftover requests after a restart;
    • an answer that can't be delivered;
    • an external form declined with its reason;
    • a subagent's ask landing on the parent turn;
    • the session-scoped grant written into the rules;
    • the empty agent list retried.
  • OpenCodeProvider.test.ts and OrchestratorReplayFixtures.contract.test.ts pass.
  • tsc exits 0. vp lint on the touched files shows only an existing shared.ts warning. vp run knip:check exits 0. ns-audit.py flags no lines I changed.
  • Live on 2.0.18, through the real driver and orchestrator (OpenCode2OrchestratorV2.live.test.ts, isolated HOME/XDG), in a Supervised thread:
    • touch approved.txt asked, was approved, and ran;
    • touch declined.txt asked, was declined, and did not run (the steered note is in the session history);
    • the question tool asked red/blue, T3 answered Blue, and the reply was "blue".
    • Plan mode on Full access: the write to ~/.opencode/plan/probe-plan.md succeeded, and the write to plan_write_probe.txt in the workspace was blocked.
    • Passed on opencode/big-pickle, and again on openrouter/deepseek/deepseek-v4-flash (key passed only through the test's environment). The layer 4 live test also passes.
  • Also probed live with small HTTP scripts (not committed):
    • always persists project-wide;
    • a reject without a message ends the execution, while one with a message fails only the call;
    • a mid-execution session.update applies to later asks;
    • interrupt and form cancel drop pending requests;
    • a subagent's ask arrives with the child's sessionID after session.created{parentID};
    • child sessions inherit the parent's session rules.

Left out

  • Subagent lifecycle and items (layer 6). Only the request routing is here.
  • Switching the session to OpenCode's plan agent (layer 8). The plan rules already hold with any agent: verified live with both the plan agent and the build agent.
  • MCP rules (layer 9) compose after these via sessionRules.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 30, 2026
Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
@macroscopeapp

macroscopeapp Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds substantial OpenCode 2 production behavior: runtime permission enforcement, mode switching, approval flows, structured questions, and request lifecycle handling. Because these changes alter execution and permission boundaries and introduce a new user-facing workflow, human review is warranted.

No code changes detected at 5e6e896. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.8 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: 5e6e896 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the v2/oc2-4b-adapter-usage branch from 608cbda to dfef90f Compare October 1, 2026 00:11
@juliusmarminge
juliusmarminge force-pushed the v2/oc2-4b-adapter-usage branch from dfef90f to 6fa4c03 Compare October 1, 2026 00:28
@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 1, 2026 — with ChatGPT Codex Connector
Base automatically changed from v2/oc2-4b-adapter-usage to t3code/codex-turn-mapping October 1, 2026 03:40
juliusmarminge and others added 3 commits September 30, 2026 20:40
Each runtime mode is now a set of OpenCode 2 session permission rules
(v2 action names), written at session.create and rewritten by
session.update on resume and before each turn when the mode or plan
mode changed. Permission asks become approval requests and question-tool
forms become user input requests on the asking session's thread,
including a subagent's. Answers go to permission.reply and
session.form.reply/cancel.

"Allow this session" is a rule on the session, not OpenCode's `always`,
which saves a grant for the whole project. A decline steers a note into
the session first, so the model stops retrying the declined call.
Requests a restarted T3 no longer shows are stopped when it loads the
session again. Forms with fields T3 cannot ask are declined with the
reason. The Full-access-only refusal and the auto-answers are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The failure can carry the server's URL or response text, so it stays in the
log's cause and only its tag is annotated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A reply or cancel for a request OpenCode already dropped counts as
delivered. Each call site now catches exactly the not-found tags its
client call can fail with, instead of a tag set checked at runtime. A
test pins that such an answer does not fail the turn.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.ts
@juliusmarminge
juliusmarminge merged commit 5eae8ff into t3code/codex-turn-mapping Oct 1, 2026
23 checks passed
@juliusmarminge
juliusmarminge deleted the v2/oc2-5-approvals branch October 1, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant