@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,388 @@
|
|
|
1
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
|
+
import * as childProcess from "node:child_process";
|
|
3
|
+
import { createHmac, randomUUID, timingSafeEqual } 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 { BASH_SHELL_SUPERVISOR_ARG, type BashShellWorkerRequest } from "./bash-shell-worker-protocol";
|
|
11
|
+
|
|
12
|
+
type NativeProcessBindings = { Process: typeof NativeProcess };
|
|
13
|
+
type OwnershipRecord = { pid: number; incarnation: string; darwinUniqueId?: string | null; signature: string };
|
|
14
|
+
|
|
15
|
+
export function parseOwnershipRecord(line: string): OwnershipRecord | undefined {
|
|
16
|
+
let value: unknown;
|
|
17
|
+
try {
|
|
18
|
+
value = JSON.parse(line);
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (!value || typeof value !== "object") return undefined;
|
|
23
|
+
const record = value as Partial<OwnershipRecord>;
|
|
24
|
+
if (
|
|
25
|
+
!Number.isSafeInteger(record.pid) ||
|
|
26
|
+
(record.pid ?? 0) <= 0 ||
|
|
27
|
+
typeof record.incarnation !== "string" ||
|
|
28
|
+
(record.darwinUniqueId !== undefined &&
|
|
29
|
+
record.darwinUniqueId !== null &&
|
|
30
|
+
(typeof record.darwinUniqueId !== "string" || !/^\d+$/.test(record.darwinUniqueId))) ||
|
|
31
|
+
typeof record.signature !== "string"
|
|
32
|
+
) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return record as OwnershipRecord;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function retainOwnedProcess<T extends Pick<NativeProcess, "pid" | "incarnation">>(
|
|
39
|
+
owned: Map<string, T>,
|
|
40
|
+
processRef: T,
|
|
41
|
+
guardianPid = process.pid,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (processRef.pid === guardianPid) return false;
|
|
44
|
+
owned.set(`${processRef.pid}:${processRef.incarnation}`, processRef);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extendOwnedDarwinAncestry<T extends { uniqueId: bigint; parentUniqueId: bigint }>(
|
|
49
|
+
knownUniqueIds: Set<bigint>,
|
|
50
|
+
candidates: ReadonlyMap<number, T>,
|
|
51
|
+
): number[] {
|
|
52
|
+
const addedPids: number[] = [];
|
|
53
|
+
let added = true;
|
|
54
|
+
while (added) {
|
|
55
|
+
added = false;
|
|
56
|
+
for (const [pid, identity] of candidates) {
|
|
57
|
+
if (knownUniqueIds.has(identity.uniqueId) || !knownUniqueIds.has(identity.parentUniqueId)) continue;
|
|
58
|
+
knownUniqueIds.add(identity.uniqueId);
|
|
59
|
+
addedPids.push(pid);
|
|
60
|
+
added = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return addedPids;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function authenticateOwnershipRecord(
|
|
67
|
+
line: string,
|
|
68
|
+
ledgerToken: string,
|
|
69
|
+
): { processRef?: NativeProcess; darwinUniqueId?: bigint } | undefined {
|
|
70
|
+
const record = parseOwnershipRecord(line);
|
|
71
|
+
if (!record) return undefined;
|
|
72
|
+
const uniqueId = record.darwinUniqueId ?? "";
|
|
73
|
+
const expected = createHmac("sha256", ledgerToken)
|
|
74
|
+
.update(`${record.pid}:${record.incarnation}:${uniqueId}`)
|
|
75
|
+
.digest();
|
|
76
|
+
let received: Buffer;
|
|
77
|
+
try {
|
|
78
|
+
received = Buffer.from(record.signature, "hex");
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (received.byteLength !== expected.byteLength || !timingSafeEqual(received, expected)) return undefined;
|
|
83
|
+
if (process.platform === "darwin" && !record.darwinUniqueId) return undefined;
|
|
84
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
85
|
+
const owned = Process.fromPid(record.pid);
|
|
86
|
+
return {
|
|
87
|
+
...(owned?.incarnation === record.incarnation ? { processRef: owned } : {}),
|
|
88
|
+
...(record.darwinUniqueId ? { darwinUniqueId: BigInt(record.darwinUniqueId) } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function enableLinuxChildSubreaper(): boolean {
|
|
93
|
+
if (process.platform !== "linux") return true;
|
|
94
|
+
try {
|
|
95
|
+
const libc = dlopen("libc.so.6", {
|
|
96
|
+
prctl: {
|
|
97
|
+
args: [FFIType.i32, FFIType.u64, FFIType.u64, FFIType.u64, FFIType.u64],
|
|
98
|
+
returns: FFIType.i32,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const enabled = libc.symbols.prctl(36, 1, 0, 0, 0) === 0;
|
|
102
|
+
libc.close();
|
|
103
|
+
return enabled;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function findDarwinLedgerHolders(device: number, inode: bigint): NativeProcess[] | undefined {
|
|
110
|
+
if (process.platform !== "darwin") return [];
|
|
111
|
+
try {
|
|
112
|
+
const proc = dlopen("/usr/lib/libproc.dylib", {
|
|
113
|
+
proc_listallpids: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
114
|
+
proc_pidfdinfo: {
|
|
115
|
+
args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.i32],
|
|
116
|
+
returns: FFIType.i32,
|
|
117
|
+
},
|
|
118
|
+
proc_pidinfo: {
|
|
119
|
+
args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
|
|
120
|
+
returns: FFIType.i32,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const capacity = proc.symbols.proc_listallpids(null, 0);
|
|
124
|
+
if (capacity <= 0) {
|
|
125
|
+
proc.close();
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const pids = new Int32Array(capacity + 64);
|
|
129
|
+
const count = proc.symbols.proc_listallpids(ptr(pids), pids.byteLength);
|
|
130
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
131
|
+
const matches: NativeProcess[] = [];
|
|
132
|
+
for (let index = 0; index < count; index++) {
|
|
133
|
+
const pid = pids[index]!;
|
|
134
|
+
const fdBytes = proc.symbols.proc_pidinfo(pid, 1, 0, null, 0);
|
|
135
|
+
if (fdBytes <= 0) continue;
|
|
136
|
+
const fds = new Uint8Array(fdBytes);
|
|
137
|
+
const readBytes = proc.symbols.proc_pidinfo(pid, 1, 0, ptr(fds), fds.byteLength);
|
|
138
|
+
const fdView = new DataView(fds.buffer);
|
|
139
|
+
for (let offset = 0; offset + 8 <= readBytes; offset += 8) {
|
|
140
|
+
const fd = fdView.getInt32(offset, true);
|
|
141
|
+
const type = fdView.getUint32(offset + 4, true);
|
|
142
|
+
if (type !== 1) continue; // PROX_FDTYPE_VNODE
|
|
143
|
+
const info = new Uint8Array(256);
|
|
144
|
+
const infoBytes = proc.symbols.proc_pidfdinfo(pid, fd, 1, ptr(info), info.byteLength);
|
|
145
|
+
if (infoBytes < 40) continue;
|
|
146
|
+
const infoView = new DataView(info.buffer);
|
|
147
|
+
if (infoView.getUint32(24, true) !== device || infoView.getBigUint64(32, true) !== inode) continue;
|
|
148
|
+
const processRef = Process.fromPid(pid);
|
|
149
|
+
if (processRef) matches.push(processRef);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
proc.close();
|
|
154
|
+
return matches;
|
|
155
|
+
} catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type DarwinAncestryTracker = {
|
|
161
|
+
seed(uniqueId: bigint): void;
|
|
162
|
+
track(processRef: NativeProcess, uniqueId?: bigint): boolean;
|
|
163
|
+
poll(): boolean;
|
|
164
|
+
close(): void;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinAncestryTracker | undefined {
|
|
168
|
+
if (process.platform !== "darwin") return undefined;
|
|
169
|
+
try {
|
|
170
|
+
const proc = dlopen("/usr/lib/libproc.dylib", {
|
|
171
|
+
proc_listallpids: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
172
|
+
proc_pidinfo: {
|
|
173
|
+
args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
|
|
174
|
+
returns: FFIType.i32,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const knownUniqueIds = new Set<bigint>();
|
|
178
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
179
|
+
const uniqueIdentity = (pid: number): { uniqueId: bigint; parentUniqueId: bigint } | undefined => {
|
|
180
|
+
const info = new Uint8Array(56);
|
|
181
|
+
if (proc.symbols.proc_pidinfo(pid, 17, 0, ptr(info), info.byteLength) !== info.byteLength) return undefined;
|
|
182
|
+
const view = new DataView(info.buffer);
|
|
183
|
+
return { uniqueId: view.getBigUint64(16, true), parentUniqueId: view.getBigUint64(24, true) };
|
|
184
|
+
};
|
|
185
|
+
const track = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
186
|
+
const identity = signedUniqueId === undefined ? uniqueIdentity(processRef.pid) : undefined;
|
|
187
|
+
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
188
|
+
if (uniqueId === undefined) return false;
|
|
189
|
+
knownUniqueIds.add(uniqueId);
|
|
190
|
+
retainOwnedProcess(owned, processRef);
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
seed(uniqueId) {
|
|
195
|
+
knownUniqueIds.add(uniqueId);
|
|
196
|
+
},
|
|
197
|
+
track,
|
|
198
|
+
poll() {
|
|
199
|
+
const capacity = proc.symbols.proc_listallpids(null, 0);
|
|
200
|
+
if (capacity <= 0) return false;
|
|
201
|
+
const pids = new Int32Array(capacity + 64);
|
|
202
|
+
const count = proc.symbols.proc_listallpids(ptr(pids), pids.byteLength);
|
|
203
|
+
if (count <= 0) return false;
|
|
204
|
+
const candidates = new Map<
|
|
205
|
+
number,
|
|
206
|
+
{ uniqueId: bigint; parentUniqueId: bigint; processRef: NativeProcess }
|
|
207
|
+
>();
|
|
208
|
+
for (let index = 0; index < count; index++) {
|
|
209
|
+
const pid = pids[index]!;
|
|
210
|
+
const processRef = Process.fromPid(pid);
|
|
211
|
+
if (!processRef) continue;
|
|
212
|
+
const identity = uniqueIdentity(pid);
|
|
213
|
+
const after = Process.fromPid(pid);
|
|
214
|
+
if (identity && after?.incarnation === processRef.incarnation) {
|
|
215
|
+
candidates.set(pid, { ...identity, processRef });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const pid of extendOwnedDarwinAncestry(knownUniqueIds, candidates)) {
|
|
219
|
+
retainOwnedProcess(owned, candidates.get(pid)!.processRef);
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
},
|
|
223
|
+
close() {
|
|
224
|
+
proc.close();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function supervisorArgv(): string[] {
|
|
233
|
+
return isCompiledBinary()
|
|
234
|
+
? [process.execPath, BASH_SHELL_SUPERVISOR_ARG]
|
|
235
|
+
: [process.execPath, path.join(import.meta.dir, "bash-shell-supervisor-entry.ts")];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function runBashShellGuardian(): Promise<void> {
|
|
239
|
+
if (!enableLinuxChildSubreaper()) throw new Error("Linux child subreaper setup failed.");
|
|
240
|
+
const ownershipFilePath = path.join(os.tmpdir(), `gjc-shell-ownership-${randomUUID()}.jsonl`);
|
|
241
|
+
const ledgerToken = randomUUID().replaceAll("-", "");
|
|
242
|
+
const ledger = await fs.open(ownershipFilePath, "a+", 0o600);
|
|
243
|
+
const ledgerStat = await ledger.stat({ bigint: true });
|
|
244
|
+
if (
|
|
245
|
+
process.platform === "darwin" &&
|
|
246
|
+
!findDarwinLedgerHolders(Number(ledgerStat.dev), ledgerStat.ino)?.some(holder => holder.pid === process.pid)
|
|
247
|
+
) {
|
|
248
|
+
await ledger.close();
|
|
249
|
+
await fs.rm(ownershipFilePath, { force: true });
|
|
250
|
+
throw new Error("Darwin ownership descriptor discovery is unavailable.");
|
|
251
|
+
}
|
|
252
|
+
const ownedProcesses = new Map<string, NativeProcess>();
|
|
253
|
+
const darwinTracker = createDarwinAncestryTracker(ownedProcesses);
|
|
254
|
+
if (process.platform === "darwin" && !darwinTracker) {
|
|
255
|
+
await ledger.close();
|
|
256
|
+
await fs.rm(ownershipFilePath, { force: true });
|
|
257
|
+
throw new Error("Darwin unique-ancestry tracking is unavailable.");
|
|
258
|
+
}
|
|
259
|
+
if (darwinTracker) {
|
|
260
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
261
|
+
const guardian = Process.fromPid(process.pid);
|
|
262
|
+
if (!guardian || !darwinTracker.track(guardian)) {
|
|
263
|
+
darwinTracker.close();
|
|
264
|
+
await ledger.close();
|
|
265
|
+
await fs.rm(ownershipFilePath, { force: true });
|
|
266
|
+
throw new Error("Darwin guardian ancestry registration failed.");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const [executable, ...args] = supervisorArgv();
|
|
270
|
+
const supervisor = childProcess.spawn(executable!, args, {
|
|
271
|
+
detached: process.platform !== "win32",
|
|
272
|
+
env: process.env,
|
|
273
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
});
|
|
276
|
+
const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
277
|
+
input.on("line", line => {
|
|
278
|
+
let forwarded = line;
|
|
279
|
+
try {
|
|
280
|
+
const request = JSON.parse(line) as BashShellWorkerRequest;
|
|
281
|
+
if (request.type === "init") {
|
|
282
|
+
forwarded = JSON.stringify({
|
|
283
|
+
...request,
|
|
284
|
+
ownershipLedger: { path: ownershipFilePath, token: ledgerToken },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
if (supervisor.stdin.writable) supervisor.stdin.write(`${forwarded}\n`);
|
|
289
|
+
});
|
|
290
|
+
input.once("close", () => supervisor.stdin.end());
|
|
291
|
+
let cleaning: Promise<void> | undefined;
|
|
292
|
+
let ledgerBuffer = "";
|
|
293
|
+
let ownershipScan = Promise.resolve(true);
|
|
294
|
+
const scanOwnership = (): Promise<boolean> => {
|
|
295
|
+
ownershipScan = ownershipScan.then(async previousOk => {
|
|
296
|
+
if (!previousOk) return false;
|
|
297
|
+
let content: string;
|
|
298
|
+
try {
|
|
299
|
+
content = await ledger.readFile({ encoding: "utf8" });
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
ledgerBuffer += content;
|
|
304
|
+
const lines = ledgerBuffer.split("\n");
|
|
305
|
+
ledgerBuffer = lines.pop() ?? "";
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
if (!line) continue;
|
|
308
|
+
const owned = authenticateOwnershipRecord(line, ledgerToken);
|
|
309
|
+
if (owned?.darwinUniqueId && darwinTracker) darwinTracker.seed(owned.darwinUniqueId);
|
|
310
|
+
if (owned?.processRef && darwinTracker && !darwinTracker.track(owned.processRef, owned.darwinUniqueId))
|
|
311
|
+
return false;
|
|
312
|
+
if (owned?.processRef && !darwinTracker) retainOwnedProcess(ownedProcesses, owned.processRef);
|
|
313
|
+
}
|
|
314
|
+
if (darwinTracker) {
|
|
315
|
+
if (!darwinTracker.poll()) return false;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
});
|
|
319
|
+
return ownershipScan;
|
|
320
|
+
};
|
|
321
|
+
let periodicScanActive = false;
|
|
322
|
+
const ownershipScanTimer = darwinTracker
|
|
323
|
+
? setInterval(() => {
|
|
324
|
+
if (periodicScanActive) return;
|
|
325
|
+
periodicScanActive = true;
|
|
326
|
+
void scanOwnership()
|
|
327
|
+
.then(ok => {
|
|
328
|
+
if (ok || supervisor.exitCode !== null || supervisor.signalCode !== null) return;
|
|
329
|
+
if (supervisor.pid) {
|
|
330
|
+
try {
|
|
331
|
+
process.kill(-supervisor.pid, "SIGKILL");
|
|
332
|
+
} catch {}
|
|
333
|
+
}
|
|
334
|
+
supervisor.kill("SIGKILL");
|
|
335
|
+
})
|
|
336
|
+
.finally(() => {
|
|
337
|
+
periodicScanActive = false;
|
|
338
|
+
});
|
|
339
|
+
}, 100)
|
|
340
|
+
: undefined;
|
|
341
|
+
const cleanup = (): Promise<void> => {
|
|
342
|
+
cleaning ??= (async () => {
|
|
343
|
+
if (ownershipScanTimer) clearInterval(ownershipScanTimer);
|
|
344
|
+
let trackingOk = await scanOwnership();
|
|
345
|
+
if (supervisor.exitCode === null && supervisor.signalCode === null) {
|
|
346
|
+
if (process.platform !== "win32" && supervisor.pid) {
|
|
347
|
+
try {
|
|
348
|
+
// The directly-owned supervisor is still the live group leader here.
|
|
349
|
+
process.kill(-supervisor.pid, "SIGKILL");
|
|
350
|
+
} catch {}
|
|
351
|
+
}
|
|
352
|
+
supervisor.kill("SIGKILL");
|
|
353
|
+
}
|
|
354
|
+
for (let wave = 0; wave < 3; wave++) {
|
|
355
|
+
await Bun.sleep(wave === 0 ? 5 : 25);
|
|
356
|
+
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
357
|
+
const guardian = Process.fromPid(process.pid);
|
|
358
|
+
const pending = guardian?.children() ?? [];
|
|
359
|
+
while (pending.length > 0) {
|
|
360
|
+
const processRef = pending.pop()!;
|
|
361
|
+
retainOwnedProcess(ownedProcesses, processRef);
|
|
362
|
+
pending.push(...processRef.children());
|
|
363
|
+
}
|
|
364
|
+
trackingOk = (await scanOwnership()) && trackingOk;
|
|
365
|
+
for (const owned of ownedProcesses.values()) owned.killTree(9);
|
|
366
|
+
}
|
|
367
|
+
darwinTracker?.close();
|
|
368
|
+
await ledger.close();
|
|
369
|
+
await fs.rm(ownershipFilePath, { force: true });
|
|
370
|
+
if (!trackingOk) throw new Error("Shell ownership tracking failed.");
|
|
371
|
+
})();
|
|
372
|
+
return cleaning;
|
|
373
|
+
};
|
|
374
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
375
|
+
try {
|
|
376
|
+
process.on(signal, () => void cleanup().finally(() => process.exit(1)));
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
379
|
+
const outcome = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(resolve =>
|
|
380
|
+
supervisor.once("close", (code, signal) => resolve({ code, signal })),
|
|
381
|
+
);
|
|
382
|
+
await cleanup();
|
|
383
|
+
if (outcome.signal) {
|
|
384
|
+
process.removeAllListeners(outcome.signal);
|
|
385
|
+
process.kill(process.pid, outcome.signal);
|
|
386
|
+
}
|
|
387
|
+
if (outcome.code && outcome.code !== 0) process.exit(outcome.code);
|
|
388
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
|
|
3
|
+
const NON_FATAL_OR_UNCATCHABLE_SIGNALS = new Set([
|
|
4
|
+
"SIGCHLD",
|
|
5
|
+
"SIGCONT",
|
|
6
|
+
"SIGKILL",
|
|
7
|
+
// Bun deliberately ignores SIGPIPE for pipe-backed runtime I/O. Turning it
|
|
8
|
+
// into a fatal handler terminates healthy workers during ordinary protocol
|
|
9
|
+
// teardown; because it cannot terminate this boundary, it is not an owned
|
|
10
|
+
// fatal group-signal path.
|
|
11
|
+
"SIGPIPE",
|
|
12
|
+
"SIGSTOP",
|
|
13
|
+
"SIGTSTP",
|
|
14
|
+
"SIGTTIN",
|
|
15
|
+
"SIGTTOU",
|
|
16
|
+
"SIGURG",
|
|
17
|
+
"SIGWINCH",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function fatalCatchableSignals(): NodeJS.Signals[] {
|
|
21
|
+
return Object.keys(os.constants.signals).filter(
|
|
22
|
+
(signal): signal is NodeJS.Signals => !NON_FATAL_OR_UNCATCHABLE_SIGNALS.has(signal),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function signalExitCode(signal: NodeJS.Signals): number | undefined {
|
|
27
|
+
const number = os.constants.signals[signal];
|
|
28
|
+
return typeof number === "number" ? 128 + number : undefined;
|
|
29
|
+
}
|