@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.
- package/CHANGELOG.md +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
|
+
import * as childProcess from "node:child_process";
|
|
3
|
+
import { createHmac } from "node:crypto";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as readline from "node:readline";
|
|
8
|
+
import type { Process as NativeProcess } from "@gajae-code/natives";
|
|
9
|
+
import { isCompiledBinary } from "@gajae-code/utils/env";
|
|
10
|
+
import { parseLinuxProcParentPid, parseLinuxProcStartTime, parseLinuxProcState } from "../gjc-runtime/linux-proc";
|
|
11
|
+
import { processIncarnation } from "../sdk/broker/process-incarnation";
|
|
12
|
+
import { fatalCatchableSignals, signalExitCode } from "./bash-shell-signals";
|
|
13
|
+
import {
|
|
14
|
+
BASH_SHELL_RUNTIME_ARG,
|
|
15
|
+
type BashShellWorkerRequest,
|
|
16
|
+
type BashShellWorkerResponse,
|
|
17
|
+
} from "./bash-shell-worker-protocol";
|
|
18
|
+
|
|
19
|
+
type NativeProcessBindings = { Process: typeof NativeProcess };
|
|
20
|
+
|
|
21
|
+
function darwinUniqueProcessId(pid: number, incarnation: string): string | undefined {
|
|
22
|
+
if (process.platform !== "darwin") return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const proc = dlopen("/usr/lib/libproc.dylib", {
|
|
25
|
+
proc_pidinfo: {
|
|
26
|
+
args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
|
|
27
|
+
returns: FFIType.i32,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const info = new Uint8Array(56);
|
|
31
|
+
const read = proc.symbols.proc_pidinfo(pid, 17, 0, ptr(info), info.byteLength);
|
|
32
|
+
proc.close();
|
|
33
|
+
if (read !== info.byteLength || processIncarnation(pid) !== incarnation) return undefined;
|
|
34
|
+
return new DataView(info.buffer).getBigUint64(16, true).toString();
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runtimeArgv(): string[] {
|
|
41
|
+
return isCompiledBinary()
|
|
42
|
+
? [process.execPath, BASH_SHELL_RUNTIME_ARG]
|
|
43
|
+
: [process.execPath, path.join(import.meta.dir, "bash-shell-runtime-entry.ts")];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function enableLinuxChildSubreaper(): void {
|
|
47
|
+
if (process.platform !== "linux") return;
|
|
48
|
+
try {
|
|
49
|
+
const libc = dlopen("libc.so.6", {
|
|
50
|
+
prctl: {
|
|
51
|
+
args: [FFIType.i32, FFIType.u64, FFIType.u64, FFIType.u64, FFIType.u64],
|
|
52
|
+
returns: FFIType.i32,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
libc.symbols.prctl(36, 1, 0, 0, 0);
|
|
56
|
+
libc.close();
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function reapLinuxSubreaperChildren(): Promise<void> {
|
|
61
|
+
if (process.platform !== "linux") return;
|
|
62
|
+
for (let wave = 0; wave < 3; wave++) {
|
|
63
|
+
await Bun.sleep(wave === 0 ? 5 : 25);
|
|
64
|
+
let entries: string[];
|
|
65
|
+
try {
|
|
66
|
+
entries = await fs.readdir("/proc");
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
72
|
+
const pid = Number.parseInt(entry, 10);
|
|
73
|
+
let stat: string;
|
|
74
|
+
try {
|
|
75
|
+
stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (parseLinuxProcParentPid(stat) !== process.pid) continue;
|
|
80
|
+
const startTime = parseLinuxProcStartTime(stat);
|
|
81
|
+
if (!startTime) continue;
|
|
82
|
+
try {
|
|
83
|
+
const current = await Bun.file(`/proc/${pid}/stat`).text();
|
|
84
|
+
if (parseLinuxProcStartTime(current) !== startTime || parseLinuxProcParentPid(current) !== process.pid)
|
|
85
|
+
continue;
|
|
86
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
87
|
+
const owned = Process.fromPid(pid);
|
|
88
|
+
if (owned?.incarnation === `linux:${startTime}`) owned.killTree(9);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function reapLinuxAdoptedZombies(excludedPid: number | undefined): Promise<void> {
|
|
95
|
+
if (process.platform !== "linux") return;
|
|
96
|
+
let entries: string[];
|
|
97
|
+
try {
|
|
98
|
+
entries = await fs.readdir("/proc");
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
104
|
+
const pid = Number.parseInt(entry, 10);
|
|
105
|
+
if (pid === excludedPid) continue;
|
|
106
|
+
let stat: string;
|
|
107
|
+
try {
|
|
108
|
+
stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (parseLinuxProcParentPid(stat) !== process.pid || parseLinuxProcState(stat) !== "Z") continue;
|
|
113
|
+
try {
|
|
114
|
+
const libc = dlopen("libc.so.6", {
|
|
115
|
+
waitpid: { args: [FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
116
|
+
});
|
|
117
|
+
const status = new Int32Array(1);
|
|
118
|
+
libc.symbols.waitpid(pid, ptr(status), 1);
|
|
119
|
+
libc.close();
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function signalFromExit(code: number | null, signal: NodeJS.Signals | null): NodeJS.Signals | null {
|
|
125
|
+
if (signal) return signal;
|
|
126
|
+
if (code === null || code <= 128) return null;
|
|
127
|
+
const signalNumber = code - 128;
|
|
128
|
+
const match = Object.entries(os.constants.signals).find(([, number]) => number === signalNumber);
|
|
129
|
+
return (match?.[0] as NodeJS.Signals | undefined) ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createUtf8LineDecoder(
|
|
133
|
+
onLine: (line: string) => void,
|
|
134
|
+
options?: { maxBufferedBytes?: number; onOverflow?: () => void },
|
|
135
|
+
): {
|
|
136
|
+
push(chunk: Uint8Array): void;
|
|
137
|
+
end(): void;
|
|
138
|
+
} {
|
|
139
|
+
const decoder = new TextDecoder();
|
|
140
|
+
let buffer = "";
|
|
141
|
+
let bufferedBytes = 0;
|
|
142
|
+
let overflowed = false;
|
|
143
|
+
// Native minimization supports 4 MiB each for original and transformed text.
|
|
144
|
+
// JSON escaping can expand every control byte to six ASCII bytes, so a 64 MiB
|
|
145
|
+
// envelope safely bounds the worst supported pair plus result metadata.
|
|
146
|
+
const maxBufferedBytes = options?.maxBufferedBytes ?? 64 * 1024 * 1024;
|
|
147
|
+
const drain = (): void => {
|
|
148
|
+
for (;;) {
|
|
149
|
+
const newline = buffer.indexOf("\n");
|
|
150
|
+
if (newline < 0) return;
|
|
151
|
+
const line = buffer.slice(0, newline);
|
|
152
|
+
bufferedBytes = Math.max(0, bufferedBytes - Buffer.byteLength(`${line}\n`));
|
|
153
|
+
onLine(line);
|
|
154
|
+
buffer = buffer.slice(newline + 1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
push(chunk) {
|
|
159
|
+
if (overflowed) return;
|
|
160
|
+
bufferedBytes += chunk.byteLength;
|
|
161
|
+
if (bufferedBytes > maxBufferedBytes) {
|
|
162
|
+
overflowed = true;
|
|
163
|
+
buffer = "";
|
|
164
|
+
options?.onOverflow?.();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
168
|
+
drain();
|
|
169
|
+
},
|
|
170
|
+
end() {
|
|
171
|
+
if (overflowed) return;
|
|
172
|
+
buffer += decoder.decode();
|
|
173
|
+
drain();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function parseAuthenticatedWorkerResponse(
|
|
179
|
+
line: string,
|
|
180
|
+
protocolToken: string | undefined,
|
|
181
|
+
): BashShellWorkerResponse | undefined {
|
|
182
|
+
if (!protocolToken) return undefined;
|
|
183
|
+
try {
|
|
184
|
+
const response = JSON.parse(line) as BashShellWorkerResponse;
|
|
185
|
+
return response.token === protocolToken ? response : undefined;
|
|
186
|
+
} catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function runBashShellSupervisor(): Promise<void> {
|
|
192
|
+
enableLinuxChildSubreaper();
|
|
193
|
+
for (const signal of fatalCatchableSignals()) {
|
|
194
|
+
try {
|
|
195
|
+
// This is a dedicated internal supervisor process. Remove the CLI's
|
|
196
|
+
// postmortem listeners before installing the ownership-preserving no-op;
|
|
197
|
+
// an appended listener cannot prevent an earlier handler from exiting.
|
|
198
|
+
process.removeAllListeners(signal);
|
|
199
|
+
process.on(signal, () => undefined);
|
|
200
|
+
} catch {
|
|
201
|
+
// The runtime may not expose every POSIX signal on every Unix target.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const [command, ...args] = runtimeArgv();
|
|
206
|
+
const worker = childProcess.spawn(command!, args, {
|
|
207
|
+
cwd: process.cwd(),
|
|
208
|
+
detached: false,
|
|
209
|
+
env: { ...process.env, GJC_ISOLATED_SHELL_RUNTIME: "1" },
|
|
210
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
});
|
|
213
|
+
const zombieReaper = setInterval(() => void reapLinuxAdoptedZombies(worker.pid), 25);
|
|
214
|
+
zombieReaper.unref();
|
|
215
|
+
const pendingRuns: number[] = [];
|
|
216
|
+
let terminating = false;
|
|
217
|
+
let protocolToken: string | undefined;
|
|
218
|
+
let ownershipLedgerPath: string | undefined;
|
|
219
|
+
|
|
220
|
+
const terminateRuntime = (): void => {
|
|
221
|
+
if (terminating) return;
|
|
222
|
+
terminating = true;
|
|
223
|
+
if (worker.exitCode === null && worker.signalCode === null) worker.kill("SIGKILL");
|
|
224
|
+
else process.exit(1);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
228
|
+
input.on("line", line => {
|
|
229
|
+
let request: BashShellWorkerRequest;
|
|
230
|
+
try {
|
|
231
|
+
request = JSON.parse(line) as BashShellWorkerRequest;
|
|
232
|
+
if (!protocolToken) protocolToken = request.token;
|
|
233
|
+
if (!request.token || request.token !== protocolToken) return;
|
|
234
|
+
if (request.type === "run") pendingRuns.push(request.id);
|
|
235
|
+
} catch {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (request.type === "init" && request.ownershipLedger && worker.pid) {
|
|
239
|
+
ownershipLedgerPath = request.ownershipLedger.path;
|
|
240
|
+
const runtimeIncarnation = processIncarnation(worker.pid);
|
|
241
|
+
if (!runtimeIncarnation) {
|
|
242
|
+
terminateRuntime();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const darwinUniqueId = darwinUniqueProcessId(worker.pid, runtimeIncarnation);
|
|
246
|
+
if (process.platform === "darwin" && !darwinUniqueId) {
|
|
247
|
+
terminateRuntime();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const payload = `${worker.pid}:${runtimeIncarnation}:${darwinUniqueId ?? ""}`;
|
|
251
|
+
const signature = createHmac("sha256", request.ownershipLedger.token).update(payload).digest("hex");
|
|
252
|
+
void fs
|
|
253
|
+
.appendFile(
|
|
254
|
+
request.ownershipLedger.path,
|
|
255
|
+
`${JSON.stringify({ pid: worker.pid, incarnation: runtimeIncarnation, darwinUniqueId, signature })}\n`,
|
|
256
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
257
|
+
)
|
|
258
|
+
.then(() => {
|
|
259
|
+
if (worker.stdin.writable) worker.stdin.write(`${line}\n`);
|
|
260
|
+
})
|
|
261
|
+
.catch(terminateRuntime);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (worker.stdin.writable) worker.stdin.write(`${line}\n`);
|
|
265
|
+
});
|
|
266
|
+
input.once("close", terminateRuntime);
|
|
267
|
+
|
|
268
|
+
const outputDecoder = createUtf8LineDecoder(
|
|
269
|
+
line => {
|
|
270
|
+
const response = parseAuthenticatedWorkerResponse(line, protocolToken);
|
|
271
|
+
if (!response) return;
|
|
272
|
+
if ((response.type === "result" || response.type === "error") && response.id !== undefined) {
|
|
273
|
+
const index = pendingRuns.indexOf(response.id);
|
|
274
|
+
if (index >= 0) pendingRuns.splice(index, 1);
|
|
275
|
+
}
|
|
276
|
+
const forwarded = response.type === "ready" ? { ...response, supervisorPid: process.pid } : response;
|
|
277
|
+
const writeForwarded = () => process.stdout.write(`${JSON.stringify(forwarded)}\n`);
|
|
278
|
+
if (response.type === "ready" && ownershipLedgerPath) {
|
|
279
|
+
const path = ownershipLedgerPath;
|
|
280
|
+
ownershipLedgerPath = undefined;
|
|
281
|
+
void fs.rm(path, { force: true }).then(writeForwarded).catch(terminateRuntime);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
writeForwarded();
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
onOverflow: () => {
|
|
288
|
+
if (protocolToken) {
|
|
289
|
+
const response = {
|
|
290
|
+
type: "error",
|
|
291
|
+
token: protocolToken,
|
|
292
|
+
message: "Shell worker protocol record exceeded the byte limit.",
|
|
293
|
+
} satisfies BashShellWorkerResponse;
|
|
294
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
295
|
+
}
|
|
296
|
+
terminateRuntime();
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
worker.stdout.on("data", chunk => outputDecoder.push(chunk));
|
|
301
|
+
worker.stdout.once("end", outputDecoder.end);
|
|
302
|
+
worker.stderr.pipe(process.stderr);
|
|
303
|
+
|
|
304
|
+
const exited = Promise.withResolvers<void>();
|
|
305
|
+
worker.once("error", error => {
|
|
306
|
+
if (!protocolToken) {
|
|
307
|
+
process.exit(1);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`${JSON.stringify({ type: "error", token: protocolToken, message: error.message } satisfies BashShellWorkerResponse)}\n`,
|
|
312
|
+
() => {
|
|
313
|
+
process.exit(1);
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
worker.once("exit", async (code, signal) => {
|
|
318
|
+
clearInterval(zombieReaper);
|
|
319
|
+
const resultSignal = signalFromExit(code, signal);
|
|
320
|
+
if (code === 0 && resultSignal === null) {
|
|
321
|
+
await reapLinuxSubreaperChildren();
|
|
322
|
+
// The runtime has already flushed its final protocol response and its
|
|
323
|
+
// reparented descendants have been reaped. Let the live guardian retire
|
|
324
|
+
// the remaining owned process group without any stale post-exit PGID use.
|
|
325
|
+
process.exit(0);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const activeRunId = pendingRuns[0];
|
|
329
|
+
if (!protocolToken) {
|
|
330
|
+
process.exit(1);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const response: BashShellWorkerResponse | undefined =
|
|
334
|
+
resultSignal && activeRunId !== undefined
|
|
335
|
+
? {
|
|
336
|
+
type: "result",
|
|
337
|
+
token: protocolToken,
|
|
338
|
+
id: activeRunId,
|
|
339
|
+
result: {
|
|
340
|
+
exitCode: signalExitCode(resultSignal),
|
|
341
|
+
cancelled: false,
|
|
342
|
+
timedOut: false,
|
|
343
|
+
signal: resultSignal,
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
: undefined;
|
|
347
|
+
if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
348
|
+
const retiring = {
|
|
349
|
+
type: "retiring",
|
|
350
|
+
token: protocolToken,
|
|
351
|
+
message: `Shell runtime exited with ${resultSignal ?? `code ${code ?? "unknown"}`}.`,
|
|
352
|
+
} satisfies BashShellWorkerResponse;
|
|
353
|
+
process.stdout.write(`${JSON.stringify(retiring)}\n`);
|
|
354
|
+
await reapLinuxSubreaperChildren();
|
|
355
|
+
process.exit(1);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await exited.promise;
|
|
359
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MinimizerOptions, ShellOptions, ShellRunOptions, ShellRunResult } from "@gajae-code/natives";
|
|
2
|
+
|
|
3
|
+
export const BASH_SHELL_WORKER_ARG = "--internal-bash-shell-worker";
|
|
4
|
+
export const BASH_SHELL_SUPERVISOR_ARG = "--internal-bash-shell-supervisor";
|
|
5
|
+
export const BASH_SHELL_RUNTIME_ARG = "--internal-bash-shell-runtime";
|
|
6
|
+
|
|
7
|
+
export type IsolatedShellOptions = Pick<ShellOptions, "sessionEnv" | "snapshotPath"> & {
|
|
8
|
+
minimizer?: MinimizerOptions;
|
|
9
|
+
containedProcessGroup?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type IsolatedShellRunOptions = Omit<ShellRunOptions, "signal">;
|
|
13
|
+
|
|
14
|
+
export type IsolatedShellRunResult = ShellRunResult & {
|
|
15
|
+
/** POSIX signal that terminated the isolated shell worker, when applicable. */
|
|
16
|
+
signal?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type BashShellWorkerRequest =
|
|
20
|
+
| {
|
|
21
|
+
type: "init";
|
|
22
|
+
token: string;
|
|
23
|
+
options?: IsolatedShellOptions;
|
|
24
|
+
ownershipLedger?: { path: string; token: string };
|
|
25
|
+
}
|
|
26
|
+
| { type: "run"; token: string; id: number; options: IsolatedShellRunOptions }
|
|
27
|
+
| { type: "abort"; token: string; id: number }
|
|
28
|
+
| { type: "close"; token: string; id: number };
|
|
29
|
+
|
|
30
|
+
export type BashShellWorkerResponse =
|
|
31
|
+
| { type: "ready"; token: string; supervisorPid?: number }
|
|
32
|
+
| { type: "chunk"; token: string; id: number; chunk: string }
|
|
33
|
+
| { type: "result"; token: string; id: number; result: IsolatedShellRunResult }
|
|
34
|
+
| { type: "void"; token: string; id: number }
|
|
35
|
+
| { type: "retiring"; token: string; message: string }
|
|
36
|
+
| { type: "error"; token: string; id?: number; message: string };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
import type { Shell as NativeShell, ShellOptions } from "@gajae-code/natives";
|
|
3
|
+
import { fatalCatchableSignals, signalExitCode } from "./bash-shell-signals";
|
|
4
|
+
import type { BashShellWorkerRequest, IsolatedShellRunResult } from "./bash-shell-worker-protocol";
|
|
5
|
+
|
|
6
|
+
type NativeShellConstructor = new (options?: ShellOptions | null) => NativeShell;
|
|
7
|
+
type NativeShellBindings = { Shell: NativeShellConstructor };
|
|
8
|
+
type BashShellWorkerResponsePayload =
|
|
9
|
+
| { type: "ready" }
|
|
10
|
+
| { type: "chunk"; id: number; chunk: string }
|
|
11
|
+
| { type: "result"; id: number; result: IsolatedShellRunResult }
|
|
12
|
+
| { type: "void"; id: number }
|
|
13
|
+
| { type: "error"; id?: number; message: string };
|
|
14
|
+
|
|
15
|
+
function restoreFatalSignalSemantics(): void {
|
|
16
|
+
for (const signal of fatalCatchableSignals()) {
|
|
17
|
+
const exitCode = signalExitCode(signal);
|
|
18
|
+
if (exitCode === undefined) continue;
|
|
19
|
+
try {
|
|
20
|
+
process.removeAllListeners(signal);
|
|
21
|
+
process.on(signal, () => process.exit(exitCode));
|
|
22
|
+
} catch {
|
|
23
|
+
// Unsupported signal handlers retain the platform's default disposition.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function errorMessage(error: unknown): string {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runBashShellWorker(): Promise<void> {
|
|
33
|
+
restoreFatalSignalSemantics();
|
|
34
|
+
let shell: NativeShell | undefined;
|
|
35
|
+
let initialized = false;
|
|
36
|
+
let protocolToken: string | undefined;
|
|
37
|
+
const writeResponse = (response: BashShellWorkerResponsePayload, callback?: () => void): void => {
|
|
38
|
+
if (!protocolToken) return;
|
|
39
|
+
process.stdout.write(`${JSON.stringify({ ...response, token: protocolToken })}\n`, callback);
|
|
40
|
+
};
|
|
41
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
42
|
+
|
|
43
|
+
input.on("line", line => {
|
|
44
|
+
let request: BashShellWorkerRequest;
|
|
45
|
+
try {
|
|
46
|
+
request = JSON.parse(line) as BashShellWorkerRequest;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
writeResponse({ type: "error", message: `Invalid shell worker request: ${errorMessage(error)}` });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (request.type === "init") {
|
|
53
|
+
protocolToken = request.token;
|
|
54
|
+
if (initialized) {
|
|
55
|
+
writeResponse({ type: "error", message: "Shell worker was initialized more than once." });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const { Shell } = require("@gajae-code/natives") as NativeShellBindings;
|
|
60
|
+
const shellOptions = {
|
|
61
|
+
...request.options,
|
|
62
|
+
containedProcessGroup: process.platform !== "win32",
|
|
63
|
+
ownershipLedgerPath: request.ownershipLedger?.path,
|
|
64
|
+
ownershipLedgerToken: request.ownershipLedger?.token,
|
|
65
|
+
} as ShellOptions;
|
|
66
|
+
shell = new Shell(shellOptions);
|
|
67
|
+
initialized = true;
|
|
68
|
+
writeResponse({ type: "ready" });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
writeResponse({ type: "error", message: errorMessage(error) }, () => process.exit(1));
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!protocolToken || request.token !== protocolToken) {
|
|
75
|
+
writeResponse({
|
|
76
|
+
type: "error",
|
|
77
|
+
id: "id" in request ? request.id : undefined,
|
|
78
|
+
message: "Invalid protocol token.",
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!shell) {
|
|
84
|
+
writeResponse({ type: "error", id: request.id, message: "Shell worker is not initialized." });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (request.type === "run") {
|
|
89
|
+
void shell
|
|
90
|
+
.run(request.options, (error, chunk) => {
|
|
91
|
+
if (!error) writeResponse({ type: "chunk", id: request.id, chunk });
|
|
92
|
+
})
|
|
93
|
+
.then(result => writeResponse({ type: "result", id: request.id, result }))
|
|
94
|
+
.catch(error => writeResponse({ type: "error", id: request.id, message: errorMessage(error) }));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (request.type === "abort") {
|
|
99
|
+
void shell
|
|
100
|
+
.abort()
|
|
101
|
+
.then(() => writeResponse({ type: "void", id: request.id }))
|
|
102
|
+
.catch(error => writeResponse({ type: "error", id: request.id, message: errorMessage(error) }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
void shell
|
|
107
|
+
.close()
|
|
108
|
+
.then(() => writeResponse({ type: "void", id: request.id }, () => process.exit(0)))
|
|
109
|
+
.catch(error => writeResponse({ type: "error", id: request.id, message: errorMessage(error) }));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const closed = Promise.withResolvers<void>();
|
|
113
|
+
input.once("close", closed.resolve);
|
|
114
|
+
await closed.promise;
|
|
115
|
+
await shell?.close().catch(() => undefined);
|
|
116
|
+
}
|