pantheon-opencode 1.6.0-beta.6 → 1.6.0-beta.8
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 +19 -769
- package/CHANGELOG.md +53 -0
- package/README.md +78 -9
- package/README.pt-BR.md +10 -4
- package/package.json +5 -1
- package/plugin.json +2 -2
- package/pyproject.toml +3 -1
- package/scripts/build-agents-md.mjs +159 -26
- package/scripts/install/agents-md.mjs +2 -2
- package/scripts/install/config-migration.mjs +115 -1
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode.mjs +245 -12
- package/scripts/install/shared.mjs +115 -7
- package/scripts/sync-artifacts.mjs +2 -2
- package/scripts/test-opencode-v2-sandbox.sh +155 -4
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +5 -5
- package/src/agents/aphrodite.md +18 -36
- package/src/agents/demeter.md +8 -27
- package/src/agents/gaia.md +1 -0
- package/src/agents/hephaestus.md +10 -20
- package/src/agents/hermes.md +15 -37
- package/src/agents/mnemosyne.md +14 -42
- package/src/agents/prometheus.md +16 -19
- package/src/agents/talos.md +4 -1
- package/src/agents/themis.md +4 -4
- package/src/agents/zeus.md +24 -127
- package/src/instructions/agent-return-format.instructions.md +2 -3
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +11 -121
- package/src/instructions/zeus-anti-stall.instructions.md +14 -69
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
- package/src/instructions/zeus-memory-operations.instructions.md +26 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
- package/src/pantheon/v2-delegation-enforce.ts +87 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-unsupported.mjs +0 -23
- package/src/plugin-v2.ts +271 -36
- package/src/plugins/tui/dist/tui.d.ts +11 -8
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +53 -37
- package/src/plugins/tui/package-lock.json +30 -33
- package/src/plugins/tui/package.json +5 -1
- package/src/plugins/tui/src/index.tsx +103 -82
- package/src/plugins/tui/tsdown.config.ts +17 -0
- package/src/routing.yml +10 -5
- package/src/skills/README.md +3 -3
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +37 -28
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
package/src/plugin-v2.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* `src/plugin-v2` in `plugins`, so the V1 plugin never loads on a V2-only
|
|
20
20
|
* install and V1's identical block was inert by construction.
|
|
21
21
|
|
|
22
|
-
* - A tool `execute.after` hook and a permission hook,
|
|
23
|
-
*
|
|
22
|
+
* - A tool `execute.after` hook and a permission hook, with permission
|
|
23
|
+
* evaluation enforcing the caller/target matrix for Pantheon agents
|
|
24
24
|
*
|
|
25
25
|
* Every tool registered through `draft.add` MUST declare `output`, and its
|
|
26
26
|
* `execute` MUST resolve to a value carrying `output`. The host enforces the
|
|
@@ -93,9 +93,17 @@ import {
|
|
|
93
93
|
type PantheonV2Bridge,
|
|
94
94
|
type V2ContextLike,
|
|
95
95
|
} from './pantheon/v2-bridge.ts'
|
|
96
|
+
import {
|
|
97
|
+
type AuthoritativeSession,
|
|
98
|
+
evaluateV2Delegation,
|
|
99
|
+
PANTHEON_AGENTS,
|
|
100
|
+
type V2PermissionEvent,
|
|
101
|
+
v2ResourceTarget,
|
|
102
|
+
} from './pantheon/v2-delegation-enforce.ts'
|
|
103
|
+
import { createV2ExecuteAfter, resolveV2SandboxConfig } from './pantheon/v2-execute-after.ts'
|
|
96
104
|
import { createV2ToolDefinitions, type V2ToolResult } from './pantheon/v2-tools.ts'
|
|
97
105
|
import { V2_UNSUPPORTED_FEATURE_SEED } from './pantheon/v2-unsupported.mjs'
|
|
98
|
-
import { type HookPayload, runHook } from './plugins/hook-runner.ts'
|
|
106
|
+
import { type HookPayload, type HookResult, runHook } from './plugins/hook-runner.ts'
|
|
99
107
|
|
|
100
108
|
// ─── Unsupported Features Registry ───────────────────────────────────────
|
|
101
109
|
|
|
@@ -220,23 +228,26 @@ interface V2EventDomain {
|
|
|
220
228
|
/**
|
|
221
229
|
* The minimal shape of `ctx.permission`.
|
|
222
230
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
231
|
+
* The V2.0.25 live contract supplies an `evaluate` event with `resources`;
|
|
232
|
+
* runtime validation remains defensive because SDK declarations omit this
|
|
233
|
+
* domain and the host may evolve independently.
|
|
226
234
|
*/
|
|
227
235
|
interface V2PermissionDomain {
|
|
228
|
-
hook?: (
|
|
236
|
+
hook?: (
|
|
237
|
+
name: string,
|
|
238
|
+
handler: (event: unknown, output?: unknown) => void | Promise<void>,
|
|
239
|
+
) => Promise<unknown>
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/**
|
|
232
|
-
* The minimal shape of `ctx.session
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* why the host-backed hook canary exists.
|
|
243
|
+
* The minimal shape of `ctx.session` used by this module. `get` follows the V2
|
|
244
|
+
* Promise API's object input (`{ sessionID }`); session hooks accept any string
|
|
245
|
+
* and silently drop an unknown one, which is why the host-backed hook canary
|
|
246
|
+
* exists.
|
|
237
247
|
*/
|
|
238
248
|
interface V2SessionDomain {
|
|
239
249
|
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
250
|
+
get?: (input: { sessionID: string }) => Promise<unknown>
|
|
240
251
|
}
|
|
241
252
|
|
|
242
253
|
/** `PluginContext` plus the four domains 1.18.x omits. See the module header. */
|
|
@@ -760,6 +771,26 @@ function maskSecret(text: string): string {
|
|
|
760
771
|
const SECRET_BLOCK_MESSAGE =
|
|
761
772
|
'[plugin-v2] Blocked: high-confidence secret detected in tool input — see .pantheon/logs/hooks.log'
|
|
762
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Message thrown when the secret scanner cannot produce a trustworthy verdict —
|
|
776
|
+
* the script is missing/unexecutable, the child timed out or was killed, the
|
|
777
|
+
* payload could not be serialized, the report was malformed, or the exit status
|
|
778
|
+
* was unexpected.
|
|
779
|
+
*
|
|
780
|
+
* This is a deliberate fail-CLOSED denial. The previous handler blocked only
|
|
781
|
+
* `code === 2` and treated every other outcome as advisory — but the runner
|
|
782
|
+
* reports a missing script, a spawn failure and a payload-serialization failure
|
|
783
|
+
* as `code: 1` too (src/plugins/hook-runner.ts), so a broken scanner read as a
|
|
784
|
+
* clean advisory and the tool ran, silently disabling the only hard secret block
|
|
785
|
+
* on the V2 surface.
|
|
786
|
+
*
|
|
787
|
+
* English and user-facing, like {@link SECRET_BLOCK_MESSAGE}. It carries no
|
|
788
|
+
* scanner output on purpose: a scan that failed mid-flight may still be holding
|
|
789
|
+
* the raw payload, and the denial must not become the leak it exists to prevent.
|
|
790
|
+
*/
|
|
791
|
+
const SECRET_SCAN_UNAVAILABLE_MESSAGE =
|
|
792
|
+
'[plugin-v2] Blocked: the secret scanner could not complete, so this tool call was denied (fail-closed) — see .pantheon/logs/hooks.log'
|
|
793
|
+
|
|
763
794
|
/**
|
|
764
795
|
* Append one masked, ISO-stamped line to .pantheon/logs/hooks.log under the
|
|
765
796
|
* working directory, and return whether it landed.
|
|
@@ -795,6 +826,98 @@ function appendV2HookLog(line: string): boolean {
|
|
|
795
826
|
}
|
|
796
827
|
}
|
|
797
828
|
|
|
829
|
+
// ─── V2 Secret-Scan Verdict ─────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The scanner's own report marker, written to stderr on every non-clean run
|
|
833
|
+
* (exit 1 advisory, exit 2 block). It is the discriminator between a genuine
|
|
834
|
+
* low-confidence advisory and a runner infrastructure failure, BOTH of which
|
|
835
|
+
* surface as `code === 1`: the runner's own failures (missing/unexecutable
|
|
836
|
+
* script, spawn error, payload-serialization error) resolve a structured
|
|
837
|
+
* `HookResult` with `code: 1` and no scan report, while the real scanner's
|
|
838
|
+
* advisory always carries this marker. Requiring it means a scanner that stops
|
|
839
|
+
* reporting fails toward a denial, never toward a silent allow.
|
|
840
|
+
*/
|
|
841
|
+
const SECRET_SCAN_MARKER = '[SECRET SCAN]'
|
|
842
|
+
|
|
843
|
+
/** Sanitized failure classes — logged in place of raw scanner output. */
|
|
844
|
+
type SecretScanFailureReason =
|
|
845
|
+
| 'scanner-timeout'
|
|
846
|
+
| 'scanner-signal'
|
|
847
|
+
| 'scanner-spawn-failure'
|
|
848
|
+
| 'scanner-serialization-failure'
|
|
849
|
+
| 'scanner-malformed-output'
|
|
850
|
+
| 'scanner-unexpected-exit'
|
|
851
|
+
|
|
852
|
+
type SecretScanDecision =
|
|
853
|
+
| { action: 'allow' }
|
|
854
|
+
| { action: 'advisory' }
|
|
855
|
+
| { action: 'block' }
|
|
856
|
+
| { action: 'fail-closed'; reason: SecretScanFailureReason }
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The runner's own infrastructure-failure messages (see hook-runner.ts), matched
|
|
860
|
+
* at the start of stderr where the runner is the only writer. Matching a prefix
|
|
861
|
+
* rather than a substring keeps a scan report that merely happens to quote one
|
|
862
|
+
* of these phrases from flipping a valid advisory into a denial.
|
|
863
|
+
*/
|
|
864
|
+
const SCANNER_INFRA_FAILURES: ReadonlyArray<[string, SecretScanFailureReason]> = [
|
|
865
|
+
['payload serialization failed:', 'scanner-serialization-failure'],
|
|
866
|
+
['spawn failed:', 'scanner-spawn-failure'],
|
|
867
|
+
['hook script failed to spawn:', 'scanner-spawn-failure'],
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Classify one secret-scan {@link HookResult} into an enforcement decision.
|
|
872
|
+
*
|
|
873
|
+
* fail-closed by construction: `allow` and `advisory` are reachable only from a
|
|
874
|
+
* result that is unambiguously a completed scanner run. Everything else —
|
|
875
|
+
* timeout, signal, spawn/serialization failure, missing report, unknown or
|
|
876
|
+
* unexpected exit status — denies. `advisory` additionally requires the
|
|
877
|
+
* scanner's own report marker, which is what separates an exit-1 advisory from
|
|
878
|
+
* the runner's exit-1 infrastructure failure.
|
|
879
|
+
*
|
|
880
|
+
* `block` covers the high-confidence match (exit 2); the caller turns it into
|
|
881
|
+
* the user-facing block.
|
|
882
|
+
*/
|
|
883
|
+
function evaluateSecretScanResult(scan: HookResult): SecretScanDecision {
|
|
884
|
+
if (scan.timedOut) return { action: 'fail-closed', reason: 'scanner-timeout' }
|
|
885
|
+
if (scan.signal !== null) return { action: 'fail-closed', reason: 'scanner-signal' }
|
|
886
|
+
if (!Number.isInteger(scan.code)) {
|
|
887
|
+
return { action: 'fail-closed', reason: 'scanner-malformed-output' }
|
|
888
|
+
}
|
|
889
|
+
for (const [signature, reason] of SCANNER_INFRA_FAILURES) {
|
|
890
|
+
if (scan.stderr.startsWith(signature)) return { action: 'fail-closed', reason }
|
|
891
|
+
}
|
|
892
|
+
if (scan.code === 0) return { action: 'allow' }
|
|
893
|
+
if (scan.code === 2) return { action: 'block' }
|
|
894
|
+
if (scan.code === 1) {
|
|
895
|
+
return scan.stderr.includes(SECRET_SCAN_MARKER)
|
|
896
|
+
? { action: 'advisory' }
|
|
897
|
+
: { action: 'fail-closed', reason: 'scanner-malformed-output' }
|
|
898
|
+
}
|
|
899
|
+
return { action: 'fail-closed', reason: 'scanner-unexpected-exit' }
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** Invocation of scripts/hooks/scan-secrets.sh used by the tool guard. */
|
|
903
|
+
type SecretScanRunner = (payload: HookPayload) => Promise<HookResult>
|
|
904
|
+
|
|
905
|
+
const defaultSecretScanRunner: SecretScanRunner = (payload) => runHook('scan-secrets.sh', payload)
|
|
906
|
+
|
|
907
|
+
let secretScanRunner: SecretScanRunner = defaultSecretScanRunner
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Test seam — override (or, with `null`, restore) the V2 secret-scan runner.
|
|
911
|
+
*
|
|
912
|
+
* Exported only for tests/pantheon/plugin-v2-contract.test.ts, which injects
|
|
913
|
+
* structured {@link HookResult}s (missing scanner, timeout, malformed output)
|
|
914
|
+
* that the real script cannot be made to produce on demand. There is no
|
|
915
|
+
* production caller; the shipped handler always uses `defaultSecretScanRunner`.
|
|
916
|
+
*/
|
|
917
|
+
export function setV2SecretScanRunnerForTests(runner: SecretScanRunner | null): void {
|
|
918
|
+
secretScanRunner = runner ?? defaultSecretScanRunner
|
|
919
|
+
}
|
|
920
|
+
|
|
798
921
|
// ─── V2 Tool Hooks ──────────────────────────────────────────────────────
|
|
799
922
|
|
|
800
923
|
/**
|
|
@@ -952,10 +1075,13 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
|
952
1075
|
// `plugins` declares only `src/plugin-v2`. V1's scan-secrets block was
|
|
953
1076
|
// therefore inert by construction, not by misconfiguration.
|
|
954
1077
|
//
|
|
955
|
-
// exit
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
1078
|
+
// exit 0 → allow. exit 1 with a valid scanner report → advisory, logged
|
|
1079
|
+
// only. exit 2 (high-confidence match) → deny. A scanner that does NOT
|
|
1080
|
+
// produce a trustworthy verdict — missing/unexecutable script, timeout,
|
|
1081
|
+
// killed child, spawn/serialization failure, malformed or unexpected
|
|
1082
|
+
// output — is fail-CLOSED: this pending tool call is denied, the session
|
|
1083
|
+
// is not. (runHook resolves a structured result instead of rejecting, so
|
|
1084
|
+
// the exit-code-only reading below cannot lean on an exception.)
|
|
959
1085
|
let blockError: Error | null = null
|
|
960
1086
|
try {
|
|
961
1087
|
const payload: HookPayload = {
|
|
@@ -964,18 +1090,29 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
|
964
1090
|
agent_id: typeof agent === 'string' ? agent : '',
|
|
965
1091
|
session_id: input.sessionID,
|
|
966
1092
|
}
|
|
967
|
-
const scan = await
|
|
968
|
-
|
|
1093
|
+
const scan = await secretScanRunner(payload)
|
|
1094
|
+
const decision = evaluateSecretScanResult(scan)
|
|
1095
|
+
if (decision.action === 'block') {
|
|
969
1096
|
appendV2HookLog(SECRET_BLOCK_MESSAGE)
|
|
970
1097
|
appendV2HookLog(scan.stderr)
|
|
971
1098
|
blockError = new Error(SECRET_BLOCK_MESSAGE)
|
|
972
|
-
} else if (
|
|
1099
|
+
} else if (decision.action === 'advisory') {
|
|
973
1100
|
// Advisory: log only, never block.
|
|
974
1101
|
appendV2HookLog(scan.stderr)
|
|
1102
|
+
} else if (decision.action === 'fail-closed') {
|
|
1103
|
+
// Sanitized failure category only — never the scanner's raw stderr,
|
|
1104
|
+
// which on a failure path is unverified and may carry the very
|
|
1105
|
+
// payload the scan exists to keep out of logs.
|
|
1106
|
+
appendV2HookLog(`${SECRET_SCAN_UNAVAILABLE_MESSAGE} (${decision.reason})`)
|
|
1107
|
+
blockError = new Error(SECRET_SCAN_UNAVAILABLE_MESSAGE)
|
|
975
1108
|
}
|
|
976
1109
|
} catch {
|
|
977
|
-
//
|
|
978
|
-
//
|
|
1110
|
+
// The runner is contracted to resolve a structured result, so this only
|
|
1111
|
+
// guards an unexpected throw (including a future change to the runner).
|
|
1112
|
+
// A scanner that cannot run is fail-CLOSED: deny this call rather than
|
|
1113
|
+
// let it through with no scan at all.
|
|
1114
|
+
appendV2HookLog(SECRET_SCAN_UNAVAILABLE_MESSAGE)
|
|
1115
|
+
blockError = new Error(SECRET_SCAN_UNAVAILABLE_MESSAGE)
|
|
979
1116
|
}
|
|
980
1117
|
if (blockError !== null) throw blockError
|
|
981
1118
|
})
|
|
@@ -985,15 +1122,19 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
|
985
1122
|
}
|
|
986
1123
|
|
|
987
1124
|
try {
|
|
988
|
-
// "execute.after" —
|
|
1125
|
+
// "execute.after" — the V1 parity chain (task-result-guard →
|
|
1126
|
+
// context-sandbox → read-enhancer) rebuilt on the V2 event shape.
|
|
989
1127
|
//
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
|
|
995
|
-
|
|
1128
|
+
// This IS behaviour, not a registration point: on a V2-only install the V1
|
|
1129
|
+
// `pantheon-hooks` plugin is not loaded, so this handler is the only thing
|
|
1130
|
+
// that truncates oversized output and tags `read` with hashline refs.
|
|
1131
|
+
// The sandbox config comes from `ctx.options.context_sandbox` (the V2 copy
|
|
1132
|
+
// the installer writes — V2.0.25 drops the V1 top-level key), resolved with
|
|
1133
|
+
// the same resolver V1 uses.
|
|
1134
|
+
const v2ExecuteAfter = createV2ExecuteAfter({
|
|
1135
|
+
sandbox: resolveV2SandboxConfig(context.options),
|
|
996
1136
|
})
|
|
1137
|
+
await toolCtx.hook('execute.after', (event: unknown) => v2ExecuteAfter(event))
|
|
997
1138
|
registered = true
|
|
998
1139
|
} catch {
|
|
999
1140
|
// Tool after-hook not supported
|
|
@@ -1008,12 +1149,15 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
|
1008
1149
|
* Register permission hook via V2 ctx.permission.hook.
|
|
1009
1150
|
*
|
|
1010
1151
|
* The V2 permission.hook API (when available) provides:
|
|
1011
|
-
* ctx.permission.hook("evaluate", handler) —
|
|
1152
|
+
* ctx.permission.hook("evaluate", handler) — host permission evaluation
|
|
1153
|
+
* The handler receives one mutable PermissionEvaluation event; decisions are
|
|
1154
|
+
* written to event.effect/event.message (there is no separate V1-style output).
|
|
1012
1155
|
*
|
|
1013
1156
|
* Returns true if the hook was registered.
|
|
1014
1157
|
*/
|
|
1015
1158
|
async function registerV2PermissionHook(context: PluginContext): Promise<boolean> {
|
|
1016
|
-
const
|
|
1159
|
+
const domains = hostDomains(context)
|
|
1160
|
+
const permCtx = domains.permission
|
|
1017
1161
|
|
|
1018
1162
|
if (!permCtx?.hook) {
|
|
1019
1163
|
markUnsupported('permission-hook')
|
|
@@ -1021,12 +1165,103 @@ async function registerV2PermissionHook(context: PluginContext): Promise<boolean
|
|
|
1021
1165
|
}
|
|
1022
1166
|
|
|
1023
1167
|
try {
|
|
1024
|
-
await permCtx.hook('evaluate', (
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1168
|
+
await permCtx.hook('evaluate', async (rawEvent: unknown) => {
|
|
1169
|
+
const event = rawEvent as V2PermissionEvent | null
|
|
1170
|
+
const effectBefore = event?.effect
|
|
1171
|
+
const writeProof = (): void => {
|
|
1172
|
+
const proofPath = process.env.PANTHEON_HOOK_CANARY_PERMISSION_PROOF
|
|
1173
|
+
if (!proofPath || !event || !Array.isArray(event.resources)) return
|
|
1174
|
+
try {
|
|
1175
|
+
appendFileSync(
|
|
1176
|
+
proofPath,
|
|
1177
|
+
`permission.evaluate ${JSON.stringify({
|
|
1178
|
+
resources: event.resources,
|
|
1179
|
+
agent: event.agent,
|
|
1180
|
+
sessionID: event.sessionID,
|
|
1181
|
+
effect: event.effect,
|
|
1182
|
+
message: event.message,
|
|
1183
|
+
eventFields: ['resources', 'agent', 'sessionID', 'effect'].filter((key) =>
|
|
1184
|
+
Object.hasOwn(event, key),
|
|
1185
|
+
),
|
|
1186
|
+
effectBefore: effectBefore ?? null,
|
|
1187
|
+
effectAfter: event.effect ?? null,
|
|
1188
|
+
})}\n`,
|
|
1189
|
+
)
|
|
1190
|
+
} catch {
|
|
1191
|
+
// Instrumentation is best-effort and must not affect permission flow.
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const deny = (reason: string): void => {
|
|
1195
|
+
if (event === null || typeof event !== 'object') {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`permission event is unavailable; denying V2 permission evaluation (${reason})`,
|
|
1198
|
+
)
|
|
1199
|
+
}
|
|
1200
|
+
event.effect = 'deny'
|
|
1201
|
+
event.message = `Pantheon delegation denied: ${reason}`
|
|
1202
|
+
if (event.effect !== 'deny') {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`permission event is not mutable; denying V2 permission evaluation (${reason})`,
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
writeProof()
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (event?.effect === 'deny') {
|
|
1211
|
+
writeProof()
|
|
1212
|
+
return // explicit host deny is final
|
|
1213
|
+
}
|
|
1214
|
+
if (event == null) {
|
|
1215
|
+
deny('permission event is missing')
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
if (!Array.isArray(event.resources) || event.resources.length === 0) {
|
|
1219
|
+
deny('permission resources are missing or malformed')
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1222
|
+
const targets = event.resources.map(v2ResourceTarget)
|
|
1223
|
+
if (targets.some((target) => target === undefined)) {
|
|
1224
|
+
deny('permission resource target is malformed')
|
|
1225
|
+
return
|
|
1226
|
+
}
|
|
1227
|
+
const hasPantheonTarget = targets.some((target) => PANTHEON_AGENTS.has(target ?? ''))
|
|
1228
|
+
let session: AuthoritativeSession | undefined
|
|
1229
|
+
if (hasPantheonTarget) {
|
|
1230
|
+
const sessionID = typeof event.sessionID === 'string' ? event.sessionID.trim() : ''
|
|
1231
|
+
if (!sessionID) {
|
|
1232
|
+
deny('caller session ID is missing')
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const sessionDomain = domains.session
|
|
1237
|
+
if (!sessionDomain || typeof sessionDomain.get !== 'function') {
|
|
1238
|
+
deny('session lookup API is unavailable')
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
let retrieved: unknown
|
|
1243
|
+
try {
|
|
1244
|
+
retrieved = await sessionDomain.get({ sessionID })
|
|
1245
|
+
} catch {
|
|
1246
|
+
deny('caller session lookup failed')
|
|
1247
|
+
return
|
|
1248
|
+
}
|
|
1249
|
+
if (retrieved === null || retrieved === undefined) {
|
|
1250
|
+
deny('caller session was not found')
|
|
1251
|
+
return
|
|
1252
|
+
}
|
|
1253
|
+
if (typeof retrieved !== 'object' || Array.isArray(retrieved)) {
|
|
1254
|
+
deny('caller session lookup returned invalid data')
|
|
1255
|
+
return
|
|
1256
|
+
}
|
|
1257
|
+
session = retrieved as AuthoritativeSession
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Native-only resources remain under host policy. If a Pantheon target
|
|
1261
|
+
// appears anywhere, the shared policy evaluates the complete resource set.
|
|
1262
|
+
const decision = evaluateV2Delegation(event, session, event.effect)
|
|
1263
|
+
if (decision === 'deny') deny('caller or target is not allowed by delegation policy')
|
|
1264
|
+
else writeProof()
|
|
1030
1265
|
})
|
|
1031
1266
|
return true
|
|
1032
1267
|
} catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
1
|
+
import { TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
2
2
|
//#region src/index.d.ts
|
|
3
3
|
export type DelegationEntry = {
|
|
4
4
|
/** Job alias, e.g. "apo-1" (from the H1 title, falling back to filename). */
|
|
@@ -487,17 +487,20 @@ export declare function navigateToDelegationSession(route: {
|
|
|
487
487
|
export declare function createDelegationRowOpenHandler(route: {
|
|
488
488
|
navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>;
|
|
489
489
|
} | undefined, taskID: string | undefined): () => void;
|
|
490
|
-
/** Plugin-level live delegation store shared
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
* durable child list and also reads the map as an optimistic live source. */
|
|
490
|
+
/** Plugin-level live delegation store shared by event subscriptions and View.
|
|
491
|
+
* Mutations notify subscribers so the View can refresh its durable child
|
|
492
|
+
* list while also reading the map as an optimistic live source. */
|
|
494
493
|
export type LiveDelegationStore = {
|
|
495
494
|
map: Map<string, LiveDelegationEntry>;
|
|
496
|
-
/**
|
|
497
|
-
|
|
498
|
-
/**
|
|
495
|
+
/** Subscribe to invalidations consumed by the sidebar's refresh path. */
|
|
496
|
+
subscribe: (listener: () => void) => () => void;
|
|
497
|
+
/** Notify sidebar subscribers after a live mutation. */
|
|
499
498
|
bump: () => void;
|
|
500
499
|
};
|
|
500
|
+
/** Create the observable live store shared by task events and the sidebar. */
|
|
501
|
+
export declare function createLiveDelegationStore(): LiveDelegationStore;
|
|
502
|
+
/** Register the live task-part events that invalidate the sidebar store. */
|
|
503
|
+
export declare function registerLiveDelegationEvents(api: TuiPluginApi, liveStore: LiveDelegationStore): () => void;
|
|
501
504
|
declare const plugin: TuiPluginModule & {
|
|
502
505
|
id: string;
|
|
503
506
|
setup: () => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promise
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { For, Show,
|
|
7
|
+
import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
|
|
8
8
|
//#region src/index.tsx
|
|
9
9
|
/** @jsxImportSource @opentui/solid */
|
|
10
10
|
/**
|
|
@@ -167,7 +167,7 @@ async function detectVersion(api) {
|
|
|
167
167
|
if (ver) return ver;
|
|
168
168
|
}
|
|
169
169
|
} catch {}
|
|
170
|
-
return "1.6.0-beta.
|
|
170
|
+
return "1.6.0-beta.8";
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
173
|
* usage-bar — AI subscription usage gauge for the opencode TUI.
|
|
@@ -1805,10 +1805,49 @@ function DelegationRow(props) {
|
|
|
1805
1805
|
return _el$13;
|
|
1806
1806
|
})();
|
|
1807
1807
|
}
|
|
1808
|
-
/** Plugin-level live delegation store shared
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
|
|
1808
|
+
/** Plugin-level live delegation store shared by event subscriptions and View.
|
|
1809
|
+
* Mutations notify subscribers so the View can refresh its durable child
|
|
1810
|
+
* list while also reading the map as an optimistic live source. */
|
|
1811
|
+
/** Create the observable live store shared by task events and the sidebar. */
|
|
1812
|
+
function createLiveDelegationStore() {
|
|
1813
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1814
|
+
return {
|
|
1815
|
+
map: /* @__PURE__ */ new Map(),
|
|
1816
|
+
subscribe(listener) {
|
|
1817
|
+
listeners.add(listener);
|
|
1818
|
+
return () => listeners.delete(listener);
|
|
1819
|
+
},
|
|
1820
|
+
bump() {
|
|
1821
|
+
for (const listener of listeners) listener();
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
/** Register the live task-part events that invalidate the sidebar store. */
|
|
1826
|
+
function registerLiveDelegationEvents(api, liveStore) {
|
|
1827
|
+
const unsubLive = [];
|
|
1828
|
+
try {
|
|
1829
|
+
unsubLive.push(api.event.on("message.part.updated", (event) => {
|
|
1830
|
+
const props = event?.properties ?? {};
|
|
1831
|
+
const part = props.part ?? props.info?.part;
|
|
1832
|
+
if (part === void 0) return;
|
|
1833
|
+
const sample = extractToolActivity(part);
|
|
1834
|
+
if (sample !== null) trackToolActivity(latestToolActivity, sample.sessionID, sample.activity);
|
|
1835
|
+
if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump();
|
|
1836
|
+
}));
|
|
1837
|
+
} catch {}
|
|
1838
|
+
try {
|
|
1839
|
+
unsubLive.push(api.event.on("message.part.removed", (event) => {
|
|
1840
|
+
const props = event?.properties ?? {};
|
|
1841
|
+
const partID = props.partID ?? props.part?.id;
|
|
1842
|
+
if (partID !== void 0 && removeDelegationEntry(liveStore.map, partID)) liveStore.bump();
|
|
1843
|
+
}));
|
|
1844
|
+
} catch {}
|
|
1845
|
+
return () => {
|
|
1846
|
+
for (const unsub of unsubLive) try {
|
|
1847
|
+
unsub();
|
|
1848
|
+
} catch {}
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1812
1851
|
function View(props) {
|
|
1813
1852
|
const [showSessions, setShowSessions] = createSignal(false);
|
|
1814
1853
|
const [showDelegations, setShowDelegations] = createSignal(true);
|
|
@@ -1938,11 +1977,12 @@ function View(props) {
|
|
|
1938
1977
|
cleanup.push(props.api.event.on("session.deleted", eventRefresh));
|
|
1939
1978
|
cleanup.push(props.api.event.on("session.status", eventRefresh));
|
|
1940
1979
|
} catch {}
|
|
1941
|
-
|
|
1942
|
-
props.liveStore.version();
|
|
1980
|
+
cleanup.push(props.liveStore.subscribe(() => {
|
|
1943
1981
|
eventRefreshCount += 1;
|
|
1944
1982
|
refreshDelegations();
|
|
1945
|
-
});
|
|
1983
|
+
}));
|
|
1984
|
+
eventRefreshCount += 1;
|
|
1985
|
+
refreshDelegations();
|
|
1946
1986
|
const poll = setInterval(() => {
|
|
1947
1987
|
setNow(Date.now());
|
|
1948
1988
|
refreshDelegations();
|
|
@@ -2169,34 +2209,10 @@ const tui = (api, _options, _meta) => {
|
|
|
2169
2209
|
const [version, setVersion] = createSignal(null);
|
|
2170
2210
|
detectVersion(api).then((detected) => setVersion(detected)).catch(() => setVersion(null));
|
|
2171
2211
|
setupUsageBar(api);
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
map: /* @__PURE__ */ new Map(),
|
|
2175
|
-
version: liveVersion,
|
|
2176
|
-
bump: () => setLiveVersion((v) => v + 1)
|
|
2177
|
-
};
|
|
2178
|
-
const unsubLive = [];
|
|
2179
|
-
try {
|
|
2180
|
-
unsubLive.push(api.event.on("message.part.updated", (event) => {
|
|
2181
|
-
const props = event?.properties ?? {};
|
|
2182
|
-
const part = props.part ?? props.info?.part;
|
|
2183
|
-
if (part === void 0) return;
|
|
2184
|
-
const sample = extractToolActivity(part);
|
|
2185
|
-
if (sample !== null) trackToolActivity(latestToolActivity, sample.sessionID, sample.activity);
|
|
2186
|
-
if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump();
|
|
2187
|
-
}));
|
|
2188
|
-
} catch {}
|
|
2189
|
-
try {
|
|
2190
|
-
unsubLive.push(api.event.on("message.part.removed", (event) => {
|
|
2191
|
-
const props = event?.properties ?? {};
|
|
2192
|
-
const partID = props.partID ?? props.part?.id;
|
|
2193
|
-
if (partID !== void 0 && removeDelegationEntry(liveStore.map, partID)) liveStore.bump();
|
|
2194
|
-
}));
|
|
2195
|
-
} catch {}
|
|
2212
|
+
const liveStore = createLiveDelegationStore();
|
|
2213
|
+
const unsubscribeLive = registerLiveDelegationEvents(api, liveStore);
|
|
2196
2214
|
api.lifecycle.onDispose(() => {
|
|
2197
|
-
|
|
2198
|
-
unsub();
|
|
2199
|
-
} catch {}
|
|
2215
|
+
unsubscribeLive();
|
|
2200
2216
|
liveStore.map.clear();
|
|
2201
2217
|
});
|
|
2202
2218
|
api.slots.register({
|
|
@@ -2221,6 +2237,6 @@ const plugin = {
|
|
|
2221
2237
|
setup: async () => {}
|
|
2222
2238
|
};
|
|
2223
2239
|
//#endregion
|
|
2224
|
-
export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
|
|
2240
|
+
export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, createLiveDelegationStore, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, registerLiveDelegationEvents, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
|
|
2225
2241
|
|
|
2226
2242
|
//# sourceMappingURL=tui.js.map
|