pantheon-opencode 1.2.1 → 1.3.0
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 +10 -0
- package/README.md +53 -15
- package/bin/pantheon-init.mjs +14 -0
- package/opencode.json +17 -1
- package/package.json +13 -4
- package/scripts/changelog-extract.mjs +112 -0
- package/scripts/doctor.mjs +303 -13
- package/scripts/hooks/audit-imports.sh +43 -0
- package/scripts/hooks/format-multi-language.sh +96 -0
- package/scripts/hooks/log-session-start.sh +42 -0
- package/scripts/hooks/on-subagent-delegation-start.sh +201 -0
- package/scripts/hooks/on-subagent-delegation-stop.sh +419 -0
- package/scripts/hooks/run-type-check.sh +21 -0
- package/scripts/hooks/scan-secrets.sh +97 -0
- package/scripts/hooks/validate-post-conditions.sh +21 -0
- package/scripts/hooks/validate-talos-scope.sh +50 -0
- package/scripts/hooks/validate-tool-safety.sh +46 -0
- package/scripts/install/opencode.mjs +58 -29
- package/scripts/install/plugin.mjs +4 -2
- package/scripts/install-mcp.mjs +104 -56
- package/scripts/mcp_persistence_server.py +1 -0
- package/scripts/prune.mjs +359 -0
- package/scripts/version-check.mjs +146 -0
- package/scripts/versioning.mjs +13 -14
- package/src/agents/zeus.md +33 -5
- package/src/mcp/requirements-mcp.txt +8 -5
- package/src/mcp/requirements-vision.txt +3 -3
- package/src/plugins/hook-runner.ts +186 -0
- package/src/plugins/pantheon-hooks.ts +1503 -0
- package/src/plugins/tui/dist/tui.d.ts +8 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui.js +1198 -0
- package/src/plugins/tui/dist/tui.js.map +1 -0
- package/src/plugins/tui/dist/tui.tsx +1204 -0
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +16 -1
- package/src/mcp/requirements-mcp-core.txt +0 -5
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-runner.ts — version-proof runner for the Pantheon security-hook layer.
|
|
3
|
+
*
|
|
4
|
+
* The previous plugin used the Bun Shell `$` injected by the opencode plugin
|
|
5
|
+
* API (`$.quiet().nothrow().timeout(...)`). `.timeout()` does not exist on the
|
|
6
|
+
* BunShell interface, so every hook crashed with a TypeError and the layer was
|
|
7
|
+
* removed (24-25/07). This runner uses ONLY `node:child_process` — stable
|
|
8
|
+
* across every Node >= 18 runtime and every opencode plugin loader version.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code stdin protocol: the hook scripts receive
|
|
11
|
+
* `{tool_name, tool_input, agent_id, session_id}` as JSON on stdin. Scripts
|
|
12
|
+
* that read env vars instead (log-session-start.sh, on-subagent-delegation-*)
|
|
13
|
+
* get their fields mapped into the child env (SESSION_ID / AGENT_NAME /
|
|
14
|
+
* TASK_DESC) so both protocols work end to end.
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT (verified empirically): the `input:` option on the async
|
|
17
|
+
* `execFile()` API is silently ignored by Node — it only exists on
|
|
18
|
+
* spawnSync/execFileSync. This runner therefore writes stdin explicitly to a
|
|
19
|
+
* spawned child, which delivers the payload reliably.
|
|
20
|
+
*
|
|
21
|
+
* Hooks NEVER throw: every failure (missing script, timeout, non-zero exit)
|
|
22
|
+
* is returned as a structured HookResult so the opencode session can never be
|
|
23
|
+
* taken down by the security layer.
|
|
24
|
+
*/
|
|
25
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import type { Readable } from 'node:stream'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
|
|
30
|
+
export const HOOK_TIMEOUT_MS = 15_000
|
|
31
|
+
export const HOOK_MAX_BUFFER = 10 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
/** Claude Code hook protocol payload. */
|
|
34
|
+
export type HookPayload = {
|
|
35
|
+
tool_name?: string
|
|
36
|
+
tool_input?: unknown
|
|
37
|
+
agent_id?: string
|
|
38
|
+
session_id?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type HookResult = {
|
|
42
|
+
code: number
|
|
43
|
+
stdout: string
|
|
44
|
+
stderr: string
|
|
45
|
+
signal: NodeJS.Signals | null
|
|
46
|
+
timedOut: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type RunHookOptions = {
|
|
50
|
+
env?: Record<string, string | undefined>
|
|
51
|
+
cwd?: string
|
|
52
|
+
timeout?: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the hooks directory relative to this module — robust to any npm
|
|
57
|
+
* global-install prefix because it anchors on import.meta.url, not __dirname
|
|
58
|
+
* or process.cwd().
|
|
59
|
+
*/
|
|
60
|
+
export function resolveHooksDir(): string {
|
|
61
|
+
return fileURLToPath(new URL('../../scripts/hooks/', import.meta.url))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Map the Claude Code protocol payload into the child environment. Scripts
|
|
66
|
+
* that read env vars (log-session-start.sh, delegation audit scripts) rely on
|
|
67
|
+
* these; scripts that read stdin receive the full JSON via `input` in runHook.
|
|
68
|
+
* PATH is always preserved so `python3`/`bash` resolution inside the scripts
|
|
69
|
+
* is never affected by a stripped-down environment.
|
|
70
|
+
*/
|
|
71
|
+
export function hookEnv(
|
|
72
|
+
payload: HookPayload,
|
|
73
|
+
extra?: Record<string, string | undefined>,
|
|
74
|
+
): NodeJS.ProcessEnv {
|
|
75
|
+
return {
|
|
76
|
+
...process.env,
|
|
77
|
+
PATH: process.env.PATH ?? '',
|
|
78
|
+
SESSION_ID: payload.session_id ?? '',
|
|
79
|
+
AGENT_NAME: payload.agent_id ?? '',
|
|
80
|
+
TASK_DESC: payload.tool_name ?? '',
|
|
81
|
+
...(extra ?? {}),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run one hook script with the Claude Code protocol payload on stdin.
|
|
87
|
+
*
|
|
88
|
+
* @param script file name inside scripts/hooks/ (e.g. "validate-tool-safety.sh")
|
|
89
|
+
* @param payload protocol payload translated from the opencode hook event
|
|
90
|
+
* @param opts optional env overrides / cwd / timeout
|
|
91
|
+
* @returns HookResult — NEVER throws; resolve with a non-zero code instead
|
|
92
|
+
*/
|
|
93
|
+
export function runHook(
|
|
94
|
+
script: string,
|
|
95
|
+
payload: HookPayload,
|
|
96
|
+
opts: RunHookOptions = {},
|
|
97
|
+
): Promise<HookResult> {
|
|
98
|
+
const scriptPath = path.join(opts.cwd ?? resolveHooksDir(), script)
|
|
99
|
+
const timeout = opts.timeout ?? HOOK_TIMEOUT_MS
|
|
100
|
+
|
|
101
|
+
return new Promise<HookResult>((resolve) => {
|
|
102
|
+
const stdoutBuf: { value: string } = { value: '' }
|
|
103
|
+
const stderrBuf: { value: string } = { value: '' }
|
|
104
|
+
let settled = false
|
|
105
|
+
|
|
106
|
+
const settle = (result: HookResult) => {
|
|
107
|
+
if (!settled) {
|
|
108
|
+
settled = true
|
|
109
|
+
resolve(result)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const spawnOpts = {
|
|
114
|
+
timeout,
|
|
115
|
+
killSignal: 'SIGKILL' as const,
|
|
116
|
+
env: hookEnv(payload, opts.env),
|
|
117
|
+
// stdio:'pipe' is equivalent to ['pipe','pipe','pipe'] — it guarantees
|
|
118
|
+
// the child's stdin is OUR pipe, NEVER the opencode TUI's terminal
|
|
119
|
+
// (which would leave `cat`-style hooks blocked reading forever and hit
|
|
120
|
+
// the opencode core hook timeout).
|
|
121
|
+
stdio: 'pipe' as const,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// stdio:'pipe' picks the ChildProcessWithoutNullStreams overload — do not
|
|
127
|
+
// widen these option literals or child.stdin/stdout become nullable.
|
|
128
|
+
let child: ChildProcessWithoutNullStreams
|
|
129
|
+
try {
|
|
130
|
+
child = spawn(scriptPath, [], spawnOpts)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Synchronous spawn failure (invalid args etc.) — never propagate.
|
|
133
|
+
settle({
|
|
134
|
+
code: 1,
|
|
135
|
+
stdout: '',
|
|
136
|
+
stderr: `spawn failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
137
|
+
signal: null,
|
|
138
|
+
timedOut: false,
|
|
139
|
+
})
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// The child may exit before we write stdin — swallow EPIPE instead of
|
|
144
|
+
// crashing the plugin process with an unhandled stream error.
|
|
145
|
+
child.stdin.on('error', () => {})
|
|
146
|
+
|
|
147
|
+
const collect = (stream: Readable, buf: { value: string }) => {
|
|
148
|
+
stream.setEncoding('utf-8')
|
|
149
|
+
stream.on('data', (chunk: string) => {
|
|
150
|
+
if (buf.value.length < HOOK_MAX_BUFFER) {
|
|
151
|
+
buf.value += chunk.slice(0, HOOK_MAX_BUFFER - buf.value.length)
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
collect(child.stdout, stdoutBuf)
|
|
156
|
+
collect(child.stderr, stderrBuf)
|
|
157
|
+
|
|
158
|
+
child.on('error', (err: NodeJS.ErrnoException) => {
|
|
159
|
+
// ENOENT / EACCES — script missing or not executable. Report, never throw.
|
|
160
|
+
settle({
|
|
161
|
+
code: 1,
|
|
162
|
+
stdout: stdoutBuf.value,
|
|
163
|
+
stderr: stderrBuf.value || `hook script failed to spawn: ${err.message}`,
|
|
164
|
+
signal: null,
|
|
165
|
+
timedOut: false,
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
child.on('close', (code, signal) => {
|
|
170
|
+
settle({
|
|
171
|
+
code: code ?? 1,
|
|
172
|
+
stdout: stdoutBuf.value,
|
|
173
|
+
stderr: stderrBuf.value,
|
|
174
|
+
signal,
|
|
175
|
+
timedOut: code === null && signal === 'SIGKILL',
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Deliver the Claude Code protocol JSON on stdin. stdin.end() runs
|
|
180
|
+
// UNCONDITIONALLY — even for an empty payload {} or a script that never
|
|
181
|
+
// reads stdin — so the child can never block waiting for input and hit
|
|
182
|
+
// the runtime hook timeout (reported as "Hook X timed out after 30000ms").
|
|
183
|
+
child.stdin.write(JSON.stringify(payload))
|
|
184
|
+
child.stdin.end()
|
|
185
|
+
})
|
|
186
|
+
}
|