opencode-swarm 7.135.2 → 7.135.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/critic-gate/SKILL.md +10 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/cli/{config-doctor-pj3yvxdb.js → config-doctor-4nznqeca.js} +3 -2
- package/dist/cli/{core-4va9z2ng.js → core-4z1s2ak1.js} +3 -2
- package/dist/cli/{curation-policy-b2caf3v3.js → curation-policy-81m14519.js} +3 -3
- package/dist/cli/{curator-drift-2ta83hss.js → curator-drift-jpap6hv4.js} +2 -2
- package/dist/cli/{curator-r1s2mwf2.js → curator-e0z5gcww.js} +24 -23
- package/dist/cli/{curator-llm-factory-9983528d.js → curator-llm-factory-fem13y9n.js} +24 -23
- package/dist/cli/{dispatch-wtk6dxky.js → dispatch-b1fge4xk.js} +2 -2
- package/dist/cli/{evidence-summary-service-feby8ny0.js → evidence-summary-service-w006jnpg.js} +5 -5
- package/dist/cli/{gate-evidence-84b03mb4.js → gate-evidence-aenyz6vt.js} +2 -2
- package/dist/cli/{guardrail-explain-pwxvg5n0.js → guardrail-explain-wgaj23hd.js} +25 -24
- package/dist/cli/{guardrail-log-yzpz8b7t.js → guardrail-log-04p1z1vt.js} +5 -4
- package/dist/cli/{hive-promoter-586dhndj.js → hive-promoter-k345z42k.js} +24 -23
- package/dist/cli/{index-g3meqrsg.js → index-168phrs7.js} +12 -10
- package/dist/cli/{index-3m7kpnqf.js → index-30pg2rs3.js} +1 -1
- package/dist/cli/{index-b0eva00y.js → index-36v55ryg.js} +1 -1
- package/dist/cli/index-3jcyn8g6.js +1219 -0
- package/dist/cli/{index-hdy2nd5h.js → index-45t7w06b.js} +4 -4
- package/dist/cli/{index-bs4m85v1.js → index-5436jxnw.js} +2 -2
- package/dist/cli/{index-j92ser8n.js → index-a01r818w.js} +5 -3
- package/dist/cli/{index-hvrrq3f3.js → index-bcmrg4t4.js} +1 -1
- package/dist/cli/{index-d5psymdk.js → index-c9ddxv4k.js} +57 -8
- package/dist/cli/{index-7pnvf04s.js → index-cggqh2dz.js} +10 -10
- package/dist/cli/{index-340chr1e.js → index-dwargd10.js} +10105 -9998
- package/dist/cli/{index-ckdva4rs.js → index-f52jt0am.js} +5 -5
- package/dist/cli/{index-jgkjxpdh.js → index-gxxxmajn.js} +2 -2
- package/dist/cli/{index-2pxk6qg4.js → index-kb7qy7an.js} +4 -2
- package/dist/cli/{index-j0kte6gd.js → index-p5kr35wh.js} +8 -1210
- package/dist/cli/{index-02cqjna3.js → index-p9ngg568.js} +4 -4
- package/dist/cli/{index-8gw8p7yh.js → index-rv7s2k9y.js} +1 -1
- package/dist/cli/{index-3xbx305x.js → index-tcn457d5.js} +1 -1
- package/dist/cli/{index-9c3gf2wd.js → index-wygxzvtf.js} +6 -4
- package/dist/cli/{index-73dfyfhq.js → index-x20cgr2q.js} +1 -1
- package/dist/cli/{index-cwdy5vw9.js → index-y47crdg9.js} +30 -28
- package/dist/cli/{index-qqkw794p.js → index-yf30m841.js} +3 -3
- package/dist/cli/index.js +27 -25
- package/dist/cli/{knowledge-escalator-w7by1m8m.js → knowledge-escalator-42ecv7m3.js} +4 -4
- package/dist/cli/{knowledge-events-n8xxsay3.js → knowledge-events-zhna7v3g.js} +2 -2
- package/dist/cli/knowledge-link-mm1w967j.js +1 -1
- package/dist/cli/{knowledge-store-xzmvfnx3.js → knowledge-store-56jcfc5k.js} +2 -2
- package/dist/cli/{knowledge-validator-5q1amsq6.js → knowledge-validator-8zk0pnb6.js} +5 -5
- package/dist/cli/{pending-delegations-mt5gs1bt.js → pending-delegations-qajsxct0.js} +4 -4
- package/dist/cli/{pr-subscriptions-v5xd3j6d.js → pr-subscriptions-qhr41epq.js} +4 -4
- package/dist/cli/{scan-cursor-1q1zmt4b.js → scan-cursor-h3ndxd41.js} +3 -3
- package/dist/cli/{schema-8mtsfspm.js → schema-r6wmd7zp.js} +2 -1
- package/dist/cli/scope-persistence-h2fpgxww.js +2 -2
- package/dist/cli/{skill-generator-54xbjgak.js → skill-generator-12cftsx4.js} +6 -6
- package/dist/cli/{worktree-collision-ownership-15hymf1y.js → worktree-collision-ownership-13btcj9g.js} +5 -5
- package/dist/commands/approve-plan-critic.d.ts +21 -0
- package/dist/commands/registry.d.ts +8 -0
- package/dist/config/cache-paths.d.ts +48 -0
- package/dist/config/constants.d.ts +18 -0
- package/dist/config/host-path.d.ts +95 -0
- package/dist/config/lane-context.d.ts +117 -0
- package/dist/config/lane-permissions.d.ts +277 -0
- package/dist/config/schema.d.ts +10 -0
- package/dist/config/swarm-branch.d.ts +130 -0
- package/dist/hooks/delegation-gate.d.ts +42 -1
- package/dist/hooks/skill-propagation-gate.d.ts +10 -0
- package/dist/index.js +211 -199
- package/dist/memory/schema.d.ts +4 -4
- package/dist/tools/approve-plan-critic.d.ts +5 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/manifest.d.ts +1 -0
- package/dist/tools/tool-metadata.d.ts +4 -0
- package/package.json +1 -1
- package/dist/cli/{index-bfd4xfpp.js → index-4qzeef9h.js} +3 -3
- package/dist/cli/{index-ryqnn3nv.js → index-mrtms113.js} +3 -3
- package/dist/cli/{index-m0ynxkaa.js → index-vm4xw9z3.js} +3 -3
|
@@ -118,6 +118,24 @@ export declare const AUTO_PROCEED_BANNER = "## \u23ED\uFE0F AUTO-PROCEED STATUS\
|
|
|
118
118
|
* Zod schema in schema.ts).
|
|
119
119
|
*/
|
|
120
120
|
export declare const DEFAULT_LEAN_TURBO_CONFIG: LeanTurboConfig;
|
|
121
|
+
/**
|
|
122
|
+
* Directory name of the DD-6 default swarm-managed worktree base, created as a
|
|
123
|
+
* SIBLING of the project root (`<project-parent>/.swarm-worktrees`).
|
|
124
|
+
*
|
|
125
|
+
* Single source of truth shared by `resolveWorktreeBaseDir` in
|
|
126
|
+
* `src/worktree/core.ts` (which BUILDS lane paths) and
|
|
127
|
+
* `src/config/lane-context.ts` (which RECOGNISES a lane path after the fact,
|
|
128
|
+
* from inside the lane's own OpenCode instance). Those two must never drift: if
|
|
129
|
+
* creation and recognition disagree, a lane instance silently fails to be
|
|
130
|
+
* identified as a lane and falls back to unscoped permission behaviour.
|
|
131
|
+
*
|
|
132
|
+
* It lives in this leaf constants module rather than in `src/worktree/core.ts`
|
|
133
|
+
* so that the init-path-safe lane modules can share it without pulling the
|
|
134
|
+
* worktree lifecycle module into the plugin entry's import graph
|
|
135
|
+
* (AGENTS.md invariant 1; enforced by
|
|
136
|
+
* `tests/unit/turbo/lean/init-safety.test.ts`).
|
|
137
|
+
*/
|
|
138
|
+
export declare const SWARM_WORKTREE_DIR_NAME = ".swarm-worktrees";
|
|
121
139
|
export declare const DEFAULT_WORKTREE_ISOLATION_CONFIG: WorktreeIsolationConfig;
|
|
122
140
|
export declare const LEAN_TURBO_BANNER = "## \uD83D\uDEE4\uFE0F LEAN TURBO ACTIVE\n\nLane-based parallel execution is enabled for this phase.\n\nBehavioral changes:\n- Tasks are partitioned into parallel lanes based on file-scope conflicts. Tasks in the same lane run sequentially; tasks in different lanes run concurrently (up to max_parallel_coders).\n- **Lane dispatch overrides the one-agent-per-message rule**: for lean lane dispatch only, you may send multiple Task tool calls concurrently (one per lane).\n- **Lane tasks skip per-task Stage B** (reviewer + test_engineer). Quality is enforced at phase-end via phase reviewer and critic gates instead.\n- **Degraded tasks** (global files, protected paths, high-risk patterns) and **serialized tasks** (lock-conflicted) run through standard serial workflow with full Stage B gates.\n- **Phase reviewer and critic are REQUIRED** before phase_complete when lean turbo is active \u2014 they serve as the holistic quality gate for all lane work.\n- **Full-Auto composition**: if Full-Auto is also active, lane dispatch is subject to Full-Auto delegation policy and phase approval.\n- Use the lean_turbo_run_phase tool to execute a phase with parallel lanes\n\nDo NOT skip phase reviewer/critic when configured. Degraded and serialized tasks MUST still go through full Stage B.\n";
|
|
123
141
|
export declare const EPIC_MODE_BANNER = "## \uD83E\uDDED EPIC MODE ACTIVE\n\n**\u26D4 THE USER ALWAYS COMES FIRST \u2014 this overrides everything below.** The user can message you at ANY time, including mid-phase while coders are running or retrying. The instant a user message arrives \u2014 a question, a slash command, a comment, anything \u2014 STOP advancing the flow. Do not dispatch, do not retry, do not call another tool. Read what they said and respond to them directly, in plain conversation, first. Never keep executing the protocol and leave a user message unanswered \u2014 ignoring the user is the single worst failure mode in this mode. After you've answered, pick up where you left off. If you're mid-wave when they interrupt, tell them the state (\"3.1 and 3.2 are still running; I'll continue once I've answered you\") rather than going silent.\n\n**Activation \u2260 start.** Until the user asks for execution (\"start phase N\", \"run task X\", \"continue\"): do nothing. On `/swarm turbo epic`, `/swarm epic *` and any slash status/config command: call the named tool ONCE, surface its output VERBATIM, then stop. Don't infer intent \u2014 if unsure, ASK. This restraint applies ONLY before activation.\n\n**Talk to the user as you work** \u2014 like you naturally would. Once they ask you to run a phase, keep them in the loop with a sentence before each step about what you're doing and why (\"Declaring scopes for 3.1\u20133.3 so the planner can find parallelism\u2026\", \"Discrimination and calibration are independent, so I'll run 3.1 and 3.2 in parallel\u2026\"). This is normal conversation, not a form to fill in \u2014 the steps below tell you the key facts to share, but say them in your own voice. Don't go silent and tool-only through a phase.\n\nUse `epic_plan_waves` (NOT `lean_turbo_plan_lanes` or the deprecated `epic_run_phase`) for the wave plan. Do NOT call `lean_turbo_run_phase` directly.\n\n### Six-step flow (only when the user asks to run a phase)\n\n> Supersedes Rule 1a/3a: declare ALL pending scopes UP FRONT (step 1), BEFORE step 2. Just-in-time declaration breaks the wave planner.\n\n**1. `declare_scope` for every pending task** \u2014 one call per single `taskId` string (NOT ranges/arrays/globs). Tight, disjoint scopes; avoid shared files (`__init__.py`, barrels, registries) \u2014 they force serial waves. Declared scope is a CONTRACT; if a task needs more files mid-run, re-declare BEFORE dispatching.\n\n**2. `epic_decide_phase(directory, phase=N, sessionID)`** \u2014 returns:\n- `decided`+`promote` \u2192 step 3\n- `demoted` \u2192 step 6 (per-task serial)\n- `scopes-missing` \u2192 `declare_scope` each `missingScopes[]`, retry step 2\n- `no-phase` | `phase-empty` | `phase-already-complete` | `epic-state-unreadable` \u2192 fix per response `message`, retry. `phase-already-complete` means call step 2 with `phase=N+1` (NOT step 4 directly).\n- other \u2192 fix per `message`, retry\n\n**3. Surface the verdict to the user immediately, before any further action:**\n> Epic Mode: <PROMOTE|DEMOTE> (p=<value>) \u2014 <one-sentence rationale or top blocking reason>\n> Dependencies: <task_id> \u2190 <deps>; \u2026 (omit if none)\n\nThe verdict is the user's only visibility into what Epic is doing \u2014 silence here makes the mode invisible. If you're going to spend time on this phase, tell the user why up front. Phrase it naturally; the format above is a guide, not a script.\n\n**4. `epic_plan_waves(directory, phase=N)`** \u2014 returns `{ waves: [{ waveId, taskIds, files }], serializedTasks, degradedTasks, degradationSummary }`. Failure reasons mirror step 2; additionally: `git-failed` (retry), `planner-error` (check `errors[0]`).\n\n**4b. Surface the wave plan to the user, before dispatching any `Task`:**\n> Wave plan (<N> waves): Wave 1 \u2192 [<ids>] (parallel); Wave 2 \u2192 [<ids>]; \u2026 \u2014 serialized [<ids>], degraded [<ids>]\n\nWalk them through which tasks run in which wave and what's parallel \u2014 naturally, in your own words. If `waves.length` exceeds the distinct-dependency-layer count, also flag the over-split and its likely cause (typical: a shared file like a barrel/registry in multiple scopes forces serial waves), e.g. \"Wave N split into K single-task waves because every scope claims `<shared-file>` \u2014 re-declare those tasks without it to restore parallelism, then re-plan.\"\n\n`serializedTasks` causes (NOT `declare_scope`-fixable): cycle, `no-scope`, `invalid-scope`, cap-exhaustion. Fix dep graph or scope contents, re-plan.\n\n`degradedTasks[].reason` keys:\n- `global file conflict` / `protected path` \u2192 balanced mode, dispatch per-task after waves\n- `cross-batch upstream not committed (greenfield-smart Rule 3): <ids>` \u2192 commit named upstreams, re-plan\n- `unresolved in-batch dependency: <ids>` \u2192 fix upstream degrade/serialize, re-plan\n- `planning leftover (no identifiable blocker)` \u2192 surface as planner bug\n\n**5. Dispatch each wave: `wave.taskIds.length` SEPARATE `Task` calls in ONE assistant message.** Per wave in order:\n- One `Task(subagent_type=\"coder\", description=\"Phase N task <id>\", prompt=\"<scope + acceptance>\")` per `taskId`\n- ALL in same turn \u2192 concurrent\n- Wait for all in wave to reach `update_task_status(completed)` + `epic_record_divergence` before next wave\n\n\u26A0\uFE0F **Three defects:**\n1. **Bundling**: multiple ids in one Task call \u2192 kills 1:1 coder visibility\n2. **Splitting across messages**: serial execution, no parallelism\n3. **Skipping single-task waves**: still emit ONE Task, wait for completion+divergence\n\nThis is the only sanctioned dispatch path. Don't use `lean_turbo_run_phase`; don't bundle through other tools \u2014 visibility requires `Task`.\n\n`serializedTasks` + `degradedTasks` (after wave loop): ONE Task per assistant message each, never batched, wait for completion+divergence between.\n\n**6. After each `update_task_status(completed)`, call `epic_record_divergence(directory, taskId, sessionID)`** (feeds calibration). If `summary.isClean: false`:\n> Divergence: task `<id>` wrote <undeclaredCount> undeclared file(s) (ratio <ratio>)\n\n### Phase-complete + audit\n\nPhase reviewer + critic still required at `phase_complete` (Epic Mode doesn't change Stage B).\n\nAudit (no architect needed): `/swarm epic status | last | decide | calibration`.\n";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faithful transcription of OpenCode's path-canonicalisation helpers.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this must exist
|
|
5
|
+
*
|
|
6
|
+
* The host asks for `external_directory` permission with a pattern it has run
|
|
7
|
+
* through `Filesystem.normalizePathPattern` — for example
|
|
8
|
+
* `src/tools/…` (offset 100715012):
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* let u = G.normalizePathPattern(Hr.join(y, "*"));
|
|
12
|
+
* yield* o.ask({ permission: "external_directory", patterns: [u], always: [u], … });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Rule patterns from config get NO such treatment: `Permission.fromConfig`
|
|
16
|
+
* applies only `~` / `$HOME` expansion. So for a rule to match, its text must
|
|
17
|
+
* already equal what the host will produce for the asked path.
|
|
18
|
+
*
|
|
19
|
+
* That matters because `normalizePathPattern` resolves symlinks via
|
|
20
|
+
* `realpathSync.native`. A lane reached through a Windows junction (or a macOS
|
|
21
|
+
* `/var` → `/private/var` symlink) is asked for under its REAL path, while an
|
|
22
|
+
* un-canonicalised rule names the link path — so the lane is denied access to
|
|
23
|
+
* its own granted directory.
|
|
24
|
+
*
|
|
25
|
+
* Verbatim host source (`C:\OpenCode\opencode.exe`, opencode 1.18.10,
|
|
26
|
+
* offsets 107196170-107196420; the minified names resolve via the chunk's own
|
|
27
|
+
* imports at offset 107192671: `ky`=`path.resolve`, `yy`=`path.join`,
|
|
28
|
+
* `ek`=`fs.realpathSync`):
|
|
29
|
+
*
|
|
30
|
+
* ```js
|
|
31
|
+
* function j(F){ let z=ky(X(F)); try{ return ek.native(z) }catch{ return z } }
|
|
32
|
+
* YW.normalizePath = j;
|
|
33
|
+
*
|
|
34
|
+
* function J(F){
|
|
35
|
+
* if(F==="*") return F;
|
|
36
|
+
* let z=F.match(/^(.*)[\\/]\*$/);
|
|
37
|
+
* if(!z) return j(F);
|
|
38
|
+
* let Z=/^[A-Za-z]:$/.test(z[1]) ? z[1]+"\\" : z[1];
|
|
39
|
+
* return yy(j(Z),"*");
|
|
40
|
+
* }
|
|
41
|
+
* YW.normalizePathPattern = J;
|
|
42
|
+
*
|
|
43
|
+
* function X(F){ return F
|
|
44
|
+
* .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/,(z,Z)=>`${Z.toUpperCase()}:/`)
|
|
45
|
+
* .replace(/^\/([a-zA-Z])(?:\/|$)/,(z,Z)=>`${Z.toUpperCase()}:/`)
|
|
46
|
+
* .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/,(z,Z)=>`${Z.toUpperCase()}:/`)
|
|
47
|
+
* .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/,(z,Z)=>`${Z.toUpperCase()}:/`) }
|
|
48
|
+
* YW.windowsPath = X;
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* If a future OpenCode release changes these, lane rules stop matching. The
|
|
52
|
+
* shared-normaliser tests in `tests/unit/config/lane-permissions.test.ts` are
|
|
53
|
+
* the tripwire — re-extract before "fixing" them.
|
|
54
|
+
*
|
|
55
|
+
* @module config/host-path
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Test-only DI seam (AGENTS.md invariant 7). Lets tests simulate a symlink /
|
|
59
|
+
* junction without creating one, and assert the ENOENT degradation.
|
|
60
|
+
*/
|
|
61
|
+
export declare const _internals: {
|
|
62
|
+
realpathSyncNative: (p: string) => string;
|
|
63
|
+
};
|
|
64
|
+
/** Host `Filesystem.windowsPath`. */
|
|
65
|
+
declare function hostWindowsPath(input: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Host `Filesystem.normalizePath`: `realpathSync.native(path.resolve(windowsPath(p)))`,
|
|
68
|
+
* degrading to the un-realpath'd resolved form on ANY realpath failure.
|
|
69
|
+
*
|
|
70
|
+
* The host uses a bare `catch` here (not an ENOENT check), so a permission
|
|
71
|
+
* error degrades identically to a missing path. Transcribed as-is: diverging
|
|
72
|
+
* would make our rule text disagree with the host's asked text in exactly the
|
|
73
|
+
* cases where a lane is already having filesystem trouble.
|
|
74
|
+
*/
|
|
75
|
+
declare function hostNormalizePath(input: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Host `Filesystem.normalizePathPattern`.
|
|
78
|
+
*
|
|
79
|
+
* Canonicalises the DIRECTORY part of a `<dir>/*` pattern and re-appends the
|
|
80
|
+
* wildcard, leaving the bare `*` catch-all untouched. Note the drive-root
|
|
81
|
+
* special case: for `C:/*` the captured directory is `C:` which
|
|
82
|
+
* `path.resolve` would turn into the process's current directory on that
|
|
83
|
+
* drive, so the host appends a separator first.
|
|
84
|
+
*/
|
|
85
|
+
export declare function hostNormalizePathPattern(pattern: string): string;
|
|
86
|
+
/**
|
|
87
|
+
* Tier-0 test seam (writing-tests skill): pure transcriptions with no external
|
|
88
|
+
* dependency beyond the DI'd realpath. Only {@link hostNormalizePathPattern} is
|
|
89
|
+
* consumed by production code, so these stay internal.
|
|
90
|
+
*/
|
|
91
|
+
export declare const _test_exports: {
|
|
92
|
+
hostWindowsPath: typeof hostWindowsPath;
|
|
93
|
+
hostNormalizePath: typeof hostNormalizePath;
|
|
94
|
+
};
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm worktree-lane context detection.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* OpenCode partitions **all** permission state by *directory*. Every service
|
|
7
|
+
* that matters — `Permission.state`, `Agent.state`, `Plugin.state`,
|
|
8
|
+
* `ToolRegistry.state` — is built through the same directory-keyed
|
|
9
|
+
* `InstanceState` cache. When opencode-swarm creates a worktree-lane session
|
|
10
|
+
* bound to a new directory (`session.create({ query: { directory: lanePath } })`),
|
|
11
|
+
* that lane gets a brand-new permission universe: an empty `approved` list, so
|
|
12
|
+
* every prior "Allow always" is forgotten, and a private pending map. Because a
|
|
13
|
+
* lane instance has no TUI attached, an `external_directory` prompt raised there
|
|
14
|
+
* can never be answered and the lane hangs forever (the host's `Permission.ask`
|
|
15
|
+
* awaits its deferred with no timeout).
|
|
16
|
+
*
|
|
17
|
+
* The fix is to pre-resolve permissions for lane instances via the plugin
|
|
18
|
+
* `config` hook. That requires answering one question cheaply and reliably:
|
|
19
|
+
* **is this directory a swarm worktree lane, and if so, what project is it a
|
|
20
|
+
* worktree of?**
|
|
21
|
+
*
|
|
22
|
+
* ## Why there is no `git` subprocess here
|
|
23
|
+
*
|
|
24
|
+
* The obvious implementation is
|
|
25
|
+
* `git -C <lane> rev-parse --path-format=absolute --git-common-dir`. This module
|
|
26
|
+
* deliberately does not do that. `resolveLaneContext` is called from the plugin
|
|
27
|
+
* `config` hook, which the host runs inside `Plugin.state` initialisation —
|
|
28
|
+
* squarely on the plugin-init path that AGENTS.md invariant 1 governs, and that
|
|
29
|
+
* invariant names Git commands explicitly as forbidden there.
|
|
30
|
+
*
|
|
31
|
+
* Instead this module reads the two files git itself would consult:
|
|
32
|
+
*
|
|
33
|
+
* - `<lane>/.git` — in a linked worktree this is a *file*, not a directory,
|
|
34
|
+
* containing `gitdir: <main>/.git/worktrees/<name>`.
|
|
35
|
+
* - `<main>/.git/worktrees/<name>/commondir` — a relative pointer back to the
|
|
36
|
+
* shared `.git` directory (normally `../..`).
|
|
37
|
+
*
|
|
38
|
+
* That is the same information the subprocess would return, obtained with at
|
|
39
|
+
* most two small synchronous reads, no child process, no timeout to get wrong,
|
|
40
|
+
* and nothing to kill in a `finally`. It is strictly safer on the init path than
|
|
41
|
+
* a spawn would be, and it removes an entire class of invariant-3 exposure.
|
|
42
|
+
*
|
|
43
|
+
* @module config/lane-context
|
|
44
|
+
*/
|
|
45
|
+
import { addDeferredWarning } from '../services/warning-buffer';
|
|
46
|
+
/**
|
|
47
|
+
* A resolved swarm worktree lane.
|
|
48
|
+
*/
|
|
49
|
+
export interface LaneContext {
|
|
50
|
+
/** Absolute, resolved path of the lane worktree itself. */
|
|
51
|
+
lanePath: string;
|
|
52
|
+
/**
|
|
53
|
+
* Absolute, resolved path of the project this lane is a git worktree of —
|
|
54
|
+
* i.e. the main working tree that owns the shared `.git` directory.
|
|
55
|
+
*/
|
|
56
|
+
parentProjectPath: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Test-only dependency-injection seam (AGENTS.md invariant 7 — prefer
|
|
60
|
+
* `_internals` over `mock.module`, which leaks across files in Bun's shared
|
|
61
|
+
* test-runner process). Tests replace these to simulate unreadable `.git`
|
|
62
|
+
* files, malformed pointers, and permission errors. Restore in `afterEach`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const _internals: {
|
|
65
|
+
readFileSync: (p: string, enc: BufferEncoding) => string;
|
|
66
|
+
statSync: (p: string) => {
|
|
67
|
+
isFile(): boolean;
|
|
68
|
+
};
|
|
69
|
+
addDeferredWarning: typeof addDeferredWarning;
|
|
70
|
+
/**
|
|
71
|
+
* Clears the detection cache. Test-only; production has no reason to call
|
|
72
|
+
* it because a directory's lane-ness cannot change while the process holds
|
|
73
|
+
* an OpenCode instance for that directory.
|
|
74
|
+
*/
|
|
75
|
+
clearCache: () => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Decides whether `directory` is a swarm worktree lane and, if so, resolves the
|
|
79
|
+
* lane path and the parent project it is a worktree of.
|
|
80
|
+
*
|
|
81
|
+
* Detection requires two independent conditions, the second of which may be
|
|
82
|
+
* satisfied either way:
|
|
83
|
+
*
|
|
84
|
+
* 1. REQUIRED — the directory (or a bounded number of its ancestors) is a real
|
|
85
|
+
* LINKED git worktree whose main working tree resolves. A main working tree
|
|
86
|
+
* has a `.git` directory rather than a file and is never a lane.
|
|
87
|
+
* 2. REQUIRED — the worktree is swarm-OWNED, established by EITHER
|
|
88
|
+
* (a) its branch matching the full grammar in `./swarm-branch.ts`
|
|
89
|
+
* (`swarm/<purpose>/<sessionId>/<id>` or `swarm-lane/<sessionId>/<id>`,
|
|
90
|
+
* with `<sessionId>` of the form `ses_…`) — the authoritative,
|
|
91
|
+
* path-independent signal; OR
|
|
92
|
+
* (b) the path sitting under a `.swarm-worktrees` base — the fallback for a
|
|
93
|
+
* worktree whose HEAD is detached or unreadable, so its branch cannot
|
|
94
|
+
* be consulted.
|
|
95
|
+
*
|
|
96
|
+
* (2a) is a full-grammar match rather than a `swarm/` prefix test on purpose: a
|
|
97
|
+
* user-authored `swarm/my-own-experiment` worktree must NOT be captured. A
|
|
98
|
+
* false positive is worse than a false negative here — see the module note in
|
|
99
|
+
* `./swarm-branch.ts`.
|
|
100
|
+
*
|
|
101
|
+
* A detached-HEAD swarm lane created OUTSIDE `.swarm-worktrees` (a
|
|
102
|
+
* `worktree_dir` override, or the Windows path-budget fallback) satisfies
|
|
103
|
+
* neither branch of (2) and is a false NEGATIVE: no permission changes, i.e.
|
|
104
|
+
* today's behaviour. That is the safe direction and is left as-is.
|
|
105
|
+
*
|
|
106
|
+
* NEVER throws. Any error — nonexistent directory, unreadable `.git`, malformed
|
|
107
|
+
* pointer, permission error, non-string input — yields `null` ("not a lane"),
|
|
108
|
+
* which preserves today's behaviour for ordinary sessions. That direction is
|
|
109
|
+
* the safe one: a false negative means "no permission changes at all", while a
|
|
110
|
+
* false positive would apply a deny-by-default ruleset to a normal project.
|
|
111
|
+
*
|
|
112
|
+
* @param directory - Directory to classify (typically the plugin's own
|
|
113
|
+
* `ctx.directory`, which under this host IS the instance
|
|
114
|
+
* directory).
|
|
115
|
+
* @returns The resolved lane context, or `null` when `directory` is not a lane.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveLaneContext(directory: string): LaneContext | null;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoped `external_directory` permission rules for swarm worktree-lane
|
|
3
|
+
* instances.
|
|
4
|
+
*
|
|
5
|
+
* ## The problem this solves
|
|
6
|
+
*
|
|
7
|
+
* A lane session is created against a new directory, so OpenCode builds it a
|
|
8
|
+
* fresh directory-keyed permission universe with an empty `approved` list. The
|
|
9
|
+
* host's default agent ruleset ends in `external_directory: { "*": "ask" }`, and
|
|
10
|
+
* a lane instance has no TUI attached to answer an ask — `Permission.ask` parks
|
|
11
|
+
* on a deferred with no timeout and the lane hangs forever.
|
|
12
|
+
*
|
|
13
|
+
* ## The mechanism
|
|
14
|
+
*
|
|
15
|
+
* The plugin `config` hook runs once per OpenCode instance (`Plugin.state` is
|
|
16
|
+
* built through the same directory-keyed `InstanceState` cache as
|
|
17
|
+
* `Permission.state`), so in a lane instance it runs with `ctx.directory` set to
|
|
18
|
+
* the lane path. Rules merged into the top-level `permission` block there are
|
|
19
|
+
* folded by the host into **every** agent's ruleset:
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
* // opencode host, Agent.state
|
|
23
|
+
* l = a.fromConfig(p.permission ?? {}) // top-level config permission
|
|
24
|
+
* r = { build: { permission: a.merge(c, ..., l) }, plan: {...}, general: {...},
|
|
25
|
+
* explore: {...}, compaction: {...}, title: {...}, summary: {...} }
|
|
26
|
+
* for (const [n, s] of Object.entries(p.agent ?? {})) {
|
|
27
|
+
* let e = r[n];
|
|
28
|
+
* if (!e) e = r[n] = { ..., permission: a.merge(c, l), ... }; // swarm agents
|
|
29
|
+
* e.permission = a.merge(e.permission, a.fromConfig(s.permission ?? {}));
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Top-level is therefore strictly broader than per-agent injection: it reaches
|
|
34
|
+
* the host-native agents (`build`, `plan`, `general`, `explore`, ...) that a
|
|
35
|
+
* lane can also run, and it lands *before* any per-agent block so explicit
|
|
36
|
+
* per-agent user config still wins. Injecting the same rules per-agent as well
|
|
37
|
+
* would duplicate every rule in every ruleset and would make our injected rules
|
|
38
|
+
* outrank the user's own top-level entries — the wrong precedence.
|
|
39
|
+
*
|
|
40
|
+
* ## DISCLOSED ASSUMPTION: hook-before-agents ordering
|
|
41
|
+
*
|
|
42
|
+
* This design requires the `config` hook to run before `Agent.state` reads the
|
|
43
|
+
* config. What is PROVEN: both sides touch the *same* object — `Plugin.state`
|
|
44
|
+
* does `U = yield* Config.get()` and mutates `U` in place, `Agent.state` does
|
|
45
|
+
* `p = yield* Config.get()`, and `Config.state` is InstanceState-cached, so
|
|
46
|
+
* `get()` returns one shared instance. There is no copy; only ordering is in
|
|
47
|
+
* question.
|
|
48
|
+
*
|
|
49
|
+
* What is NOT proven: any static ordering guarantee. Two bounded searches of
|
|
50
|
+
* the host binary came back negative — the Agent layer's deps
|
|
51
|
+
* (`deps:[J.node,O.node,P.node,_.node,$.node,Ve]`, offset 100817167) do not
|
|
52
|
+
* include the Plugin node (the three symbols the Agent chunk imports from the
|
|
53
|
+
* Plugin chunk are `$G`, `b8`, `r1`, none of which is the Plugin service), and
|
|
54
|
+
* `Plugin.init` (offset 102128681) has no caller anywhere in the bundle. The
|
|
55
|
+
* hook therefore fires lazily, when `Plugin.state` is first populated by a
|
|
56
|
+
* `Plugin.trigger` / `Plugin.list`.
|
|
57
|
+
*
|
|
58
|
+
* The evidence that it holds is empirical and strong: opencode-swarm's agents
|
|
59
|
+
* are registered ONLY by this config hook, and they demonstrably appear in the
|
|
60
|
+
* TUI, which is impossible unless the hook ran before `Agent.state` was built.
|
|
61
|
+
* Treat it as a verified-in-practice property of this host build rather than a
|
|
62
|
+
* contract. If a future OpenCode release reorders layer construction, the
|
|
63
|
+
* symptom is loud and immediate — no swarm agents at all — not a silent
|
|
64
|
+
* permission regression.
|
|
65
|
+
*
|
|
66
|
+
* ## Rule ordering is load-bearing
|
|
67
|
+
*
|
|
68
|
+
* The host evaluates with `findLast` over the flattened rule list:
|
|
69
|
+
*
|
|
70
|
+
* ```js
|
|
71
|
+
* function c(j, J, ...K) {
|
|
72
|
+
* return K.flat().findLast((z) => g.match(j, z.permission) && g.match(J, z.pattern))
|
|
73
|
+
* ?? { action: "ask", permission: j, pattern: "*" };
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* and `fromConfig` preserves `Object.entries` order. So **later wins**, and the
|
|
78
|
+
* catch-all `"*": "deny"` must be emitted FIRST, before the specific allows.
|
|
79
|
+
* Emitting it last would deny everything. `tests/unit/config/lane-permissions.test.ts`
|
|
80
|
+
* pins this against a faithful re-implementation of the host's own
|
|
81
|
+
* `fromConfig`/`merge`/`evaluate`, so the ordering contract is verified rather
|
|
82
|
+
* than assumed.
|
|
83
|
+
*
|
|
84
|
+
* Two further host behaviours matter and are deliberately relied upon:
|
|
85
|
+
*
|
|
86
|
+
* - `Wildcard.match` compiles the rule pattern with `*` -> `.*` under the `s`
|
|
87
|
+
* (dotAll) flag, so `<dir>/*` covers the entire subtree, and it normalises
|
|
88
|
+
* `\` to `/` on both sides before matching, so native Windows paths are fine.
|
|
89
|
+
* - After merging config, the host appends
|
|
90
|
+
* `external_directory: { <Global.Path.data>/tool-output/*: "allow" }` to every
|
|
91
|
+
* agent unless a rule already matches that exact pattern with `action: "deny"`.
|
|
92
|
+
* Our catch-all uses pattern `"*"`, which does not satisfy that exact-string
|
|
93
|
+
* check, so the append still happens and still lands last. Tool output stays
|
|
94
|
+
* readable. The emitted ruleset is intentionally not the final ruleset.
|
|
95
|
+
*
|
|
96
|
+
* @module config/lane-permissions
|
|
97
|
+
*/
|
|
98
|
+
import { addDeferredWarning } from '../services/warning-buffer';
|
|
99
|
+
import type { LaneContext } from './lane-context';
|
|
100
|
+
import { resolveLaneContext } from './lane-context';
|
|
101
|
+
/**
|
|
102
|
+
* Lane permission policy. Mirrors `worktree.lane_permissions` in
|
|
103
|
+
* `src/config/schema.ts`.
|
|
104
|
+
*/
|
|
105
|
+
export type LanePermissionMode = 'scoped_allow' | 'deny' | 'off';
|
|
106
|
+
/** Permission actions OpenCode understands for a rule. */
|
|
107
|
+
type PermissionAction = 'allow' | 'ask' | 'deny';
|
|
108
|
+
/** Result of assembling a lane's `external_directory` rule map. */
|
|
109
|
+
interface LaneRuleBuild {
|
|
110
|
+
/** The rule map, in emission (== evaluation) order. */
|
|
111
|
+
rules: Record<string, PermissionAction>;
|
|
112
|
+
/**
|
|
113
|
+
* User-configured patterns whose `"ask"` was coerced to `"deny"` because a
|
|
114
|
+
* lane has no TUI that could answer an ask. Empty in the common case.
|
|
115
|
+
*/
|
|
116
|
+
coercedAskPatterns: string[];
|
|
117
|
+
}
|
|
118
|
+
/** An allowlisted directory plus the justification for granting it. */
|
|
119
|
+
interface LaneAllowlistEntry {
|
|
120
|
+
/** Absolute directory path. */
|
|
121
|
+
dir: string;
|
|
122
|
+
/**
|
|
123
|
+
* The emitted `external_directory` rule pattern for {@link dir}.
|
|
124
|
+
*
|
|
125
|
+
* Computed ONCE here and reused by both consumers — the rule map and the
|
|
126
|
+
* `.swarm/events.jsonl` record. Deriving it twice meant a second round of
|
|
127
|
+
* `realpathSync.native` per entry on the config-hook path and, more
|
|
128
|
+
* importantly, left room for the two to disagree: if they ever did, the
|
|
129
|
+
* event log would misreport the rule that was actually emitted, which is
|
|
130
|
+
* precisely the observability this subsystem exists to provide.
|
|
131
|
+
*/
|
|
132
|
+
pattern: string;
|
|
133
|
+
/** Why this directory is justified — recorded in `.swarm/events.jsonl`. */
|
|
134
|
+
reason: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Converts an absolute directory into the `external_directory` rule pattern
|
|
138
|
+
* that covers it and everything beneath it.
|
|
139
|
+
*
|
|
140
|
+
* The literal part of the directory is escaped for the host's matcher. The host
|
|
141
|
+
* compiles a rule pattern as:
|
|
142
|
+
*
|
|
143
|
+
* ```js
|
|
144
|
+
* o.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")
|
|
145
|
+
* // then: new RegExp("^"+compiled+"$","si")
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* Note what is NOT escaped: `*` and `?`. A directory legitimately named `a*`
|
|
149
|
+
* (legal on POSIX) would compile to `a.*` and silently grant every sibling
|
|
150
|
+
* starting with `a`; `?` becomes a single-character wildcard the same way. The
|
|
151
|
+
* matcher offers no escape syntax for either, so such a directory cannot be
|
|
152
|
+
* expressed exactly. {@link isExpressibleDirectory} therefore drops it from the
|
|
153
|
+
* allowlist rather than emitting an over-broad grant: dropping fails safe (the
|
|
154
|
+
* path is denied, and its absence is visible in the event log), emitting would
|
|
155
|
+
* fail open.
|
|
156
|
+
*
|
|
157
|
+
* The matcher also uses the `i` flag, so patterns are case-insensitive even on
|
|
158
|
+
* case-sensitive filesystems. `/home/u/Work/*` will therefore also match
|
|
159
|
+
* `/home/u/work/...`. That widening is inherent to the host and cannot be
|
|
160
|
+
* avoided from a rule pattern; it is recorded here so it is a known, reviewed
|
|
161
|
+
* property rather than a surprise.
|
|
162
|
+
*/
|
|
163
|
+
declare function laneDirectoryPattern(dir: string): string;
|
|
164
|
+
/**
|
|
165
|
+
* Builds the justified `external_directory` allowlist for a lane.
|
|
166
|
+
*
|
|
167
|
+
* Every entry is a directory the lane provably needs in order to do the work it
|
|
168
|
+
* was created for. Nothing is added "just in case" — an over-broad allowlist
|
|
169
|
+
* here is the security cost of this fix, so each entry carries its reason and
|
|
170
|
+
* is asserted in tests.
|
|
171
|
+
*
|
|
172
|
+
* @param lane - Resolved lane context.
|
|
173
|
+
* @returns Deduplicated absolute directories with justifications.
|
|
174
|
+
*/
|
|
175
|
+
declare function buildLaneAllowlist(lane: LaneContext, configuredSkillPaths?: readonly string[], configuredSkillUrls?: readonly string[]): LaneAllowlistEntry[];
|
|
176
|
+
/**
|
|
177
|
+
* Builds the `external_directory` rule map to write into the top-level
|
|
178
|
+
* `permission` block of a lane instance's config.
|
|
179
|
+
*
|
|
180
|
+
* Emission order (which IS the evaluation order, later-wins):
|
|
181
|
+
*
|
|
182
|
+
* 1. `"*": "deny"` — our catch-all, so nothing can be left pending in a lane
|
|
183
|
+
* that has no TUI to answer it. This is the only mechanism that delivers
|
|
184
|
+
* the approved policy's "answered, not hung" guarantee now that the host's
|
|
185
|
+
* `permission.ask` plugin hook is confirmed dead (never triggered by
|
|
186
|
+
* opencode 1.18.10).
|
|
187
|
+
* 2. Our justified allowlist — beats the catch-all because it comes later.
|
|
188
|
+
* 3. Whatever the user already configured — beats everything, because explicit
|
|
189
|
+
* user configuration must always outrank a plugin default. A user entry
|
|
190
|
+
* keyed `"*"` replaces our catch-all's value in place (same key), so
|
|
191
|
+
* `external_directory: "allow"` in `opencode.json` still means allow-all.
|
|
192
|
+
*
|
|
193
|
+
* ## Why `"ask"` is coerced to `"deny"` INSIDE A LANE
|
|
194
|
+
*
|
|
195
|
+
* A user's `allow` and `deny` are honoured verbatim. `ask` is not, and that is
|
|
196
|
+
* deliberate: a lane instance has no TUI attached, so `ask` cannot be answered
|
|
197
|
+
* by anyone. It is not a third policy choice there — it is a guaranteed
|
|
198
|
+
* indefinite hang, which is the exact defect this module exists to remove.
|
|
199
|
+
* Honouring it literally would let a top-level `external_directory: "ask"` (or
|
|
200
|
+
* any `"ask"` pattern) silently reinstate the bug while this code reported
|
|
201
|
+
* success. Coercion resolves to the fail-safe direction (`deny`, never
|
|
202
|
+
* `allow`), the affected patterns are named in the advisory, and they are
|
|
203
|
+
* recorded under `coercedAskPatterns` in the `.swarm/events.jsonl` entry so the
|
|
204
|
+
* decision is auditable rather than silent. Users who genuinely want the
|
|
205
|
+
* prompting behaviour back have `worktree.lane_permissions: "off"`.
|
|
206
|
+
*
|
|
207
|
+
* @param mode - Configured lane permission policy.
|
|
208
|
+
* @param lane - Resolved lane context.
|
|
209
|
+
* @param existing - Current `external_directory` value from the config being
|
|
210
|
+
* mutated, if any.
|
|
211
|
+
* @returns The rule map plus any patterns whose `ask` was coerced, or `null`
|
|
212
|
+
* when the policy is `off` (the caller must then touch nothing).
|
|
213
|
+
*/
|
|
214
|
+
declare function buildLaneExternalDirectoryRules(mode: LanePermissionMode, lane: LaneContext, existing?: unknown, precomputedAllowlist?: readonly LaneAllowlistEntry[]): LaneRuleBuild | null;
|
|
215
|
+
/**
|
|
216
|
+
* Renders the operator-facing remedy text for a lane whose permissions are
|
|
217
|
+
* being scoped.
|
|
218
|
+
*
|
|
219
|
+
* The permission system itself carries an action, not a message — a `deny` rule
|
|
220
|
+
* cannot explain itself. So the explanation is delivered out-of-band (a deferred
|
|
221
|
+
* warning plus a `.swarm/events.jsonl` record) and names the exact
|
|
222
|
+
* `opencode.json` edit that widens the allowlist.
|
|
223
|
+
*/
|
|
224
|
+
declare function renderLanePermissionAdvisory(mode: Exclude<LanePermissionMode, 'off'>, lane: LaneContext, allowlist: readonly LaneAllowlistEntry[], coercedAskPatterns?: readonly string[]): string;
|
|
225
|
+
/**
|
|
226
|
+
* Tier-0 test seam (see the writing-tests skill): these are pure functions with
|
|
227
|
+
* no external dependencies, so they are tested directly rather than mocked.
|
|
228
|
+
* They are intentionally NOT part of the module's public API — only
|
|
229
|
+
* {@link applyLanePermissions} is called from production.
|
|
230
|
+
*/
|
|
231
|
+
export declare const _test_exports: {
|
|
232
|
+
buildLaneAllowlist: typeof buildLaneAllowlist;
|
|
233
|
+
buildLaneExternalDirectoryRules: typeof buildLaneExternalDirectoryRules;
|
|
234
|
+
laneDirectoryPattern: typeof laneDirectoryPattern;
|
|
235
|
+
renderLanePermissionAdvisory: typeof renderLanePermissionAdvisory;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Test-only dependency-injection seam (AGENTS.md invariant 7). Tests replace
|
|
239
|
+
* these to assert the event record and advisory without touching the real
|
|
240
|
+
* filesystem or the process-wide warning buffer. Restore in `afterEach`.
|
|
241
|
+
*/
|
|
242
|
+
export declare const _internals: {
|
|
243
|
+
resolveLaneContext: typeof resolveLaneContext;
|
|
244
|
+
addDeferredWarning: typeof addDeferredWarning;
|
|
245
|
+
appendFileSync: (p: string, data: string, enc: BufferEncoding) => void;
|
|
246
|
+
mkdirSync: (p: string, opts: {
|
|
247
|
+
recursive: boolean;
|
|
248
|
+
}) => string | undefined;
|
|
249
|
+
};
|
|
250
|
+
/** Outcome of {@link applyLanePermissions}, returned for tests and logging. */
|
|
251
|
+
export interface LanePermissionApplication {
|
|
252
|
+
/** `true` when the directory was recognised as a swarm worktree lane. */
|
|
253
|
+
lane: boolean;
|
|
254
|
+
/** Effective policy. `undefined` when `lane` is `false`. */
|
|
255
|
+
mode?: LanePermissionMode;
|
|
256
|
+
/** Rules written into `permission.external_directory`, when any. */
|
|
257
|
+
rules?: Record<string, PermissionAction>;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Applies scoped lane permissions to an OpenCode config object, in place.
|
|
261
|
+
*
|
|
262
|
+
* Called from the plugin `config` hook. When `directory` is not a swarm
|
|
263
|
+
* worktree lane this returns immediately and mutates NOTHING — ordinary
|
|
264
|
+
* sessions must be byte-for-byte unaffected, which
|
|
265
|
+
* `tests/unit/config/lane-permissions-config.test.ts` asserts directly.
|
|
266
|
+
*
|
|
267
|
+
* @param opencodeConfig - The live merged config object the host handed to the
|
|
268
|
+
* `config` hook. Mutated in place, as the hook contract
|
|
269
|
+
* requires.
|
|
270
|
+
* @param directory - The plugin's own `ctx.directory`. Under this host that
|
|
271
|
+
* IS the instance directory, so in a lane instance it is
|
|
272
|
+
* the lane path.
|
|
273
|
+
* @param mode - Configured `worktree.lane_permissions` policy.
|
|
274
|
+
* @returns What was decided, for logging and tests.
|
|
275
|
+
*/
|
|
276
|
+
export declare function applyLanePermissions(opencodeConfig: Record<string, unknown>, directory: string, mode: LanePermissionMode): LanePermissionApplication;
|
|
277
|
+
export {};
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1197,6 +1197,11 @@ export declare const WorktreeIsolationConfigSchema: z.ZodObject<{
|
|
|
1197
1197
|
skip: "skip";
|
|
1198
1198
|
copy: "copy";
|
|
1199
1199
|
}>>;
|
|
1200
|
+
lane_permissions: z.ZodDefault<z.ZodEnum<{
|
|
1201
|
+
scoped_allow: "scoped_allow";
|
|
1202
|
+
deny: "deny";
|
|
1203
|
+
off: "off";
|
|
1204
|
+
}>>;
|
|
1200
1205
|
serialization_release_after_dispatches: z.ZodDefault<z.ZodNumber>;
|
|
1201
1206
|
serialization_release_after_ms: z.ZodDefault<z.ZodNumber>;
|
|
1202
1207
|
runtime_isolation: z.ZodOptional<z.ZodObject<{
|
|
@@ -2449,6 +2454,11 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
2449
2454
|
skip: "skip";
|
|
2450
2455
|
copy: "copy";
|
|
2451
2456
|
}>>;
|
|
2457
|
+
lane_permissions: z.ZodDefault<z.ZodEnum<{
|
|
2458
|
+
scoped_allow: "scoped_allow";
|
|
2459
|
+
deny: "deny";
|
|
2460
|
+
off: "off";
|
|
2461
|
+
}>>;
|
|
2452
2462
|
serialization_release_after_dispatches: z.ZodDefault<z.ZodNumber>;
|
|
2453
2463
|
serialization_release_after_ms: z.ZodDefault<z.ZodNumber>;
|
|
2454
2464
|
runtime_isolation: z.ZodOptional<z.ZodObject<{
|