@gajae-code/coding-agent 0.16.4 → 0.16.6

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -0,0 +1,393 @@
1
+ import * as childProcess from "node:child_process";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import * as readline from "node:readline";
6
+ import type { ShellRunOptions } from "@gajae-code/natives";
7
+ import { isCompiledBinary } from "@gajae-code/utils/env";
8
+ import {
9
+ BASH_SHELL_WORKER_ARG,
10
+ type BashShellWorkerRequest,
11
+ type BashShellWorkerResponse,
12
+ type IsolatedShellOptions,
13
+ type IsolatedShellRunResult,
14
+ } from "./bash-shell-worker-protocol";
15
+
16
+ const CLOSE_TIMEOUT_MS = 1_000;
17
+ const STDERR_LIMIT = 8 * 1024;
18
+
19
+ type PendingRun = {
20
+ kind: "run";
21
+ resolve: (result: IsolatedShellRunResult) => void;
22
+ reject: (error: Error) => void;
23
+ onChunk?: (error: Error | null, chunk: string) => void;
24
+ removeAbortListener?: () => void;
25
+ };
26
+
27
+ type PendingVoid = {
28
+ kind: "void";
29
+ resolve: () => void;
30
+ reject: (error: Error) => void;
31
+ };
32
+
33
+ type PendingRequest = PendingRun | PendingVoid;
34
+
35
+ export interface IsolatedShellLifecycle {
36
+ onTerminal?: (shell: IsolatedShell) => void;
37
+ workerArgv?: string[];
38
+ }
39
+
40
+ function workerArgv(): string[] {
41
+ return isCompiledBinary()
42
+ ? [process.execPath, BASH_SHELL_WORKER_ARG]
43
+ : [process.execPath, path.join(import.meta.dir, "bash-shell-worker-entry.ts")];
44
+ }
45
+
46
+ function signalExitCode(signal: NodeJS.Signals): number | undefined {
47
+ const number = os.constants.signals[signal];
48
+ return typeof number === "number" ? 128 + number : undefined;
49
+ }
50
+
51
+ function signalFromExitCode(code: number | null): NodeJS.Signals | null {
52
+ if (code === null || code <= 128) return null;
53
+ const signalNumber = code - 128;
54
+ const match = Object.entries(os.constants.signals).find(([, number]) => number === signalNumber);
55
+ return (match?.[0] as NodeJS.Signals | undefined) ?? null;
56
+ }
57
+
58
+ function workerExitError(code: number | null, signal: NodeJS.Signals | null, stderr: string): Error {
59
+ const status = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
60
+ const diagnostic = stderr.trim();
61
+ return new Error(`Isolated shell worker exited with ${status}${diagnostic ? `: ${diagnostic}` : ""}`);
62
+ }
63
+
64
+ export class IsolatedShell {
65
+ #child: childProcess.ChildProcessWithoutNullStreams;
66
+ #ready = Promise.withResolvers<void>();
67
+ #exited = Promise.withResolvers<void>();
68
+ #pending = new Map<number, PendingRequest>();
69
+ #nextId = 1;
70
+ #protocolToken = crypto.randomUUID();
71
+ #supervisorPid: number | undefined;
72
+ #runTail: Promise<void> = Promise.resolve();
73
+ #activeRunSignals = new WeakSet<AbortSignal>();
74
+ #dispatchedRunSignals = new WeakSet<AbortSignal>();
75
+ #abortPromise: Promise<void> | undefined;
76
+ #closed = false;
77
+ #terminalNotified = false;
78
+ #stderr = "";
79
+ #lifecycle: IsolatedShellLifecycle;
80
+
81
+ constructor(options?: IsolatedShellOptions, lifecycle: IsolatedShellLifecycle = {}) {
82
+ this.#lifecycle = lifecycle;
83
+ const [command, ...args] = lifecycle.workerArgv ?? workerArgv();
84
+ this.#child = childProcess.spawn(command!, args, {
85
+ cwd: process.cwd(),
86
+ detached: process.platform !== "win32",
87
+ env: process.env,
88
+ stdio: ["pipe", "pipe", "pipe"],
89
+ windowsHide: true,
90
+ });
91
+
92
+ const output = readline.createInterface({ input: this.#child.stdout, crlfDelay: Infinity });
93
+ output.on("line", line => this.#handleLine(line));
94
+ this.#child.stderr.on("data", chunk => {
95
+ this.#stderr = `${this.#stderr}${String(chunk)}`.slice(-STDERR_LIMIT);
96
+ });
97
+ this.#child.once("error", error => {
98
+ this.#terminal(error);
99
+ this.#terminateWorker();
100
+ });
101
+ // `close` fires only after stdout/stderr are drained, so a supervisor's
102
+ // flushed signal result is processed before terminal fallback settlement.
103
+ this.#child.once("close", (code, signal) => {
104
+ this.#exited.resolve();
105
+ this.#handleExit(code, signal);
106
+ });
107
+ this.#send({ type: "init", token: this.#protocolToken, options });
108
+ }
109
+
110
+ isTerminal(): boolean {
111
+ return this.#terminalNotified;
112
+ }
113
+
114
+ async ready(): Promise<void> {
115
+ await this.#ready.promise;
116
+ }
117
+
118
+ supervisorPid(): number | undefined {
119
+ return this.#supervisorPid ?? this.#child.pid;
120
+ }
121
+
122
+ isRunSignalActive(signal: AbortSignal): boolean {
123
+ return this.#activeRunSignals.has(signal);
124
+ }
125
+
126
+ wasRunSignalDispatched(signal: AbortSignal): boolean {
127
+ return this.#dispatchedRunSignals.has(signal);
128
+ }
129
+
130
+ async run(
131
+ options: ShellRunOptions,
132
+ onChunk?: (error: Error | null, chunk: string) => void,
133
+ ): Promise<IsolatedShellRunResult> {
134
+ const predecessor = this.#runTail;
135
+ const admission = Promise.withResolvers<void>();
136
+ this.#runTail = admission.promise;
137
+ try {
138
+ await predecessor;
139
+ await this.#ready.promise;
140
+ if (this.#closed) throw new Error("Isolated shell worker is closed.");
141
+ if (options.signal instanceof AbortSignal && options.signal.aborted) {
142
+ return { exitCode: undefined, cancelled: true, timedOut: false };
143
+ }
144
+ if (options.signal instanceof AbortSignal) this.#activeRunSignals.add(options.signal);
145
+ const id = this.#nextId++;
146
+ const deferred = Promise.withResolvers<IsolatedShellRunResult>();
147
+ const pending: PendingRun = { kind: "run", resolve: deferred.resolve, reject: deferred.reject, onChunk };
148
+ if (options.signal instanceof AbortSignal) {
149
+ const signal = options.signal;
150
+ const abort = () => void this.abort().catch(() => undefined);
151
+ signal.addEventListener("abort", abort, { once: true });
152
+ pending.removeAbortListener = () => signal.removeEventListener("abort", abort);
153
+ }
154
+ this.#pending.set(id, pending);
155
+ const { signal: _signal, ...workerOptions } = options;
156
+ if (options.signal instanceof AbortSignal) this.#dispatchedRunSignals.add(options.signal);
157
+ this.#send({ type: "run", token: this.#protocolToken, id, options: workerOptions });
158
+ return await deferred.promise;
159
+ } finally {
160
+ if (options.signal instanceof AbortSignal) this.#activeRunSignals.delete(options.signal);
161
+ admission.resolve();
162
+ }
163
+ }
164
+
165
+ async abort(): Promise<void> {
166
+ if (this.#closed) return;
167
+ this.#abortPromise ??= this.#ready.promise
168
+ .then(() => this.#requestVoid("abort"))
169
+ .finally(() => {
170
+ this.#abortPromise = undefined;
171
+ });
172
+ await this.#abortPromise;
173
+ }
174
+
175
+ async close(): Promise<void> {
176
+ if (this.#closed) return;
177
+ this.#closed = true;
178
+ try {
179
+ const gracefulClose = this.#ready.promise.then(() => this.#requestVoid("close")).catch(() => undefined);
180
+ const graceful = await Promise.race([
181
+ gracefulClose.then(() => true),
182
+ Bun.sleep(CLOSE_TIMEOUT_MS).then(() => false),
183
+ ]);
184
+ if (graceful) await Promise.race([this.#exited.promise, Bun.sleep(CLOSE_TIMEOUT_MS)]);
185
+ } finally {
186
+ this.#terminateWorker();
187
+ this.#terminal(new Error("Isolated shell worker closed."));
188
+ }
189
+ }
190
+
191
+ async #requestVoid(type: "abort" | "close"): Promise<void> {
192
+ if (this.#child.exitCode !== null || this.#child.signalCode !== null) return;
193
+ const id = this.#nextId++;
194
+ const deferred = Promise.withResolvers<void>();
195
+ this.#pending.set(id, { kind: "void", resolve: deferred.resolve, reject: deferred.reject });
196
+ this.#send({ type, token: this.#protocolToken, id });
197
+ await deferred.promise;
198
+ }
199
+
200
+ #send(request: BashShellWorkerRequest): void {
201
+ if (!this.#child.stdin.writable) throw new Error("Isolated shell worker input is closed.");
202
+ this.#child.stdin.write(`${JSON.stringify(request)}\n`);
203
+ }
204
+
205
+ #handleLine(line: string): void {
206
+ let response: BashShellWorkerResponse;
207
+ try {
208
+ response = JSON.parse(line) as BashShellWorkerResponse;
209
+ } catch {
210
+ this.#terminal(new Error(`Isolated shell worker emitted invalid protocol output: ${line}`));
211
+ this.#terminateWorker();
212
+ return;
213
+ }
214
+ if (response.token !== this.#protocolToken) {
215
+ this.#terminal(new Error("Isolated shell worker emitted an unauthenticated protocol record."));
216
+ this.#terminateWorker();
217
+ return;
218
+ }
219
+ if (response.type === "ready") {
220
+ this.#supervisorPid = response.supervisorPid;
221
+ this.#ready.resolve();
222
+ return;
223
+ }
224
+ if (response.type === "retiring") {
225
+ this.#terminal(new Error(response.message));
226
+ return;
227
+ }
228
+ if (response.type === "error") {
229
+ if (response.id === undefined) {
230
+ this.#terminal(new Error(response.message));
231
+ this.#terminateWorker();
232
+ return;
233
+ }
234
+ const pending = this.#pending.get(response.id);
235
+ if (!pending) return;
236
+ this.#pending.delete(response.id);
237
+ if (pending.kind === "run") pending.removeAbortListener?.();
238
+ pending.reject(new Error(response.message));
239
+ return;
240
+ }
241
+ if (response.type === "chunk") {
242
+ const pending = this.#pending.get(response.id);
243
+ if (pending?.kind === "run") pending.onChunk?.(null, response.chunk);
244
+ return;
245
+ }
246
+ const pending = this.#pending.get(response.id);
247
+ if (!pending) return;
248
+ this.#pending.delete(response.id);
249
+ if (pending.kind === "run") pending.removeAbortListener?.();
250
+ if (response.type === "result" && pending.kind === "run") {
251
+ if (response.result.signal) {
252
+ this.#closed = true;
253
+ this.#terminal(new Error(`Isolated shell worker retired after ${response.result.signal}.`));
254
+ }
255
+ pending.resolve(response.result);
256
+ } else if (response.type === "void" && pending.kind === "void") {
257
+ pending.resolve();
258
+ } else {
259
+ pending.reject(new Error("Isolated shell worker returned a mismatched response."));
260
+ }
261
+ }
262
+
263
+ #handleExit(code: number | null, signal: NodeJS.Signals | null): void {
264
+ this.#closed = true;
265
+ const activeRun = [...this.#pending.entries()].find(([, pending]) => pending.kind === "run");
266
+ const resultSignal = signal ?? (activeRun ? signalFromExitCode(code) : null);
267
+ if (resultSignal && activeRun) {
268
+ const [id, pending] = activeRun;
269
+ this.#pending.delete(id);
270
+ if (pending.kind === "run") {
271
+ pending.removeAbortListener?.();
272
+ pending.resolve({
273
+ exitCode: signalExitCode(resultSignal),
274
+ cancelled: false,
275
+ timedOut: false,
276
+ signal: resultSignal,
277
+ });
278
+ }
279
+ }
280
+ this.#terminal(workerExitError(code, signal, this.#stderr));
281
+ }
282
+
283
+ #terminal(error: Error): void {
284
+ if (!this.#terminalNotified) {
285
+ this.#terminalNotified = true;
286
+ this.#lifecycle.onTerminal?.(this);
287
+ }
288
+ this.#failAll(error);
289
+ }
290
+
291
+ #failAll(error: Error): void {
292
+ this.#ready.reject(error);
293
+ for (const pending of this.#pending.values()) {
294
+ if (pending.kind === "run") pending.removeAbortListener?.();
295
+ pending.reject(error);
296
+ }
297
+ this.#pending.clear();
298
+ }
299
+
300
+ #terminateWorker(): void {
301
+ if (this.#child.exitCode === null && this.#child.signalCode === null) {
302
+ if (process.platform !== "win32" && this.#supervisorPid) {
303
+ try {
304
+ process.kill(-this.#supervisorPid, "SIGKILL");
305
+ } catch {}
306
+ }
307
+ this.#child.kill("SIGKILL");
308
+ }
309
+ }
310
+ }
311
+
312
+ export async function smokeTestIsolatedShell(): Promise<void> {
313
+ if (process.platform === "win32") return;
314
+ const persistent = new IsolatedShell();
315
+ try {
316
+ await persistent.run({ command: "export GJC_SHELL_SMOKE=ok", timeoutMs: 5_000 });
317
+ const result = await persistent.run({ command: 'printf "%s" "$GJC_SHELL_SMOKE"', timeoutMs: 5_000 });
318
+ if (result.exitCode !== 0) throw new Error(`isolated shell smoke exited ${result.exitCode}`);
319
+ } finally {
320
+ await persistent.close();
321
+ }
322
+
323
+ const markerId = crypto.randomUUID();
324
+ const pidFile = path.join(os.tmpdir(), `gjc-shell-smoke-${markerId}.pid`);
325
+ const readyFile = path.join(os.tmpdir(), `gjc-shell-smoke-${markerId}.ready`);
326
+ const runtimePidFile = path.join(os.tmpdir(), `gjc-shell-smoke-${markerId}.runtime`);
327
+ const quotedPidFile = `'${pidFile.replaceAll("'", "'\\''")}'`;
328
+ const quotedReadyFile = `'${readyFile.replaceAll("'", "'\\''")}'`;
329
+ const quotedRuntimePidFile = `'${runtimePidFile.replaceAll("'", "'\\''")}'`;
330
+ let descendantPid: number | undefined;
331
+ let runtimePid: number | undefined;
332
+ try {
333
+ const signalled = new IsolatedShell();
334
+ // The descendant marker is written with content: the in-shell wait loop
335
+ // below tests `-s` (non-empty), so an empty marker would never satisfy it
336
+ // and the command would always burn its whole timeout instead of parking
337
+ // in the intended `sleep`. The budget is generous because this runs as a
338
+ // release smoke check on cold hosts (a first-boot Intel macOS runner
339
+ // needed longer than 5s just to start the worker/supervisor/runtime
340
+ // chain) — the smoke still kills the runtime long before it elapses.
341
+ const runPromise = signalled.run({
342
+ command: `echo $$ > ${quotedRuntimePidFile}; /bin/sh -c 'trap "" TERM; echo $$ > "$1"; echo ready > "$2"; sleep 5' sh ${quotedPidFile} ${quotedReadyFile} & while [ ! -s ${quotedReadyFile} ]; do sleep 0.01; done; sleep 30`,
343
+ timeoutMs: 60_000,
344
+ });
345
+ const readyDeadline = Date.now() + 30_000;
346
+ const descendantReady = (): boolean => Bun.file(readyFile).size > 0;
347
+ while (Date.now() < readyDeadline && !descendantReady()) await Bun.sleep(25);
348
+ if (!descendantReady()) throw new Error("isolated shell smoke descendant did not become ready");
349
+ descendantPid = Number.parseInt(await Bun.file(pidFile).text(), 10);
350
+ runtimePid = Number.parseInt(await Bun.file(runtimePidFile).text(), 10);
351
+ const supervisorPid = signalled.supervisorPid();
352
+ if (!supervisorPid) throw new Error("isolated shell smoke supervisor has no pid");
353
+ process.kill(supervisorPid, "SIGTERM");
354
+ await Bun.sleep(25);
355
+ process.kill(runtimePid, "SIGKILL");
356
+ const result = await runPromise;
357
+ if (result.exitCode !== 137 || result.signal !== "SIGKILL" || result.cancelled) {
358
+ throw new Error(`isolated shell signal smoke failed: ${JSON.stringify(result)}`);
359
+ }
360
+ let descendantGone = false;
361
+ // Reaping an ignored-SIGTERM descendant goes through the supervisor's
362
+ // group escalation, which a loaded or cold host can take seconds to
363
+ // complete; a one-second window turned that latency into a failure.
364
+ const reapDeadline = Date.now() + 15_000;
365
+ while (Date.now() < reapDeadline) {
366
+ try {
367
+ process.kill(descendantPid, 0);
368
+ await Bun.sleep(25);
369
+ } catch {
370
+ descendantGone = true;
371
+ break;
372
+ }
373
+ }
374
+ if (!descendantGone) throw new Error(`isolated shell smoke left descendant ${descendantPid} alive`);
375
+ } finally {
376
+ await Promise.all([
377
+ fs.rm(pidFile, { force: true }),
378
+ fs.rm(readyFile, { force: true }),
379
+ fs.rm(runtimePidFile, { force: true }),
380
+ ]);
381
+ }
382
+
383
+ const userSignal = new IsolatedShell();
384
+ const userSignalResult = await userSignal.run({ command: "kill -USR1 $$", timeoutMs: 5_000 });
385
+ const expectedUserSignalExit = 128 + os.constants.signals.SIGUSR1;
386
+ if (
387
+ userSignalResult.exitCode !== expectedUserSignalExit ||
388
+ userSignalResult.signal !== "SIGUSR1" ||
389
+ userSignalResult.cancelled
390
+ ) {
391
+ throw new Error(`isolated shell SIGUSR1 smoke failed: ${JSON.stringify(userSignalResult)}`);
392
+ }
393
+ }
@@ -0,0 +1,73 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { type FunctionHookRegistration, validateFunctionHookTarget } from "./function-hooks";
4
+
5
+ export type TaggedFunctionHookHandler = (...args: unknown[]) => Promise<unknown>;
6
+
7
+ const registrations = new WeakMap<TaggedFunctionHookHandler, FunctionHookRegistration>();
8
+ const registrationOrders = new WeakMap<(...args: never[]) => unknown, number>();
9
+
10
+ export function tagExtensionHandlerRegistrationOrder<T extends (...args: never[]) => unknown>(
11
+ handler: T,
12
+ registrationOrder: number,
13
+ ): T {
14
+ registrationOrders.set(handler, registrationOrder);
15
+ return handler;
16
+ }
17
+
18
+ export function wrapExtensionHandlerRegistration<T extends (...args: never[]) => unknown>(
19
+ handler: T,
20
+ registrationOrder: number,
21
+ ): T {
22
+ const registered = ((...args: never[]) => handler(...args)) as T;
23
+ registrationOrders.set(registered, registrationOrder);
24
+ return registered;
25
+ }
26
+
27
+ export function getExtensionHandlerRegistrationOrder(value: unknown): number | undefined {
28
+ return typeof value === "function" ? registrationOrders.get(value as (...args: never[]) => unknown) : undefined;
29
+ }
30
+
31
+ export function tagFunctionHookHandler(registration: FunctionHookRegistration): TaggedFunctionHookHandler {
32
+ if (registration.event === "*" && registration.target !== undefined)
33
+ throw new Error("Wildcard function hooks cannot declare a target");
34
+ if (registration.target !== undefined && registration.target !== "*")
35
+ validateFunctionHookTarget(registration.target);
36
+ const tagged: TaggedFunctionHookHandler = async () => undefined;
37
+ registrations.set(
38
+ tagged,
39
+ Object.freeze({
40
+ ...registration,
41
+ grant: Object.freeze(registration.grant),
42
+ provenance: Object.freeze({ ...registration.provenance }),
43
+ }),
44
+ );
45
+ return tagged;
46
+ }
47
+
48
+ export function getFunctionHookRegistration(value: unknown): FunctionHookRegistration | undefined {
49
+ return typeof value === "function" ? registrations.get(value as TaggedFunctionHookHandler) : undefined;
50
+ }
51
+
52
+ /** Host-only root-confined read used by the mediated filesystem capability. */
53
+ export async function readConstrainedFunctionHookFile(
54
+ filePath: string,
55
+ cwd: string,
56
+ roots: readonly string[],
57
+ ): Promise<string> {
58
+ if (roots.length === 0) throw new Error("Function hook filesystem.read has no declared root");
59
+ const candidateReal = await fs.realpath(path.resolve(cwd, filePath));
60
+ for (const root of roots) {
61
+ const rootReal = await fs.realpath(path.resolve(cwd, root));
62
+ const relative = path.relative(rootReal, candidateReal);
63
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
64
+ const handle = await fs.open(candidateReal, "r");
65
+ try {
66
+ return (await handle.readFile("utf8")).slice(0, 1_000_000);
67
+ } finally {
68
+ await handle.close();
69
+ }
70
+ }
71
+ }
72
+ throw new Error("Function hook filesystem path is outside its declared grant");
73
+ }