pantheon-opencode 1.6.0-beta.3 → 1.6.0-beta.5
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/AGENTS.md +24 -5
- package/CHANGELOG.md +49 -0
- package/README.md +51 -19
- package/README.pt-BR.md +32 -13
- package/bin/pantheon-init.mjs +26 -3
- package/package.json +12 -11
- package/plugin.json +1 -1
- package/pyproject.toml +6 -6
- package/scripts/{code_mode_server.py → code_mode.py} +3 -3
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/agents-md.mjs +2 -2
- package/scripts/install/config-migration.mjs +80 -2
- package/scripts/install/health-check.mjs +21 -19
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +250 -119
- package/scripts/install/shared.mjs +5 -5
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/scripts/probe-context-rehydrate.mjs +8 -2
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/sync-artifacts.mjs +5 -5
- package/scripts/test-opencode-v2-sandbox.sh +21 -21
- package/scripts/validate-routing.mjs +3 -3
- package/skills-lock.json +1 -1
- package/src/agents/apollo.md +7 -2
- package/src/agents/zeus.md +29 -1
- package/src/instructions/memory-protocol.instructions.md +15 -4
- package/src/instructions/zeus-anti-stall.instructions.md +9 -1
- package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +2 -2
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +1 -1
- package/src/mcp/requirements-mcp.txt +6 -4
- package/src/mcp/requirements-vision.txt +2 -2
- package/src/pantheon/cost-command.ts +399 -101
- package/src/pantheon/presets.mjs +3 -3
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +113 -0
- package/src/plugin-v2.ts +269 -100
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +74 -74
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +17 -20
- package/src/plugins/tui/package-lock.json +364 -317
- package/src/plugins/tui/package.json +8 -5
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +3 -0
- package/src/skills/context-compression/SKILL.md +1 -1
- package/opencode.json +0 -58
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for v2-unsupported.mjs (the V2 feature-reduction seed).
|
|
3
|
+
* Consumed by src/plugin-v2.ts via `import ... from './pantheon/v2-unsupported.mjs'`,
|
|
4
|
+
* and by the plain-`.mjs` install/health scripts at runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Statically-known V2 feature reductions, in report order. */
|
|
8
|
+
export declare const V2_UNSUPPORTED_FEATURE_SEED: readonly string[]
|
|
9
|
+
|
|
10
|
+
/** A fresh, mutable copy of the seed, so a reporter cannot mutate the shared list. */
|
|
11
|
+
export declare function listV2UnsupportedFeatures(): string[]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* v2-unsupported.mjs — the SEED of V2 feature reductions, in plain ESM.
|
|
4
|
+
*
|
|
5
|
+
* WHY A SEPARATE PLAIN-`.mjs` MODULE: these strings must be readable by three
|
|
6
|
+
* very different consumers, and only one of them can import TypeScript.
|
|
7
|
+
*
|
|
8
|
+
* - `src/plugin-v2.ts` (TypeScript) — owns the LIVE list, appending to it at
|
|
9
|
+
* runtime via `markUnsupported()` when a host API turns out to be missing,
|
|
10
|
+
* and exposes it through `getUnsupportedFeatures()`.
|
|
11
|
+
* - `scripts/install/opencode.mjs` — reports the reduction at install time
|
|
12
|
+
* for a V2-generation install.
|
|
13
|
+
* - `scripts/doctor.mjs` — reports it in the health gate.
|
|
14
|
+
*
|
|
15
|
+
* The scripts are plain Node ESM and must stay that way, and `.mjs` is a
|
|
16
|
+
* REPO-WIDE INVARIANT rather than a judgement about Node: `tsx` is a
|
|
17
|
+
* devDependency only, and no plain-Node script in `scripts/` or `bin/` imports
|
|
18
|
+
* a `.ts` module — every `.ts` path there is a string to copy, stat or read as
|
|
19
|
+
* text (`scripts/install/opencode.mjs`, `scripts/probe-pantheon-cost.mjs`,
|
|
20
|
+
* `scripts/package-evidence.mjs`, `scripts/check-plugin-v2-coverage.mjs`).
|
|
21
|
+
* So a `.mjs` import is the only form all three consumers can share, and it
|
|
22
|
+
* matches the existing `src/pantheon/presets.mjs` + `.d.mts` precedent.
|
|
23
|
+
*
|
|
24
|
+
* NOT the reason: type-stripping. `package.json#engines` admits
|
|
25
|
+
* `^22.22.2 || ^24.15.0 || >=26.0.0`, every admitted version already defaults to
|
|
26
|
+
* unflagged type-stripping (it landed in 22.18.0), so that argument would not
|
|
27
|
+
* have held on any admitted version. The live hazard is that `engines` is not
|
|
28
|
+
* ENFORCED: a Node 20 user can run `doctor` and would get no type-stripping,
|
|
29
|
+
* which is an argument for a runtime version check, not for this file's shape.
|
|
30
|
+
*
|
|
31
|
+
* The SEED is the statically-known half of `getUnsupportedFeatures()`: what a
|
|
32
|
+
* migrating user loses by choosing the V2 contract, knowable with no host at
|
|
33
|
+
* all. Runtime-appended entries (`tool-transform`, `event-stream`, …) are
|
|
34
|
+
* observations about one live host and are deliberately NOT seeded here — they
|
|
35
|
+
* are not known until the plugin runs against a real V2 host.
|
|
36
|
+
*
|
|
37
|
+
* ONE SOURCE OF TRUTH: install and doctor both read the seed from here, so the
|
|
38
|
+
* two cannot drift apart, and what they report IS what this file lists. Note the
|
|
39
|
+
* scope of that claim: it holds between the SEED and the two reporters. The
|
|
40
|
+
* LIVE list `getUnsupportedFeatures()` returns is this seed PLUS markers appended
|
|
41
|
+
* at runtime (`tool-transform`, `event-stream`, …), so it is a superset and the
|
|
42
|
+
* two are expected to differ there. Do not copy these strings into a script;
|
|
43
|
+
* import them.
|
|
44
|
+
*
|
|
45
|
+
* @module v2-unsupported
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Statically-known V2 feature reductions, in the order a reader should meet
|
|
50
|
+
* them. Frozen: `plugin-v2.ts` copies it into its own mutable live array, and
|
|
51
|
+
* every other consumer goes through {@link listV2UnsupportedFeatures}.
|
|
52
|
+
*
|
|
53
|
+
* @type {readonly string[]}
|
|
54
|
+
*/
|
|
55
|
+
export const V2_UNSUPPORTED_FEATURE_SEED = Object.freeze([
|
|
56
|
+
'legacy-hooks',
|
|
57
|
+
// `ctx.catalog` is the one domain a live 2.0.22 host does NOT have: it is
|
|
58
|
+
// absent from `Object.keys(ctx)`, measured in
|
|
59
|
+
// tests/canary/plugin-v2-tool-canary.test.mjs. (`catalog` IS in the 1.18.33
|
|
60
|
+
// SDK's PluginContext, so the SDK types it and the host does not provide it.)
|
|
61
|
+
'catalog-transform',
|
|
62
|
+
// Adapter support, NOT host availability: 2.0.22 exposes ctx.integration and
|
|
63
|
+
// ctx.skill with callable transforms, and Pantheon deliberately registers
|
|
64
|
+
// neither. These entries record what this plugin does not implement.
|
|
65
|
+
'integration-transform',
|
|
66
|
+
// The inspected SkillEditor shape has no `source()` helper — i.e. no
|
|
67
|
+
// `SkillEditor.source()` — for adding a directory source. This is narrower
|
|
68
|
+
// than (and distinct from) ctx.skill's host availability or callable
|
|
69
|
+
// transform, which a live 2.0.22 host does provide.
|
|
70
|
+
'skill-transform',
|
|
71
|
+
// Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
|
|
72
|
+
// GoalLoopClient and a BackgroundJobBoard, none of which the V2
|
|
73
|
+
// PluginContext exposes, and the V1 bridge resolves to null outside V1.
|
|
74
|
+
// pantheon_goal_create/get/update are therefore absent from the V2 surface
|
|
75
|
+
// rather than registered as non-functional placeholders.
|
|
76
|
+
'goal-tools',
|
|
77
|
+
// Adapter limitation, not a host gap — TWO separate reasons, which are easy
|
|
78
|
+
// to conflate and were once stated wrongly (see the v2EnforcementGuard
|
|
79
|
+
// comment in src/plugin-v2.ts for the long form):
|
|
80
|
+
// - MISSING WIRING: V2 keeps no session→agent map (the host puts the agent
|
|
81
|
+
// on the execute.before event), so there is no `getSessionAgent` to pass.
|
|
82
|
+
// The guard would then call isDelegationAllowed(undefined, target) → false
|
|
83
|
+
// and throw "caller agent is unavailable" on EVERY task() call.
|
|
84
|
+
// - UNWIRED HIERARCHY: V2 seeds no session→parent map anywhere, and an
|
|
85
|
+
// unseeded SessionHierarchyRegistry gates exactly two things — the depth-2
|
|
86
|
+
// child deny and the root-session gate. Unseeded, isChild is always false,
|
|
87
|
+
// so neither can be trusted. UNWIRED, not impossible: the V2
|
|
88
|
+
// `session.created` event carries `properties.info: Session` with
|
|
89
|
+
// `parentID?`, the same field V1 seeds from on the same event, and one
|
|
90
|
+
// line in onSessionCreated would seed live sessions. V1's OTHER source —
|
|
91
|
+
// the client.session.list() startup seed for sessions predating plugin
|
|
92
|
+
// load — has no V2 equivalent at all, because V2's PluginContext exposes
|
|
93
|
+
// no `client`. Whether a live 2.0.22 host populates info.parentID on that
|
|
94
|
+
// event is UNVERIFIED: the canary records Object.keys(ctx), not payloads.
|
|
95
|
+
// Note the hierarchy is NOT what would deny: isRoot reports `true` for an
|
|
96
|
+
// unknown session while unseeded, which PASSES the root gate.
|
|
97
|
+
// Read-only depth-2 still holds via the blocked-tool list in the guard, so
|
|
98
|
+
// this marker records an unenforced matrix, not an unenforced depth limit.
|
|
99
|
+
'delegation-matrix',
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A fresh, mutable copy of the seed.
|
|
104
|
+
*
|
|
105
|
+
* Callers get a copy so a reporter that sorts, filters or appends cannot reach
|
|
106
|
+
* back into the shared list — this is what makes "both surfaces report the same
|
|
107
|
+
* strings" a structural property rather than a convention.
|
|
108
|
+
*
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
export function listV2UnsupportedFeatures() {
|
|
112
|
+
return [...V2_UNSUPPORTED_FEATURE_SEED]
|
|
113
|
+
}
|
package/src/plugin-v2.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - 3 self-sufficient tools via V2 tool.transform (hashline_edit,
|
|
6
6
|
* pantheon_cost, pantheon_model — see ./pantheon/v2-tools.ts; the goal tools
|
|
7
7
|
* need V1 infrastructure and are absent from this surface)
|
|
8
|
-
* -
|
|
8
|
+
* - 5 event subscriptions (session.created, idle, deleted, error, compacted)
|
|
9
9
|
* - Session hooks (prompt, context, compaction)
|
|
10
10
|
* - A tool `execute.before` hook that ENFORCES read-only sessions: the agent
|
|
11
11
|
* arrives on the host's event, a read-only agent (apollo/gaia) registers its
|
|
@@ -26,21 +26,44 @@
|
|
|
26
26
|
* `execute` MUST resolve to a value carrying `output`. The host enforces the
|
|
27
27
|
* two as a pair — see `V2ToolDraft` for the exact contract.
|
|
28
28
|
*
|
|
29
|
-
* The
|
|
30
|
-
* ctx.agent / ctx.command / ctx.model / ctx.reference / ctx.skill /
|
|
31
|
-
* ctx.tool / ctx.event / ctx.permission / ctx.session
|
|
29
|
+
* ## The four domains the SDK types do not carry
|
|
32
30
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
31
|
+
* `@opencode-ai/plugin` 1.18.33 types `PluginContext` as exactly:
|
|
32
|
+
* options · agent · aisdk · catalog · command · integration · plugin ·
|
|
33
|
+
* reference · skill
|
|
34
|
+
* — no `tool`, `event`, `permission` or `session`. 1.18.34 (`latest`) is the
|
|
35
|
+
* same, so following the stable SDK line does not resolve this: it is a
|
|
36
|
+
* STRUCTURAL DECLARATION task, not a pin reconciliation.
|
|
37
|
+
*
|
|
38
|
+
* A live opencode 2.0.22 host DOES have all four, with the shapes this module
|
|
39
|
+
* uses. Measured by `tests/canary/plugin-v2-tool-canary.test.mjs`, which loads
|
|
40
|
+
* this file through the real loader and records `Object.keys(ctx)` from inside
|
|
41
|
+
* the host:
|
|
42
|
+
*
|
|
43
|
+
* agent aisdk app command event experimental generate integration location
|
|
44
|
+
* mcp model options permission plugin provider reference rpc session shell
|
|
45
|
+
* skill storage tool vcs websearch worktree
|
|
46
|
+
*
|
|
47
|
+
* ctx.tool.transform function
|
|
48
|
+
* ctx.tool.hook function
|
|
49
|
+
* ctx.tool.list function
|
|
50
|
+
* ctx.event.subscribe function
|
|
51
|
+
* ctx.permission.hook function
|
|
52
|
+
* ctx.session.hook function
|
|
53
|
+
* ctx.catalog absent
|
|
54
|
+
*
|
|
55
|
+
* `HostPluginContext` below declares exactly the minimum of that, and
|
|
56
|
+
* `hostDomains` is the SINGLE documented cast that reaches them — replacing the
|
|
57
|
+
* six repeated `as unknown as Record<string, unknown>` casts this file carried
|
|
58
|
+
* (Phases 2, 3, 4, 5, 6 and 7 — every phase except Phase 1, which reads the
|
|
59
|
+
* SDK-typed domains directly), each of which type-checked nothing at all. The
|
|
60
|
+
* per-domain definition checks stay: `registerV2Tools` still returns `false`
|
|
61
|
+
* and marks `tool-transform` when `ctx.tool.transform` is missing, and likewise
|
|
62
|
+
* for the others. The cast is not a licence to assume: the canary above is what
|
|
63
|
+
* proves the domains exist on a real host, and it fails if they stop existing.
|
|
64
|
+
*
|
|
65
|
+
* If the SDK ever exposes these four, replacing `hostDomains` with the real
|
|
66
|
+
* type is a one-line change. Do NOT make that change while 1.18.x lacks them.
|
|
44
67
|
*
|
|
45
68
|
* V2_UNSUPPORTED_FEATURES distinguishes adapter limitations from observed
|
|
46
69
|
* host-absent APIs; setup handles optional registrations on a best-effort basis.
|
|
@@ -71,6 +94,7 @@ import {
|
|
|
71
94
|
type V2ContextLike,
|
|
72
95
|
} from './pantheon/v2-bridge.ts'
|
|
73
96
|
import { createV2ToolDefinitions, type V2ToolResult } from './pantheon/v2-tools.ts'
|
|
97
|
+
import { V2_UNSUPPORTED_FEATURE_SEED } from './pantheon/v2-unsupported.mjs'
|
|
74
98
|
import { type HookPayload, runHook } from './plugins/hook-runner.ts'
|
|
75
99
|
|
|
76
100
|
// ─── Unsupported Features Registry ───────────────────────────────────────
|
|
@@ -80,33 +104,18 @@ import { type HookPayload, runHook } from './plugins/hook-runner.ts'
|
|
|
80
104
|
* the observed host. A listed feature is not necessarily a host-absent API.
|
|
81
105
|
* Additional features are appended during setup when a required API is missing.
|
|
82
106
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
|
|
96
|
-
// GoalLoopClient and a BackgroundJobBoard, none of which the V2
|
|
97
|
-
// PluginContext exposes, and the V1 bridge resolves to null outside V1.
|
|
98
|
-
// pantheon_goal_create/get/update are therefore absent from the V2 surface
|
|
99
|
-
// rather than registered as non-functional placeholders.
|
|
100
|
-
'goal-tools',
|
|
101
|
-
// Adapter limitation, not a host gap: the caller/target delegation matrix
|
|
102
|
-
// needs a session hierarchy seeded from session metadata, and V2 exposes no
|
|
103
|
-
// seed path for it — SessionHierarchyRegistry.isRoot reports `true` for
|
|
104
|
-
// unknown sessions while unseeded, which would deny every `task()` call. The
|
|
105
|
-
// branch is therefore skipped rather than left to deny indiscriminately.
|
|
106
|
-
// Read-only depth-2 still holds via the blocked-tool list in the guard below,
|
|
107
|
-
// so this marker records an unenforced matrix, not an unenforced depth limit.
|
|
108
|
-
'delegation-matrix',
|
|
109
|
-
]
|
|
107
|
+
/**
|
|
108
|
+
* Live unsupported-feature list: the shared seed plus anything `markUnsupported`
|
|
109
|
+
* appends while this process talks to a real host.
|
|
110
|
+
*
|
|
111
|
+
* The seed lives in `src/pantheon/v2-unsupported.mjs` — a plain-`.mjs` module —
|
|
112
|
+
* so that the V2 installer and `scripts/doctor.mjs` report the SAME strings
|
|
113
|
+
* without either of them having to import TypeScript. This array stays a
|
|
114
|
+
* mutable copy rather than the shared seed itself: `markUnsupported` appends to
|
|
115
|
+
* it, and the seed must stay frozen. `getUnsupportedFeatures()` still returns
|
|
116
|
+
* this exact reference.
|
|
117
|
+
*/
|
|
118
|
+
export const V2_UNSUPPORTED_FEATURES: string[] = [...V2_UNSUPPORTED_FEATURE_SEED]
|
|
110
119
|
|
|
111
120
|
// ─── Constants ───────────────────────────────────────────────────────────
|
|
112
121
|
|
|
@@ -189,6 +198,68 @@ function markUnsupported(feature: string): void {
|
|
|
189
198
|
}
|
|
190
199
|
}
|
|
191
200
|
|
|
201
|
+
// ─── Host context domains the SDK types do not carry ────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The minimal shape of `ctx.tool` this module uses, as a live opencode 2.0.22
|
|
205
|
+
* host provides it. `transform` registers the Pantheon tools, `hook` installs
|
|
206
|
+
* the `execute.before`/`execute.after` guards, `list` is the host-built
|
|
207
|
+
* descriptor dump the canary asserts on.
|
|
208
|
+
*/
|
|
209
|
+
interface V2ToolDomain {
|
|
210
|
+
transform?: (cb: (draft: V2ToolDraft) => void) => Promise<unknown>
|
|
211
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
212
|
+
list?: () => Promise<unknown>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The minimal shape of `ctx.event`: one async-iterable subscription. */
|
|
216
|
+
interface V2EventDomain {
|
|
217
|
+
subscribe?: (opts?: { signal?: AbortSignal }) => AsyncIterable<V2SessionEvent>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The minimal shape of `ctx.permission`.
|
|
222
|
+
*
|
|
223
|
+
* Only a hook name and a handler: the plugin registers `evaluate` as a
|
|
224
|
+
* registration point and deliberately runs no V2-side permission logic, so no
|
|
225
|
+
* richer payload type is claimed here.
|
|
226
|
+
*/
|
|
227
|
+
interface V2PermissionDomain {
|
|
228
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The minimal shape of `ctx.session`.
|
|
233
|
+
*
|
|
234
|
+
* Only `hook(name, handler)`. The host accepts any string and silently drops an
|
|
235
|
+
* unknown one — which is why the hook names below were a live defect once and
|
|
236
|
+
* why the host-backed hook canary exists.
|
|
237
|
+
*/
|
|
238
|
+
interface V2SessionDomain {
|
|
239
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `PluginContext` plus the four domains 1.18.x omits. See the module header. */
|
|
243
|
+
interface HostPluginContext extends PluginContext {
|
|
244
|
+
tool?: V2ToolDomain
|
|
245
|
+
event?: V2EventDomain
|
|
246
|
+
permission?: V2PermissionDomain
|
|
247
|
+
session?: V2SessionDomain
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* THE cast to the host's context. Every optional member stays optional and every
|
|
252
|
+
* caller re-checks before use, so this narrows nothing away: it replaces five
|
|
253
|
+
* anonymous `Record<string, unknown>` casts with one named, documented,
|
|
254
|
+
* type-checked boundary.
|
|
255
|
+
*
|
|
256
|
+
* When the SDK ships these four domains, delete this function and use
|
|
257
|
+
* `PluginContext` directly.
|
|
258
|
+
*/
|
|
259
|
+
function hostDomains(context: PluginContext): HostPluginContext {
|
|
260
|
+
return context as HostPluginContext
|
|
261
|
+
}
|
|
262
|
+
|
|
192
263
|
// ─── V1 Bridge Integration ──────────────────────────────────────────────
|
|
193
264
|
|
|
194
265
|
/**
|
|
@@ -334,9 +405,7 @@ function transformReferences(draft: ReferenceDraft): void {
|
|
|
334
405
|
* Returns true if tools were registered, false if the API is unavailable.
|
|
335
406
|
*/
|
|
336
407
|
async function registerV2Tools(context: PluginContext): Promise<boolean> {
|
|
337
|
-
const toolCtx = (context
|
|
338
|
-
| { transform?: (cb: (draft: V2ToolDraft) => void) => Promise<unknown> }
|
|
339
|
-
| undefined
|
|
408
|
+
const toolCtx = hostDomains(context).tool
|
|
340
409
|
|
|
341
410
|
if (!toolCtx?.transform) {
|
|
342
411
|
return false
|
|
@@ -387,9 +456,7 @@ async function registerV2Tools(context: PluginContext): Promise<boolean> {
|
|
|
387
456
|
* Returns a cleanup function, or undefined if the API is unavailable.
|
|
388
457
|
*/
|
|
389
458
|
function subscribeV2Events(context: PluginContext): (() => void) | undefined {
|
|
390
|
-
const eventCtx = (context
|
|
391
|
-
| { subscribe?: (opts?: { signal?: AbortSignal }) => AsyncIterable<V2SessionEvent> }
|
|
392
|
-
| undefined
|
|
459
|
+
const eventCtx = hostDomains(context).event
|
|
393
460
|
|
|
394
461
|
if (!eventCtx?.subscribe) {
|
|
395
462
|
return undefined
|
|
@@ -423,6 +490,9 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
|
|
|
423
490
|
case 'session.idle':
|
|
424
491
|
await onSessionIdle(event)
|
|
425
492
|
break
|
|
493
|
+
case 'session.deleted':
|
|
494
|
+
await onSessionDeleted(event)
|
|
495
|
+
break
|
|
426
496
|
case 'session.error':
|
|
427
497
|
await onSessionError(event)
|
|
428
498
|
break
|
|
@@ -437,18 +507,44 @@ async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
|
|
|
437
507
|
/**
|
|
438
508
|
* Session-created handler: currently a no-op on the V2-only surface.
|
|
439
509
|
*
|
|
440
|
-
* V1 seeds session state here
|
|
441
|
-
* root-session set
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
510
|
+
* V1 seeds session state here — `sessionHierarchy.register(info)` plus the
|
|
511
|
+
* root-session set — from the full session metadata carried by its own event.
|
|
512
|
+
*
|
|
513
|
+
* THE V2 EVENT CARRIES THE SAME METADATA, so this is an UNWIRED seed, not an
|
|
514
|
+
* impossible one. The V2 SDK types `session.created` as
|
|
515
|
+
* `{ sessionID: string; info: Session }`, and `Session` declares
|
|
516
|
+
* `parentID?: string` (`@opencode-ai/sdk` v2 `gen/types.gen.d.ts`) — a superset
|
|
517
|
+
* of the two fields V1 reads on this exact event. Seeding here would be a copy
|
|
518
|
+
* of V1's line. An earlier revision of this comment claimed the metadata was
|
|
519
|
+
* absent from the V2 event; the SDK types refute that, so do not restore it.
|
|
447
520
|
*
|
|
448
|
-
*
|
|
449
|
-
* `
|
|
450
|
-
*
|
|
451
|
-
*
|
|
521
|
+
* WHAT IS NOT MEASURED, in either direction: whether a live 2.0.22 host
|
|
522
|
+
* populates `info.parentID` on this event. The tool canary records
|
|
523
|
+
* `Object.keys(ctx)` from inside the host, not event payloads, so host
|
|
524
|
+
* population is UNVERIFIED. What is proven is only that the type carries the
|
|
525
|
+
* field and that no code path here reads it.
|
|
526
|
+
*
|
|
527
|
+
* The real asymmetry is the OTHER of V1's two seed sources. Besides this event,
|
|
528
|
+
* V1 runs a fail-open startup seed from `client.session.list()`
|
|
529
|
+
* (`seedRootSessionsInBackground`, `src/plugin.ts`) to cover sessions that
|
|
530
|
+
* predate plugin load, because opencode does not replay `session.created` for
|
|
531
|
+
* them. V2's `PluginContext` exposes no `client` at all — `options`, `agent`,
|
|
532
|
+
* `aisdk`, `catalog`, `command`, `integration`, `plugin`, `reference`, `skill`,
|
|
533
|
+
* and nothing else — so that enumeration has NO V2 EQUIVALENT. Wiring this
|
|
534
|
+
* handler would recover live sessions only, never resumed ones.
|
|
535
|
+
*
|
|
536
|
+
* And — more to the point — V1 is a separate plugin instance that is not loaded
|
|
537
|
+
* when only `plugin-v2` is configured, so on a V2-only install nothing seeds
|
|
538
|
+
* the hierarchy at all. Do not read the bridge check below as coverage: a
|
|
539
|
+
* non-null bridge means a host process that ALSO loaded V1, and V1 does the
|
|
540
|
+
* seeding itself.
|
|
541
|
+
*
|
|
542
|
+
* The unwired hierarchy seed is ONE of the two reasons the V2 enforcement guard
|
|
543
|
+
* is built without `isRootSession` / `isChildSession`; the other — and the one
|
|
544
|
+
* that would actually deny every `task()` call — is the absent
|
|
545
|
+
* `getSessionAgent`. See the `v2EnforcementGuard` comment below for both, and
|
|
546
|
+
* `docs/UPGRADING.md`, "Known limitation — V2 enforcement covers the
|
|
547
|
+
* blocked-tool list, not the V1 delegation matrix".
|
|
452
548
|
*/
|
|
453
549
|
async function onSessionCreated(_event: V2SessionEvent): Promise<void> {
|
|
454
550
|
const _bridge = resolveBridge()
|
|
@@ -475,6 +571,58 @@ async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
|
|
|
475
571
|
}
|
|
476
572
|
}
|
|
477
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Session-deleted handler: drop the read-only registration.
|
|
576
|
+
*
|
|
577
|
+
* `readOnlyRegistry` is a process-wide singleton and the `execute.before` hook
|
|
578
|
+
* populates it on every delegated `apollo`/`gaia` call. V1 prunes it on
|
|
579
|
+
* `session.deleted` (`src/plugin.ts`); V2 did not, so on a long-lived host
|
|
580
|
+
* every finished investigation session stayed registered for the life of the
|
|
581
|
+
* process — a slow leak, and a stale entry that would keep denying tools if
|
|
582
|
+
* the id were ever reused.
|
|
583
|
+
*
|
|
584
|
+
* This mirrors V1: unregister the session, which drops its agent entry with it
|
|
585
|
+
* (the registry stores one `ReadOnlyEntry` per session id). V1 also clears a
|
|
586
|
+
* `sessionAgents` map here; V2 has no such map and needs no equivalent, since
|
|
587
|
+
* it reads the active agent off the event instead of caching it.
|
|
588
|
+
*
|
|
589
|
+
* The id is read from `properties.sessionID` — the field the sibling
|
|
590
|
+
* `session.idle` handler already uses — falling back to V1's
|
|
591
|
+
* `properties.info.id`. Both spellings are accepted because the V2 SDK types
|
|
592
|
+
* `session.deleted` as `{ sessionID, info }` and a miss here is a silent leak,
|
|
593
|
+
* which is precisely the bug this handler exists to close.
|
|
594
|
+
*
|
|
595
|
+
* A payload with NEITHER spelling is fail-open, and unavoidably so: unregistering
|
|
596
|
+
* is what needs the id, so an id that cannot be resolved cannot be cleaned up
|
|
597
|
+
* and the entry survives. The docstring used to leave that as a documented
|
|
598
|
+
* silent branch; it now warns on the way out, because the only other symptom of
|
|
599
|
+
* a renamed field is a registry that grows for the life of the process.
|
|
600
|
+
*/
|
|
601
|
+
async function onSessionDeleted(event: V2SessionEvent): Promise<void> {
|
|
602
|
+
const properties = event.properties
|
|
603
|
+
if (properties === undefined) return
|
|
604
|
+
const info = properties.info
|
|
605
|
+
const sessionID =
|
|
606
|
+
typeof properties.sessionID === 'string'
|
|
607
|
+
? properties.sessionID
|
|
608
|
+
: info !== null && typeof info === 'object'
|
|
609
|
+
? (info as { id?: unknown }).id
|
|
610
|
+
: undefined
|
|
611
|
+
if (typeof sessionID !== 'string' || sessionID === '') {
|
|
612
|
+
// Fail-open by necessity, loud by choice: the entry for this session (if
|
|
613
|
+
// any) cannot be identified, so it stays registered. Same `console.warn`
|
|
614
|
+
// channel as the event-subscription error above — this module has no
|
|
615
|
+
// logger, and a diagnostic nobody reads on stderr is still a silent leak.
|
|
616
|
+
console.warn(
|
|
617
|
+
'[Pantheon V2] session.deleted carried no session id ' +
|
|
618
|
+
'(neither properties.sessionID nor properties.info.id); ' +
|
|
619
|
+
'its read-only registration cannot be pruned and will persist.',
|
|
620
|
+
)
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
readOnlyRegistry.unregister(sessionID)
|
|
624
|
+
}
|
|
625
|
+
|
|
478
626
|
/**
|
|
479
627
|
* Session-error handler: no V2-side implementation.
|
|
480
628
|
*
|
|
@@ -505,11 +653,7 @@ async function onSessionCompacted(_event: V2SessionEvent): Promise<void> {}
|
|
|
505
653
|
* Returns true if any hooks were registered.
|
|
506
654
|
*/
|
|
507
655
|
async function registerV2SessionHooks(context: PluginContext): Promise<boolean> {
|
|
508
|
-
const sessionCtx = (context
|
|
509
|
-
| {
|
|
510
|
-
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
511
|
-
}
|
|
512
|
-
| undefined
|
|
656
|
+
const sessionCtx = hostDomains(context).session
|
|
513
657
|
|
|
514
658
|
if (!sessionCtx?.hook) {
|
|
515
659
|
return false
|
|
@@ -600,16 +744,21 @@ function maskSecret(text: string): string {
|
|
|
600
744
|
/**
|
|
601
745
|
* Message thrown when scan-secrets.sh exits 2 (a high-confidence token match).
|
|
602
746
|
*
|
|
603
|
-
* This is the only deliberate throw on the V2 surface
|
|
604
|
-
* message (src/plugins/pantheon-hooks.ts `SECRET_BLOCK_MESSAGE`). opencode's
|
|
747
|
+
* This is the only deliberate throw on the V2 surface. opencode's
|
|
605
748
|
* Plugin.trigger has no catch around hook invocations, so the rejection
|
|
606
749
|
* propagates and fails the tool's execute promise BEFORE the tool body runs —
|
|
607
|
-
* the same hard block V1
|
|
750
|
+
* the same hard block V1 has (src/plugins/pantheon-hooks.ts
|
|
751
|
+
* `SECRET_BLOCK_MESSAGE`), which was inert on a V2-only install because
|
|
608
752
|
* pantheon-hooks.ts is never loaded there (opencode.json `plugins` declares only
|
|
609
753
|
* `src/plugin-v2`).
|
|
754
|
+
*
|
|
755
|
+
* English, like every other message this module emits: this string surfaces in
|
|
756
|
+
* the user's session as the tool error, so it is read by whoever hit the block,
|
|
757
|
+
* not by the code that raised it. A Portuguese release note elsewhere in the
|
|
758
|
+
* repo is not a reason for the runtime message to be Portuguese too.
|
|
610
759
|
*/
|
|
611
760
|
const SECRET_BLOCK_MESSAGE =
|
|
612
|
-
'[plugin-v2]
|
|
761
|
+
'[plugin-v2] Blocked: high-confidence secret detected in tool input — see .pantheon/logs/hooks.log'
|
|
613
762
|
|
|
614
763
|
/**
|
|
615
764
|
* Append one masked, ISO-stamped line to .pantheon/logs/hooks.log under the
|
|
@@ -682,15 +831,41 @@ interface V2ToolExecuteBeforeEvent {
|
|
|
682
831
|
* tool — `hashline_edit` and `pantheon_model` are denied here for exactly the
|
|
683
832
|
* reason they are denied in V1.
|
|
684
833
|
*
|
|
685
|
-
* Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
834
|
+
* Deliberately NOT passed `getSessionAgent` / `isRootSession` / `isChildSession`.
|
|
835
|
+
* There are TWO distinct reasons, and conflating them is what produced a wrong
|
|
836
|
+
* causal chain here once already — keep them apart:
|
|
837
|
+
*
|
|
838
|
+
* 1. MISSING `getSessionAgent` (wiring). This is what would deny every
|
|
839
|
+
* `task()` call. V2 learns the active agent from the `execute.before` event
|
|
840
|
+
* itself and keeps no session→agent map, so there is no lookup to hand the
|
|
841
|
+
* guard. The guard would call `isDelegationAllowed(undefined, target)`,
|
|
842
|
+
* which returns `false`, and throw "caller agent is unavailable" — for
|
|
843
|
+
* every caller, including Zeus. Note what is NOT happening: an unseeded
|
|
844
|
+
* `isRootSession` would *pass* the root gate (`isRoot` reports `true` for an
|
|
845
|
+
* unknown session), so the hierarchy is not what denies here.
|
|
846
|
+
*
|
|
847
|
+
* 2. UNWIRED HIERARCHY (design). `SessionHierarchyRegistry` is what gates
|
|
848
|
+
* exactly two things, and nothing else: the depth-2 child deny
|
|
849
|
+
* (`isChildSession`) and the root-session gate (`isRootSession`). Left
|
|
850
|
+
* unseeded, `isChild` is `false` for every session, so depth-2 would never
|
|
851
|
+
* fire on a genuine child — a hierarchy that cannot tell a child from a root
|
|
852
|
+
* is not a trustworthy input to either check.
|
|
853
|
+
*
|
|
854
|
+
* "Unwired", not "impossible": the V2 `session.created` event carries
|
|
855
|
+
* `properties.info: Session` including `parentID?`, the same field V1 seeds
|
|
856
|
+
* from on the same event, so one line in `onSessionCreated` would seed live
|
|
857
|
+
* sessions (see its docstring). What has no V2 equivalent is V1's SECOND
|
|
858
|
+
* source, the `client.session.list()` startup seed — V2's `PluginContext`
|
|
859
|
+
* exposes no `client`. Whether a live 2.0.22 host populates `info.parentID`
|
|
860
|
+
* is UNVERIFIED; no canary reads an event payload. So the correct summary is
|
|
861
|
+
* "seeded for nothing", not "unseedable", and the guard's omission is right
|
|
862
|
+
* today for the weaker reason that nothing seeds it at all.
|
|
863
|
+
*
|
|
864
|
+
* Omitting all three skips the branch wholesale: the guard only enters it when
|
|
865
|
+
* `options?.isRootSession !== undefined`. `task` is still denied inside a
|
|
866
|
+
* read-only session via the blocked-tool list, so depth-2 holds for
|
|
867
|
+
* apollo/gaia; the caller/target matrix itself is NOT enforced on V2. That is
|
|
868
|
+
* a known gap, not a covered case.
|
|
694
869
|
*/
|
|
695
870
|
const v2EnforcementGuard = createEnforcementGuard({
|
|
696
871
|
getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
|
|
@@ -740,11 +915,7 @@ function adaptV2ExecuteBeforeEvent(event: unknown): {
|
|
|
740
915
|
* Returns true if any hooks were registered.
|
|
741
916
|
*/
|
|
742
917
|
async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
743
|
-
const toolCtx = (context
|
|
744
|
-
| {
|
|
745
|
-
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
746
|
-
}
|
|
747
|
-
| undefined
|
|
918
|
+
const toolCtx = hostDomains(context).tool
|
|
748
919
|
|
|
749
920
|
if (!toolCtx?.hook) {
|
|
750
921
|
return false
|
|
@@ -842,11 +1013,7 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
|
842
1013
|
* Returns true if the hook was registered.
|
|
843
1014
|
*/
|
|
844
1015
|
async function registerV2PermissionHook(context: PluginContext): Promise<boolean> {
|
|
845
|
-
const permCtx = (context
|
|
846
|
-
| {
|
|
847
|
-
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
848
|
-
}
|
|
849
|
-
| undefined
|
|
1016
|
+
const permCtx = hostDomains(context).permission
|
|
850
1017
|
|
|
851
1018
|
if (!permCtx?.hook) {
|
|
852
1019
|
markUnsupported('permission-hook')
|
|
@@ -920,10 +1087,12 @@ export const plugin = define({
|
|
|
920
1087
|
//
|
|
921
1088
|
// Pantheon's Phase 1/config transform registrations cover agent, command,
|
|
922
1089
|
// and reference; ctx.tool.transform is attempted separately in Phase 2.
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
1090
|
+
// ctx.integration and ctx.skill are present on a live 2.0.22 host and neither
|
|
1091
|
+
// is registered here — that is a Pantheon scope choice, not a host gap.
|
|
1092
|
+
// ctx.catalog is the one domain the host does not provide.
|
|
1093
|
+
// Phase 1 registers against the SDK-typed `context.agent` / `.command` /
|
|
1094
|
+
// `.reference` directly; the four domains the SDK omits are reached only
|
|
1095
|
+
// through `hostDomains` (see the module header).
|
|
927
1096
|
const phase1Transforms: Array<{ feature: string; register: () => Promise<unknown> }> = [
|
|
928
1097
|
{ feature: 'agent-transform', register: () => context.agent.transform(transformAgents) },
|
|
929
1098
|
{
|
|
@@ -976,14 +1145,7 @@ export const plugin = define({
|
|
|
976
1145
|
await registerV2PermissionHook(context).catch(() => false)
|
|
977
1146
|
|
|
978
1147
|
// ─── Phase 7: V2 Compaction Hook (best-effort) ───────────────────
|
|
979
|
-
const compactionCtx = (context
|
|
980
|
-
| {
|
|
981
|
-
hook?: (
|
|
982
|
-
name: string,
|
|
983
|
-
handler: (event: unknown) => void | Promise<void>,
|
|
984
|
-
) => Promise<unknown>
|
|
985
|
-
}
|
|
986
|
-
| undefined
|
|
1148
|
+
const compactionCtx = hostDomains(context).session
|
|
987
1149
|
if (compactionCtx?.hook) {
|
|
988
1150
|
try {
|
|
989
1151
|
// 2.0.16 `SessionHooks` key is `compaction` (the migration doc maps
|
|
@@ -1042,7 +1204,14 @@ export function v2Dispose(): void {
|
|
|
1042
1204
|
}
|
|
1043
1205
|
|
|
1044
1206
|
/**
|
|
1045
|
-
*
|
|
1207
|
+
* The current list of unsupported V2 features.
|
|
1208
|
+
*
|
|
1209
|
+
* Read by the installer and by `doctor` (both via the shared seed in
|
|
1210
|
+
* `src/pantheon/v2-unsupported.mjs`) so the markers a user is shown are the
|
|
1211
|
+
* same ones this returns, rather than a hand-copied duplicate.
|
|
1212
|
+
*
|
|
1213
|
+
* This returns the LIVE array: the shared seed plus whatever `markUnsupported`
|
|
1214
|
+
* appended while this process talked to a real host.
|
|
1046
1215
|
*/
|
|
1047
1216
|
export function getUnsupportedFeatures(): readonly string[] {
|
|
1048
1217
|
return V2_UNSUPPORTED_FEATURES
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* This stub satisfies the loader contract; pantheon-tui is TUI-only and has
|
|
12
12
|
* no background server work of its own.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
export default function server(): {};
|
|
15
15
|
//#endregion
|
|
16
|
-
export { server as default };
|
|
17
16
|
//# sourceMappingURL=server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;wBAYwB"}
|