Worktree sessions: inherit parent's always-allow permissions #101

Closed
opened 2026-06-04 01:29:03 +02:00 by dries · 0 comments
Owner

Problem

Worktree sessions launched from a parent OpenCode session (via MCP split_to_worktree or the /wt UI flow) start with no knowledge of the parent's runtime "Allow always" approvals. The user is re-prompted for every pattern the parent already accepted.

OpenCode keeps "Allow always" state in-memory in the parent process and exposes no API to read it. ocman, however, already records every judge-approved permission's patterns in the auto_approved_permission table. By extending that capture to also include user-clicked "Allow always" replies, ocman has a near-complete shadow of the parent's runtime allowlist — and can replay it into the child via OPENCODE_PERMISSION.

Goal

Worktree sessions launched from a parent session inherit the parent's accumulated always-allow patterns at split time. Default-on Settings toggle. Soft-fail on any error (log + result-payload note, never block launch).

Scope

  • IN: MCP split_to_worktree
  • IN: /wt UI flow (POST /api/worktree/create-and-launch)
  • OUT: split_to_session (same cwd, already inherits project config via cwd-walk)
  • OUT: live propagation after launch — snapshot at split time only
  • OUT: inheriting Allow once replies
  • OUT: per-permission-key granularity in the setting (start global; can split later)

Approach in one paragraph

OpenCode permissions config is loaded with OPENCODE_PERMISSION (inline JSON) taking precedence over project/global config. We pass that env var through tmux's -e KEY=VALUE flag at child launch. The value is a permission object synthesised from the parent session's auto_approved_permission rows: judge-approved patterns (already captured) plus user-clicked "Allow always" patterns (new capture in this issue). The child OpenCode loads it as policy — effectively replaying the parent's runtime allowlist.

Implementation steps

1. Capture user-clicked "Allow always" approvals

internal/server/autoapprove.go + autoapprove_watcher.go

  • Maintain a bounded (~1024-entry LRU) map[sessionID|permissionID] -> {permission, patterns} populated on permission.asked, evicted on permission.replied.
  • On permission.replied with reply == "always", call stateDB.RecordApprovedPermission with Reasoning = "user clicked Allow always", JudgeSessionID = "".
  • Tests in autoapprove_test.go: asked->replied("always") writes a row; "once"/"reject" do not.

2. New package internal/permissions/inherit.go

  • BuildInheritedPermissionsJSON(lister, platform, parentSessionID) (json string, count int, err error).
  • Parse permission key out of PermissionText against the OpenCode allowlist: read, edit, glob, grep, list, bash, task, external_directory, todowrite, question, webfetch, websearch, repo_clone, repo_overview, lsp, doom_loop, skill.
  • Flat-action keys (todowrite, question, webfetch, websearch, doom_loop) -> "<key>": "allow".
  • Patternable keys -> ordered object {"*": "ask", <dedup allow patterns>...}. OpenCode evaluates the last matching rule, so *: ask first, allows last.
  • Compact-marshal; cap at 500 patterns total (log + truncate above).
  • Empty parent -> ("", 0, nil).
  • Tests: grouping, ordering, flat keys, dedup, unknown-key drop, truncation.

3. tmux env-var plumbing

internal/server/tmux.go + tests

  • Extend tmuxRunner callbacks with env map[string]string:
    • newSession(name, directory string, env map[string]string, command string) error
    • newWindow(name, directory string, env map[string]string, command string) error
    • newNamedWindow(sessionName, windowName, directory string, env map[string]string, command string) error
  • Translate env to repeated -e KEY=VALUE args between -c <dir> and the command.
  • Validate keys (^[A-Z_][A-Z0-9_]*$); reject NUL/newline in values.
  • Update all call sites (most pass nil) and test fakes in tmux_idempotent_test.go.
  • New test asserting -e OPENCODE_PERMISSION=... reaches the runner.

4. Launcher accepts inherited env

internal/mcp/launcher.go

  • Add InheritedEnv map[string]string to LaunchRequest.
  • Change TmuxLauncher to func(projectDir, worktreeDir string, env map[string]string) (target string, launched bool, err error).
  • LaunchWithWorktree forwards req.InheritedEnv.
  • Update server.go wiring (three NewSessionLauncher call sites) and launcher_test.go fakes.

5. Setting (no schema migration — reuse generic setting KV table from v12)

internal/state/db.go

  • Key: worktree.inherit_permissions, values "1"/"0", default "1" when row absent.
  • GetWorktreeInheritPermissions() (bool, error) + SetWorktreeInheritPermissions(bool) error.
  • GET/POST /api/settings/worktree-inherit-permissions, wired in server.go mux.

6. Wire MCP split_to_worktree

internal/mcp/tools_split.go

  • After GetSession, if setting on, call BuildInheritedPermissionsJSON.
  • Non-empty result -> req.InheritedEnv["OPENCODE_PERMISSION"] = json.
  • Result map additions:
    • permissionsInherited (bool)
    • permissionsInheritedCount (int)
    • permissionsInheritError (string, only on failure)
  • Update tools_test.go.

7. Wire /wt UI flow

Backendinternal/server/handlers_worktree.go

  • Accept optional parentSessionId in request body.
  • If setting on AND parentSessionId != "", build JSON via the same helper.
  • Use the env-aware tmux launch variant.
  • Add permissionsInherited/permissionsInheritedCount/permissionsInheritError to response JSON.
  • Extend handlers_worktree_test.go.

Frontend

  • frontend/src/lib/api.types.ts: add parentSessionId?: string to WorktreeCreateRequest; add the three new fields to WorktreeCreateResponse.
  • frontend/src/components/WorktreeFormModal.tsx:
    • Accept current sessionId prop (audit all call sites: command-palette /wt, per-project Worktrees view).
    • Pass through as parentSessionId.
    • Render small grey hint: "Will inherit N approved permissions from current session" — reuse the existing approved-permissions endpoint and .length client-side.
    • Hint hidden when setting off or count is 0.
  • New Settings UI toggle: "Worktree sessions inherit parent permissions" wired to the new endpoint.
  • Tests: api.worktree.test.ts, modal hint rendering, settings panel.

8. Docs

  • AGENTS.md: short note under MCP server and Worktrees sections.
  • .opencode/skills/ocman-session-splitting/SKILL.md: agent should mention permissionsInherited{,Count} in its summary.

Verification

After each phase:

  • make lint && make test
  • Manual:
    1. Approve a bash "git *" pattern in a parent session -> spawn worktree -> child does not prompt for git status.
    2. Disable the setting -> child prompts as before.
    3. Force malformed pattern (NUL injection in test) -> soft-fail surfaces in response.

Risks

Risk Mitigation
OPENCODE_PERMISSION exceeds OS env-var limit on highly active sessions Cap at 500 patterns; log+truncate above
In-process asked->replied map leak Bounded LRU + eviction on replied
OpenCode permission JSON schema drift Pin to documented shape; soft-fail with note in response
PermissionText parser misidentifies key Allowlist of known keys; drop unknown with debug log
tmux -e quoting edge cases exec.Command argv (no shell interpolation); document min tmux version
Many test fakes need signature updates One-time mechanical refactor; CI catches misses
## Problem Worktree sessions launched from a parent OpenCode session (via MCP `split_to_worktree` or the `/wt` UI flow) start with no knowledge of the parent's runtime "Allow always" approvals. The user is re-prompted for every pattern the parent already accepted. OpenCode keeps "Allow always" state in-memory in the parent process and exposes no API to read it. ocman, however, already records every judge-approved permission's patterns in the `auto_approved_permission` table. By extending that capture to also include user-clicked "Allow always" replies, ocman has a near-complete shadow of the parent's runtime allowlist — and can replay it into the child via `OPENCODE_PERMISSION`. ## Goal Worktree sessions launched from a parent session inherit the parent's accumulated always-allow patterns at split time. Default-on Settings toggle. Soft-fail on any error (log + result-payload note, never block launch). ## Scope - IN: MCP `split_to_worktree` - IN: `/wt` UI flow (`POST /api/worktree/create-and-launch`) - OUT: `split_to_session` (same cwd, already inherits project config via cwd-walk) - OUT: live propagation after launch — snapshot at split time only - OUT: inheriting `Allow once` replies - OUT: per-permission-key granularity in the setting (start global; can split later) ## Approach in one paragraph OpenCode permissions config is loaded with `OPENCODE_PERMISSION` (inline JSON) taking precedence over project/global config. We pass that env var through tmux's `-e KEY=VALUE` flag at child launch. The value is a permission object synthesised from the parent session's `auto_approved_permission` rows: judge-approved patterns (already captured) plus user-clicked "Allow always" patterns (new capture in this issue). The child OpenCode loads it as policy — effectively replaying the parent's runtime allowlist. ## Implementation steps ### 1. Capture user-clicked "Allow always" approvals `internal/server/autoapprove.go` + `autoapprove_watcher.go` - Maintain a bounded (~1024-entry LRU) `map[sessionID|permissionID] -> {permission, patterns}` populated on `permission.asked`, evicted on `permission.replied`. - On `permission.replied` with `reply == "always"`, call `stateDB.RecordApprovedPermission` with `Reasoning = "user clicked Allow always"`, `JudgeSessionID = ""`. - Tests in `autoapprove_test.go`: asked->replied("always") writes a row; "once"/"reject" do not. ### 2. New package `internal/permissions/inherit.go` - `BuildInheritedPermissionsJSON(lister, platform, parentSessionID) (json string, count int, err error)`. - Parse permission key out of `PermissionText` against the OpenCode allowlist: `read, edit, glob, grep, list, bash, task, external_directory, todowrite, question, webfetch, websearch, repo_clone, repo_overview, lsp, doom_loop, skill`. - Flat-action keys (`todowrite, question, webfetch, websearch, doom_loop`) -> `"<key>": "allow"`. - Patternable keys -> ordered object `{"*": "ask", <dedup allow patterns>...}`. OpenCode evaluates the **last** matching rule, so `*: ask` first, allows last. - Compact-marshal; cap at 500 patterns total (log + truncate above). - Empty parent -> `("", 0, nil)`. - Tests: grouping, ordering, flat keys, dedup, unknown-key drop, truncation. ### 3. tmux env-var plumbing `internal/server/tmux.go` + tests - Extend `tmuxRunner` callbacks with `env map[string]string`: - `newSession(name, directory string, env map[string]string, command string) error` - `newWindow(name, directory string, env map[string]string, command string) error` - `newNamedWindow(sessionName, windowName, directory string, env map[string]string, command string) error` - Translate env to repeated `-e KEY=VALUE` args between `-c <dir>` and the command. - Validate keys (`^[A-Z_][A-Z0-9_]*$`); reject NUL/newline in values. - Update all call sites (most pass `nil`) and test fakes in `tmux_idempotent_test.go`. - New test asserting `-e OPENCODE_PERMISSION=...` reaches the runner. ### 4. Launcher accepts inherited env `internal/mcp/launcher.go` - Add `InheritedEnv map[string]string` to `LaunchRequest`. - Change `TmuxLauncher` to `func(projectDir, worktreeDir string, env map[string]string) (target string, launched bool, err error)`. - `LaunchWithWorktree` forwards `req.InheritedEnv`. - Update `server.go` wiring (three `NewSessionLauncher` call sites) and `launcher_test.go` fakes. ### 5. Setting (no schema migration — reuse generic `setting` KV table from v12) `internal/state/db.go` - Key: `worktree.inherit_permissions`, values `"1"`/`"0"`, default `"1"` when row absent. - `GetWorktreeInheritPermissions() (bool, error)` + `SetWorktreeInheritPermissions(bool) error`. - `GET`/`POST /api/settings/worktree-inherit-permissions`, wired in `server.go` mux. ### 6. Wire MCP `split_to_worktree` `internal/mcp/tools_split.go` - After `GetSession`, if setting on, call `BuildInheritedPermissionsJSON`. - Non-empty result -> `req.InheritedEnv["OPENCODE_PERMISSION"] = json`. - Result map additions: - `permissionsInherited` (bool) - `permissionsInheritedCount` (int) - `permissionsInheritError` (string, only on failure) - Update `tools_test.go`. ### 7. Wire `/wt` UI flow **Backend** — `internal/server/handlers_worktree.go` - Accept optional `parentSessionId` in request body. - If setting on AND `parentSessionId != ""`, build JSON via the same helper. - Use the env-aware tmux launch variant. - Add `permissionsInherited`/`permissionsInheritedCount`/`permissionsInheritError` to response JSON. - Extend `handlers_worktree_test.go`. **Frontend** - `frontend/src/lib/api.types.ts`: add `parentSessionId?: string` to `WorktreeCreateRequest`; add the three new fields to `WorktreeCreateResponse`. - `frontend/src/components/WorktreeFormModal.tsx`: - Accept current `sessionId` prop (audit all call sites: command-palette `/wt`, per-project Worktrees view). - Pass through as `parentSessionId`. - Render small grey hint: "Will inherit N approved permissions from current session" — reuse the existing approved-permissions endpoint and `.length` client-side. - Hint hidden when setting off or count is 0. - New Settings UI toggle: "Worktree sessions inherit parent permissions" wired to the new endpoint. - Tests: `api.worktree.test.ts`, modal hint rendering, settings panel. ### 8. Docs - `AGENTS.md`: short note under MCP server and Worktrees sections. - `.opencode/skills/ocman-session-splitting/SKILL.md`: agent should mention `permissionsInherited{,Count}` in its summary. ## Verification After each phase: - `make lint && make test` - Manual: 1. Approve a `bash "git *"` pattern in a parent session -> spawn worktree -> child does not prompt for `git status`. 2. Disable the setting -> child prompts as before. 3. Force malformed pattern (NUL injection in test) -> soft-fail surfaces in response. ## Risks | Risk | Mitigation | |---|---| | `OPENCODE_PERMISSION` exceeds OS env-var limit on highly active sessions | Cap at 500 patterns; log+truncate above | | In-process asked->replied map leak | Bounded LRU + eviction on `replied` | | OpenCode `permission` JSON schema drift | Pin to documented shape; soft-fail with note in response | | `PermissionText` parser misidentifies key | Allowlist of known keys; drop unknown with debug log | | tmux `-e` quoting edge cases | `exec.Command` argv (no shell interpolation); document min tmux version | | Many test fakes need signature updates | One-time mechanical refactor; CI catches misses |
dries closed this issue 2026-07-10 09:31:32 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dries/ocman#101
No description provided.