@neuralnomads/codenomad-dev 0.18.0-dev-20260715-07e8ce68 → 0.18.0-dev-20260716-40cee071
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/README.md +12 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/dist/events/bus.js +13 -0
- package/dist/events/bus.test.js +37 -0
- package/dist/index.js +33 -56
- package/dist/index.test.js +32 -0
- package/dist/server/__tests__/remote-proxy.test.js +102 -6
- package/dist/server/http-server.js +2 -0
- package/dist/server/remote-proxy.js +96 -9
- package/dist/server/routes/usage.js +18 -0
- package/dist/server/routes/usage.test.js +26 -0
- package/dist/server/routes/workspaces.js +32 -1
- package/dist/server/routes/workspaces.test.js +75 -0
- package/dist/settings/binaries.js +4 -1
- package/dist/settings/binaries.test.js +24 -0
- package/dist/shutdown.js +75 -0
- package/dist/shutdown.test.js +111 -0
- package/dist/usage/providers/api-key.js +171 -0
- package/dist/usage/providers/google.js +171 -0
- package/dist/usage/providers/minimax.js +109 -0
- package/dist/usage/providers/oauth.js +111 -0
- package/dist/usage/providers/special.js +148 -0
- package/dist/usage/service.js +83 -0
- package/dist/usage/service.test.js +59 -0
- package/dist/usage/shared.js +130 -0
- package/dist/usage/shared.test.js +33 -0
- package/dist/usage/types.js +1 -0
- package/dist/workspaces/__tests__/spawn.test.js +64 -7
- package/dist/workspaces/__tests__/workspace-identity.test.js +177 -25
- package/dist/workspaces/launch-cleanup.js +132 -0
- package/dist/workspaces/launch-cleanup.test.js +33 -0
- package/dist/workspaces/manager.js +393 -135
- package/dist/workspaces/manager.test.js +318 -0
- package/dist/workspaces/process-identity.js +408 -0
- package/dist/workspaces/process-identity.test.js +165 -0
- package/dist/workspaces/runtime.js +567 -122
- package/dist/workspaces/runtime.test.js +603 -0
- package/dist/workspaces/spawn.js +86 -15
- package/package.json +1 -1
- package/public/assets/{FilesTab-EPvldvn1.js → FilesTab-BjztpSdj.js} +2 -2
- package/public/assets/{GitChangesTab-ofjovmv2.js → GitChangesTab-B9XraSoR.js} +2 -2
- package/public/assets/{SplitFilePanel-CyoPkcLd.js → SplitFilePanel-Cu_5-Z-E.js} +1 -1
- package/public/assets/StatusTab-D_IE79r-.js +1 -0
- package/public/assets/{align-justify-Bcf1-ZOl.js → align-justify-BKS9gyRk.js} +1 -1
- package/public/assets/{bundle-full-D5dzvToA.js → bundle-full-CXGXXlzR.js} +1 -1
- package/public/assets/{diff-viewer-DViXCI-4.js → diff-viewer-CSNOYoEB.js} +1 -1
- package/public/assets/{git-diff-vendor-CSgooKT_.js → git-diff-vendor-CYS3ApzA.js} +1 -1
- package/public/assets/index-B3YjNCh4.js +1 -0
- package/public/assets/index-B3ZD2_SU.js +1 -0
- package/public/assets/index-Bh2ofaQo.js +1 -0
- package/public/assets/index-CM0c9xRZ.js +1 -0
- package/public/assets/index-CdDxouJl.js +2 -0
- package/public/assets/index-D06Y2cFq.js +1 -0
- package/public/assets/{index-lf4nUOU1.js → index-DVc4ls8c.js} +1 -1
- package/public/assets/index-PmE51vPh.js +1 -0
- package/public/assets/{index-BMvFMsuy.css → index-Qj63IiSj.css} +1 -1
- package/public/assets/index-VY_zwmsG.js +1 -0
- package/public/assets/index-qiiH7egV.js +1 -0
- package/public/assets/{index-UbL8tz_V.js → index-sLe9hEfQ.js} +1 -1
- package/public/assets/{loading-CZ_tt3h0.js → loading-CDmt9OJU.js} +1 -1
- package/public/assets/main-CTN6Wwz1.js +68 -0
- package/public/assets/{markdown-G11aN6hz.js → markdown-1ZLSxCFT.js} +55 -44
- package/public/assets/monaco-viewer-D6JgH76W.js +26 -0
- package/public/assets/{tool-call-BsSXWErm.js → tool-call-CyXb63LH.js} +32 -32
- package/public/assets/{unified-picker-BRNI3UvE.js → unified-picker-DCCdSVwI.js} +1 -1
- package/public/assets/{wrap-text-B7up-uv7.js → wrap-text-pNMdTIq3.js} +1 -1
- package/public/index.html +5 -5
- package/public/loading.html +5 -5
- package/public/sw.js +1 -1
- package/public/assets/StatusTab-Bgu1Fh24.js +0 -1
- package/public/assets/index-3KsjbWGP.js +0 -1
- package/public/assets/index-B0WK8-cF.js +0 -1
- package/public/assets/index-BDv_iVtj.js +0 -1
- package/public/assets/index-BjxZvyb9.js +0 -2
- package/public/assets/index-BleKf9Nj.js +0 -1
- package/public/assets/index-CiIQ43NW.js +0 -1
- package/public/assets/index-DBquqL6Q.js +0 -1
- package/public/assets/index-yf9zolv4.js +0 -1
- package/public/assets/index-ypaL7i_s.js +0 -1
- package/public/assets/main-CAkg_fp_.js +0 -68
- package/public/assets/monaco-viewer-CZefBfct.js +0 -26
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
const LINUX_SNAPSHOT_SCRIPT = String.raw `
|
|
2
|
+
boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || exit 1
|
|
3
|
+
test -n "$boot" || exit 1
|
|
4
|
+
for stat in /proc/[0-9]*/stat; do
|
|
5
|
+
line=$(cat "$stat" 2>/dev/null) || continue
|
|
6
|
+
pid=$(printf '%s\n' "$line" | cut -d' ' -f1)
|
|
7
|
+
rest=$(printf '%s\n' "$line" | sed 's/^.*) //')
|
|
8
|
+
set -- $rest
|
|
9
|
+
printf '%s|%s|%s|%s|%s|%s\n' "$pid" "$2" "$3" "$20" "$boot" "$20"
|
|
10
|
+
done
|
|
11
|
+
`;
|
|
12
|
+
const LINUX_GUARDED_SIGNAL_SCRIPT = String.raw `
|
|
13
|
+
boot=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null) || exit 20
|
|
14
|
+
read_stat() {
|
|
15
|
+
line=$(cat "/proc/$1/stat" 2>/dev/null) || return 1
|
|
16
|
+
stat_pid=$(printf '%s\n' "$line" | cut -d' ' -f1); rest=$(printf '%s\n' "$line" | sed 's/^.*) //'); set -- $rest
|
|
17
|
+
stat_ppid=$2; stat_group=$3; stat_start=$20
|
|
18
|
+
}
|
|
19
|
+
emit_target() {
|
|
20
|
+
printf 'CODENOMAD_TARGET|%s|%s|%s|%s|%s|%s\n' "$stat_pid" "$stat_ppid" "$stat_group" "$stat_start" "$boot" "$stat_start"
|
|
21
|
+
}
|
|
22
|
+
leader_pid=$1; leader_start=$2; leader_boot=$3; expected_group=$4; requested_signal=$5
|
|
23
|
+
shift 5
|
|
24
|
+
matched=0; cutoff=; signal_sent=0
|
|
25
|
+
if read_stat "$leader_pid" && test "$boot" = "$leader_boot" && test "$stat_start" = "$leader_start" && test "$stat_group" = "$expected_group"; then
|
|
26
|
+
matched=1
|
|
27
|
+
for stat in /proc/[0-9]*/stat; do
|
|
28
|
+
candidate=$(basename "$(dirname "$stat")")
|
|
29
|
+
if read_stat "$candidate" && test "$stat_group" = "$expected_group"; then emit_target; fi
|
|
30
|
+
done
|
|
31
|
+
if kill "-$requested_signal" -- "-$expected_group" 2>/dev/null; then
|
|
32
|
+
signal_sent=1
|
|
33
|
+
hz=$(getconf CLK_TCK 2>/dev/null) || exit 21
|
|
34
|
+
uptime=$(cut -d' ' -f1 /proc/uptime 2>/dev/null) || exit 21
|
|
35
|
+
cutoff=$(awk -v uptime="$uptime" -v hz="$hz" 'BEGIN { printf "%.0f", uptime * hz }')
|
|
36
|
+
fi
|
|
37
|
+
else
|
|
38
|
+
while test "$#" -ge 3; do
|
|
39
|
+
expected_pid=$1; expected_start=$2; expected_boot=$3; shift 3
|
|
40
|
+
if read_stat "$expected_pid" && test "$boot" = "$expected_boot" && test "$stat_start" = "$expected_start"; then
|
|
41
|
+
emit_target
|
|
42
|
+
if kill "-$requested_signal" "$expected_pid" 2>/dev/null; then signal_sent=1; fi
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
fi
|
|
46
|
+
printf 'CODENOMAD_RESULT|%s|%s|%s\n' "$matched" "$cutoff" "$signal_sent"
|
|
47
|
+
`;
|
|
48
|
+
const POSIX_GUARDED_SIGNAL_SCRIPT = String.raw `
|
|
49
|
+
encode() { printf '%s' "$1" | base64 | tr -d '\r\n'; }
|
|
50
|
+
read_identity() {
|
|
51
|
+
current_meta=$(ps -p "$1" -o ppid= -o pgid= -o lstart= 2>/dev/null) || return 1
|
|
52
|
+
current_command=$(ps -p "$1" -o command= 2>/dev/null) || return 1
|
|
53
|
+
current_verify=$(ps -p "$1" -o ppid= -o pgid= -o lstart= 2>/dev/null) || return 1
|
|
54
|
+
test "$current_meta" = "$current_verify" || return 1
|
|
55
|
+
set -- $current_meta
|
|
56
|
+
test "$#" -ge 7 || return 1
|
|
57
|
+
current_ppid=$1; current_group=$2; shift 2
|
|
58
|
+
current_start="$1 $2 $3 $4 $5"
|
|
59
|
+
current_identity=$(printf '%s\t%s' "$current_start" "$current_command")
|
|
60
|
+
}
|
|
61
|
+
emit_target() {
|
|
62
|
+
printf 'CODENOMAD_TARGET_B64|%s|%s|%s|' "$current_pid" "$current_ppid" "$current_group"
|
|
63
|
+
encode "$current_start"; printf '|'; encode "$current_command"; printf '\n'
|
|
64
|
+
}
|
|
65
|
+
leader_pid=$1; leader_start=$2; expected_group=$3; requested_signal=$4; shift 4
|
|
66
|
+
matched=0; signal_sent=0
|
|
67
|
+
if read_identity "$leader_pid" && test "$current_group" = "$expected_group" && test "$current_identity" = "$leader_start"; then
|
|
68
|
+
matched=1
|
|
69
|
+
for current_pid in $(ps -eo pid= 2>/dev/null); do
|
|
70
|
+
if read_identity "$current_pid" && test "$current_group" = "$expected_group"; then emit_target; fi
|
|
71
|
+
done
|
|
72
|
+
if kill "-$requested_signal" -- "-$expected_group" 2>/dev/null; then signal_sent=1; fi
|
|
73
|
+
else
|
|
74
|
+
while test "$#" -ge 2; do
|
|
75
|
+
expected_pid=$1; expected_start=$2; shift 2
|
|
76
|
+
current_pid=$expected_pid
|
|
77
|
+
if read_identity "$expected_pid" && test "$current_identity" = "$expected_start"; then
|
|
78
|
+
emit_target
|
|
79
|
+
if kill "-$requested_signal" "$expected_pid" 2>/dev/null; then signal_sent=1; fi
|
|
80
|
+
fi
|
|
81
|
+
done
|
|
82
|
+
fi
|
|
83
|
+
printf 'CODENOMAD_RESULT|%s||%s\n' "$matched" "$signal_sent"
|
|
84
|
+
`;
|
|
85
|
+
const POSIX_OWNED_GROUP_SIGNAL_SCRIPT = String.raw `
|
|
86
|
+
encode() { printf '%s' "$1" | base64 | tr -d '\r\n'; }
|
|
87
|
+
read_identity() {
|
|
88
|
+
current_meta=$(ps -p "$1" -o ppid= -o pgid= -o lstart= 2>/dev/null) || return 1
|
|
89
|
+
current_command=$(ps -p "$1" -o command= 2>/dev/null) || return 1
|
|
90
|
+
current_verify=$(ps -p "$1" -o ppid= -o pgid= -o lstart= 2>/dev/null) || return 1
|
|
91
|
+
test "$current_meta" = "$current_verify" || return 1
|
|
92
|
+
set -- $current_meta
|
|
93
|
+
test "$#" -ge 7 || return 1
|
|
94
|
+
current_ppid=$1; current_group=$2; shift 2
|
|
95
|
+
current_start="$1 $2 $3 $4 $5"
|
|
96
|
+
current_identity=$(printf '%s\t%s' "$current_start" "$current_command")
|
|
97
|
+
}
|
|
98
|
+
emit_target() {
|
|
99
|
+
printf 'CODENOMAD_TARGET_B64|%s|%s|%s|' "$current_pid" "$current_ppid" "$current_group"
|
|
100
|
+
encode "$current_start"; printf '|'; encode "$current_command"; printf '\n'
|
|
101
|
+
}
|
|
102
|
+
root_pid=$1; requested_signal=$2; matched=0; signal_sent=0
|
|
103
|
+
if read_identity "$root_pid" && test "$current_group" = "$root_pid"; then
|
|
104
|
+
matched=1
|
|
105
|
+
for current_pid in $(ps -eo pid= 2>/dev/null); do
|
|
106
|
+
if read_identity "$current_pid" && test "$current_group" = "$root_pid"; then emit_target; fi
|
|
107
|
+
done
|
|
108
|
+
if kill "-$requested_signal" -- "-$root_pid" 2>/dev/null; then signal_sent=1; fi
|
|
109
|
+
for current_pid in $(ps -eo pid= 2>/dev/null); do
|
|
110
|
+
if read_identity "$current_pid" && test "$current_group" = "$root_pid"; then emit_target; fi
|
|
111
|
+
done
|
|
112
|
+
fi
|
|
113
|
+
printf 'CODENOMAD_RESULT|%s||%s\n' "$matched" "$signal_sent"
|
|
114
|
+
`;
|
|
115
|
+
function commandError(result) {
|
|
116
|
+
return result.error?.message || String(result.stderr ?? result.stdout ?? "").trim() || `exit code ${result.status}`;
|
|
117
|
+
}
|
|
118
|
+
function parseDelimitedSnapshot(output, requireBootId = false) {
|
|
119
|
+
const processes = new Map();
|
|
120
|
+
for (const line of output.split(/\r?\n/)) {
|
|
121
|
+
if (!line)
|
|
122
|
+
continue;
|
|
123
|
+
const fields = line.split("|");
|
|
124
|
+
if (fields.length !== 6)
|
|
125
|
+
return null;
|
|
126
|
+
const [pidText, parentPidText, groupIdText, startTime = "", bootId = "", startOrder = ""] = fields;
|
|
127
|
+
const pid = Number.parseInt(pidText ?? "", 10);
|
|
128
|
+
const parentPid = Number.parseInt(parentPidText ?? "", 10);
|
|
129
|
+
const groupId = Number.parseInt(groupIdText ?? "", 10);
|
|
130
|
+
if (!/^\d+$/.test(pidText ?? "") || !/^\d+$/.test(parentPidText ?? "") || !/^\d+$/.test(groupIdText ?? "") ||
|
|
131
|
+
!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(parentPid) || !startTime || (requireBootId && !bootId))
|
|
132
|
+
return null;
|
|
133
|
+
processes.set(pid, {
|
|
134
|
+
pid,
|
|
135
|
+
parentPid,
|
|
136
|
+
groupId: Number.isInteger(groupId) && groupId > 0 ? groupId : pid,
|
|
137
|
+
startTime,
|
|
138
|
+
...(bootId ? { bootId } : {}),
|
|
139
|
+
...(startOrder ? { startOrder } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return processes;
|
|
143
|
+
}
|
|
144
|
+
function decodeBase64Field(value) {
|
|
145
|
+
if (value.length === 0 || value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const bytes = Buffer.from(value, "base64");
|
|
150
|
+
if (bytes.toString("base64") !== value)
|
|
151
|
+
return null;
|
|
152
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function parseBase64Snapshot(output, prefix = "CODENOMAD_B64|") {
|
|
159
|
+
const processes = new Map();
|
|
160
|
+
for (const line of output.split(/\r?\n/)) {
|
|
161
|
+
if (!line)
|
|
162
|
+
continue;
|
|
163
|
+
if (!line.startsWith(prefix))
|
|
164
|
+
return null;
|
|
165
|
+
const fields = line.slice(prefix.length).split("|");
|
|
166
|
+
if (fields.length !== 5)
|
|
167
|
+
return null;
|
|
168
|
+
const [pidText = "", parentPidText = "", groupIdText = "", startEncoded = "", commandEncoded = ""] = fields;
|
|
169
|
+
if (!/^\d+$/.test(pidText) || !/^\d+$/.test(parentPidText) || !/^\d+$/.test(groupIdText))
|
|
170
|
+
return null;
|
|
171
|
+
const pid = Number.parseInt(pidText, 10);
|
|
172
|
+
const parentPid = Number.parseInt(parentPidText, 10);
|
|
173
|
+
const groupId = Number.parseInt(groupIdText, 10);
|
|
174
|
+
const start = decodeBase64Field(startEncoded);
|
|
175
|
+
const command = decodeBase64Field(commandEncoded);
|
|
176
|
+
if (pid <= 0 || parentPid < 0 || groupId <= 0 || start === null || command === null)
|
|
177
|
+
return null;
|
|
178
|
+
processes.set(pid, { pid, parentPid, groupId, startTime: `${start}\t${command}` });
|
|
179
|
+
}
|
|
180
|
+
return processes;
|
|
181
|
+
}
|
|
182
|
+
function parsePortablePosixSnapshot(output) {
|
|
183
|
+
const processes = new Map();
|
|
184
|
+
for (const line of output.split(/\r?\n/)) {
|
|
185
|
+
if (!line.trim())
|
|
186
|
+
continue;
|
|
187
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+\s+\S+\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+)$/);
|
|
188
|
+
if (!match)
|
|
189
|
+
return null;
|
|
190
|
+
const [, pidText = "", parentPidText = "", groupIdText = "", start = "", command = ""] = match;
|
|
191
|
+
const pid = Number.parseInt(pidText, 10);
|
|
192
|
+
const parentPid = Number.parseInt(parentPidText, 10);
|
|
193
|
+
const groupId = Number.parseInt(groupIdText, 10);
|
|
194
|
+
if (pid <= 0 || parentPid < 0 || groupId <= 0)
|
|
195
|
+
return null;
|
|
196
|
+
processes.set(pid, { pid, parentPid, groupId, startTime: `${start}\t${command}` });
|
|
197
|
+
}
|
|
198
|
+
return processes;
|
|
199
|
+
}
|
|
200
|
+
function snapshotOrFailure(processes) {
|
|
201
|
+
return processes && processes.size > 0
|
|
202
|
+
? { ok: true, processes }
|
|
203
|
+
: { ok: false, error: "process identity query returned no parseable processes" };
|
|
204
|
+
}
|
|
205
|
+
function parseGuardedResult(result) {
|
|
206
|
+
const signaled = new Map();
|
|
207
|
+
const failure = (error) => ({
|
|
208
|
+
ok: false,
|
|
209
|
+
error,
|
|
210
|
+
...(signaled.size > 0 ? { observed: Array.from(signaled.values()) } : {}),
|
|
211
|
+
});
|
|
212
|
+
let matched;
|
|
213
|
+
let signalSent = false;
|
|
214
|
+
let cutoff;
|
|
215
|
+
for (const line of String(result.stdout ?? "").split(/\r?\n/)) {
|
|
216
|
+
if (line.startsWith("CODENOMAD_TARGET|")) {
|
|
217
|
+
const parsed = parseDelimitedSnapshot(line.slice("CODENOMAD_TARGET|".length));
|
|
218
|
+
if (!parsed)
|
|
219
|
+
return failure("guarded signal command returned a malformed target record");
|
|
220
|
+
for (const identity of parsed.values())
|
|
221
|
+
signaled.set(identity.pid, identity);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (line.startsWith("CODENOMAD_TARGET_B64|")) {
|
|
225
|
+
const parsed = parseBase64Snapshot(line, "CODENOMAD_TARGET_B64|");
|
|
226
|
+
if (!parsed)
|
|
227
|
+
return failure("guarded signal command returned a malformed target record");
|
|
228
|
+
for (const identity of parsed.values())
|
|
229
|
+
signaled.set(identity.pid, identity);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (line.startsWith("CODENOMAD_RESULT|")) {
|
|
233
|
+
const fields = line.split("|");
|
|
234
|
+
if (fields.length !== 4 || !/^[01]$/.test(fields[1] ?? "") || !/^[01]$/.test(fields[3] ?? "")) {
|
|
235
|
+
return failure("guarded signal command returned a malformed result record");
|
|
236
|
+
}
|
|
237
|
+
const [, matchedText, cutoffText, signalSentText] = fields;
|
|
238
|
+
matched = matchedText === "1";
|
|
239
|
+
cutoff = cutoffText || undefined;
|
|
240
|
+
signalSent = signalSentText === "1";
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (line)
|
|
244
|
+
return failure("guarded signal command returned unexpected output");
|
|
245
|
+
}
|
|
246
|
+
if (result.status !== 0)
|
|
247
|
+
return failure(commandError(result));
|
|
248
|
+
return matched === undefined
|
|
249
|
+
? failure("guarded signal command returned no structured result")
|
|
250
|
+
: { ok: true, matched, signalSent, signaled: Array.from(signaled.values()), ...(cutoff ? { cutoff } : {}) };
|
|
251
|
+
}
|
|
252
|
+
function runGuardedCommand(spawnCommand, command, args, timeoutMs) {
|
|
253
|
+
try {
|
|
254
|
+
return parseGuardedResult(spawnCommand(command, args, { encoding: "utf8", timeout: timeoutMs }));
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function signalName(signal) {
|
|
261
|
+
return signal === "SIGKILL" ? "KILL" : "TERM";
|
|
262
|
+
}
|
|
263
|
+
function shellGuardArgs(request, linux) {
|
|
264
|
+
const leader = request.leader;
|
|
265
|
+
const args = linux
|
|
266
|
+
? [String(leader?.pid ?? 0), leader?.startTime ?? "", leader?.bootId ?? "", String(request.groupId ?? 0), signalName(request.signal)]
|
|
267
|
+
: [String(leader?.pid ?? 0), leader?.startTime ?? "", String(request.groupId ?? 0), signalName(request.signal)];
|
|
268
|
+
for (const member of request.members) {
|
|
269
|
+
args.push(String(member.pid), member.startTime);
|
|
270
|
+
if (linux)
|
|
271
|
+
args.push(member.bootId ?? "");
|
|
272
|
+
}
|
|
273
|
+
return args;
|
|
274
|
+
}
|
|
275
|
+
function quotePowerShell(value) {
|
|
276
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
277
|
+
}
|
|
278
|
+
function buildWindowsGuardedScript(request) {
|
|
279
|
+
const leaderPid = request.leader?.pid ?? 0;
|
|
280
|
+
const leaderStart = quotePowerShell(request.leader?.startTime ?? "");
|
|
281
|
+
const expected = request.members
|
|
282
|
+
.map((identity) => `@{ Pid = ${identity.pid}; Start = ${quotePowerShell(identity.startTime)} }`)
|
|
283
|
+
.join(", ");
|
|
284
|
+
return [
|
|
285
|
+
"$ErrorActionPreference = 'Stop'",
|
|
286
|
+
`$leaderPid = ${leaderPid}`,
|
|
287
|
+
`$leaderStart = ${leaderStart}`,
|
|
288
|
+
`$expected = @(${expected})`,
|
|
289
|
+
"function Get-CodeNomadStart($process) { return ([datetime]$process.CreationDate).ToUniversalTime().Ticks.ToString() }",
|
|
290
|
+
"$all = @(Get-CimInstance Win32_Process -ErrorAction Stop)",
|
|
291
|
+
"$byPid = @{}; $all | ForEach-Object { $byPid[[int]$_.ProcessId] = $_ }",
|
|
292
|
+
"$leader = $byPid[$leaderPid]",
|
|
293
|
+
"$matched = $null -ne $leader -and (Get-CodeNomadStart $leader) -eq $leaderStart",
|
|
294
|
+
"$selected = @()",
|
|
295
|
+
"if ($matched) {",
|
|
296
|
+
" $ids = @($leaderPid); $changed = $true",
|
|
297
|
+
" while ($changed) { $changed = $false; foreach ($process in $all) { if ($ids -contains [int]$process.ParentProcessId -and $ids -notcontains [int]$process.ProcessId) { $ids += [int]$process.ProcessId; $changed = $true } } }",
|
|
298
|
+
" $selected = @($all | Where-Object { $ids -contains [int]$_.ProcessId } | Sort-Object ProcessId -Descending)",
|
|
299
|
+
"} else {",
|
|
300
|
+
" foreach ($item in $expected) { $process = $byPid[[int]$item.Pid]; if ($null -ne $process -and (Get-CodeNomadStart $process) -eq [string]$item.Start) { $selected += $process } }",
|
|
301
|
+
"}",
|
|
302
|
+
"foreach ($process in $selected) {",
|
|
303
|
+
" $start = Get-CodeNomadStart $process",
|
|
304
|
+
" '{0}|{1}|0|{2}||{2}' -f [int]$process.ProcessId, [int]$process.ParentProcessId, $start | ForEach-Object { 'CODENOMAD_TARGET|' + $_ }",
|
|
305
|
+
"}",
|
|
306
|
+
"foreach ($process in $selected) {",
|
|
307
|
+
" Invoke-CimMethod -InputObject $process -MethodName Terminate -Arguments @{ Reason = 1 } -ErrorAction Stop | Out-Null",
|
|
308
|
+
"}",
|
|
309
|
+
"'CODENOMAD_RESULT|' + ($(if ($matched) { '1' } else { '0' })) + '||' + ($(if ($selected.Count -gt 0) { '1' } else { '0' }))",
|
|
310
|
+
].join("; ");
|
|
311
|
+
}
|
|
312
|
+
export function sameProcess(left, right) {
|
|
313
|
+
return Boolean(left && right && left.pid === right.pid && left.startTime === right.startTime &&
|
|
314
|
+
(!left.bootId || !right.bootId || left.bootId === right.bootId));
|
|
315
|
+
}
|
|
316
|
+
export function startedNoLaterThan(identity, cutoff) {
|
|
317
|
+
const startOrder = identity.startOrder ?? identity.startTime;
|
|
318
|
+
if (!/^\d+$/.test(startOrder) || !/^\d+$/.test(cutoff))
|
|
319
|
+
return false;
|
|
320
|
+
try {
|
|
321
|
+
return BigInt(startOrder) <= BigInt(cutoff);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export function descendantsOf(processes, rootPid) {
|
|
328
|
+
const descendants = [];
|
|
329
|
+
const pending = [rootPid];
|
|
330
|
+
const seen = new Set(pending);
|
|
331
|
+
while (pending.length > 0) {
|
|
332
|
+
const parentPid = pending.shift();
|
|
333
|
+
for (const process of processes.values()) {
|
|
334
|
+
if (process.parentPid !== parentPid || seen.has(process.pid))
|
|
335
|
+
continue;
|
|
336
|
+
seen.add(process.pid);
|
|
337
|
+
pending.push(process.pid);
|
|
338
|
+
descendants.push(process);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return descendants;
|
|
342
|
+
}
|
|
343
|
+
export function probePosixProcesses(spawnCommand, timeoutMs, platform = process.platform) {
|
|
344
|
+
try {
|
|
345
|
+
if (platform === "linux") {
|
|
346
|
+
const result = spawnCommand("sh", ["-c", LINUX_SNAPSHOT_SCRIPT, "codenomad-posix-identity"], {
|
|
347
|
+
encoding: "utf8",
|
|
348
|
+
timeout: timeoutMs,
|
|
349
|
+
});
|
|
350
|
+
if (result.status !== 0)
|
|
351
|
+
return { ok: false, error: commandError(result) };
|
|
352
|
+
return snapshotOrFailure(parseDelimitedSnapshot(String(result.stdout ?? ""), true));
|
|
353
|
+
}
|
|
354
|
+
// POSIX has no portable pidfd/start ticks; collect one coherent table instead of probing every PID.
|
|
355
|
+
const result = spawnCommand("ps", ["-axo", "pid=,ppid=,pgid=,lstart=,command="], {
|
|
356
|
+
encoding: "utf8",
|
|
357
|
+
timeout: timeoutMs,
|
|
358
|
+
});
|
|
359
|
+
if (result.status !== 0)
|
|
360
|
+
return { ok: false, error: commandError(result) };
|
|
361
|
+
return snapshotOrFailure(parsePortablePosixSnapshot(String(result.stdout ?? "")));
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
export function probeWindowsProcesses(spawnCommand, timeoutMs) {
|
|
368
|
+
const script = [
|
|
369
|
+
"$all = @(Get-CimInstance Win32_Process -ErrorAction Stop)",
|
|
370
|
+
"$all | Where-Object { [int]$_.ProcessId -gt 0 } | ForEach-Object { $start = ([datetime]$_.CreationDate).ToUniversalTime().Ticks.ToString(); '{0}|{1}|0|{2}||{2}' -f [int]$_.ProcessId, [int]$_.ParentProcessId, $start }",
|
|
371
|
+
].join("; ");
|
|
372
|
+
try {
|
|
373
|
+
const result = spawnCommand("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
374
|
+
encoding: "utf8",
|
|
375
|
+
timeout: timeoutMs,
|
|
376
|
+
});
|
|
377
|
+
if (result.status !== 0)
|
|
378
|
+
return { ok: false, error: commandError(result) };
|
|
379
|
+
return snapshotOrFailure(parseDelimitedSnapshot(String(result.stdout ?? "")));
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
export function probeWslProcesses(spawnCommand, distro, timeoutMs) {
|
|
386
|
+
try {
|
|
387
|
+
const result = spawnCommand("wsl.exe", ["--distribution", distro, "--exec", "sh", "-c", LINUX_SNAPSHOT_SCRIPT, "codenomad-wsl-identity"], { encoding: "utf8", timeout: timeoutMs });
|
|
388
|
+
if (result.status !== 0)
|
|
389
|
+
return { ok: false, error: commandError(result) };
|
|
390
|
+
return snapshotOrFailure(parseDelimitedSnapshot(String(result.stdout ?? ""), true));
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export function signalPosixProcesses(spawnCommand, request, timeoutMs, platform) {
|
|
397
|
+
const linux = platform === "linux";
|
|
398
|
+
return runGuardedCommand(spawnCommand, "sh", ["-c", linux ? LINUX_GUARDED_SIGNAL_SCRIPT : POSIX_GUARDED_SIGNAL_SCRIPT, "codenomad-guarded-signal", ...shellGuardArgs(request, linux)], timeoutMs);
|
|
399
|
+
}
|
|
400
|
+
export function signalOwnedPosixProcessGroup(spawnCommand, rootPid, signal, timeoutMs) {
|
|
401
|
+
return runGuardedCommand(spawnCommand, "sh", ["-c", POSIX_OWNED_GROUP_SIGNAL_SCRIPT, "codenomad-owned-group-cleanup", String(rootPid), signalName(signal)], timeoutMs);
|
|
402
|
+
}
|
|
403
|
+
export function signalWslProcesses(spawnCommand, distro, request, timeoutMs) {
|
|
404
|
+
return runGuardedCommand(spawnCommand, "wsl.exe", ["--distribution", distro, "--exec", "sh", "-c", LINUX_GUARDED_SIGNAL_SCRIPT, "codenomad-wsl-guarded-signal", ...shellGuardArgs(request, true)], timeoutMs);
|
|
405
|
+
}
|
|
406
|
+
export function signalWindowsProcesses(spawnCommand, request, timeoutMs) {
|
|
407
|
+
return runGuardedCommand(spawnCommand, "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", buildWindowsGuardedScript(request)], timeoutMs);
|
|
408
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { probePosixProcesses, probeWindowsProcesses, probeWslProcesses, sameProcess, signalOwnedPosixProcessGroup, signalPosixProcesses, signalWindowsProcesses, startedNoLaterThan, } from "./process-identity";
|
|
4
|
+
function result(stdout = "", status = 0, stderr = "") {
|
|
5
|
+
return { pid: 1, output: [null, stdout, stderr], stdout, stderr, status, signal: null };
|
|
6
|
+
}
|
|
7
|
+
function b64(value) {
|
|
8
|
+
return Buffer.from(value).toString("base64");
|
|
9
|
+
}
|
|
10
|
+
describe("process identity probes", () => {
|
|
11
|
+
it("parses Linux PID, PGID, and kernel process start ticks", () => {
|
|
12
|
+
let invocation;
|
|
13
|
+
const probe = probePosixProcesses(((command, args) => {
|
|
14
|
+
invocation = { command, args };
|
|
15
|
+
return result("42|1|42|123456|boot-a|123456\n");
|
|
16
|
+
}), 25, "linux");
|
|
17
|
+
assert.equal(invocation?.command, "sh");
|
|
18
|
+
assert.ok(invocation?.args.includes("codenomad-posix-identity"));
|
|
19
|
+
assert.equal(probe.ok, true);
|
|
20
|
+
if (probe.ok) {
|
|
21
|
+
assert.deepEqual(probe.processes.get(42), {
|
|
22
|
+
pid: 42,
|
|
23
|
+
parentPid: 1,
|
|
24
|
+
groupId: 42,
|
|
25
|
+
startTime: "123456",
|
|
26
|
+
bootId: "boot-a",
|
|
27
|
+
startOrder: "123456",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it("uses one process-table query on non-Linux POSIX platforms", () => {
|
|
32
|
+
let invocation;
|
|
33
|
+
const probe = probePosixProcesses(((command, args) => {
|
|
34
|
+
invocation = { command, args };
|
|
35
|
+
return result(" 42 1 42 Fri Jul 10 12:34:56 2026 /usr/bin/opencode serve\n");
|
|
36
|
+
}), 25, "darwin");
|
|
37
|
+
assert.equal(invocation?.command, "ps");
|
|
38
|
+
assert.deepEqual(invocation?.args, ["-axo", "pid=,ppid=,pgid=,lstart=,command="]);
|
|
39
|
+
assert.equal(probe.ok && probe.processes.get(42)?.startTime, "Fri Jul 10 12:34:56 2026\t/usr/bin/opencode serve");
|
|
40
|
+
});
|
|
41
|
+
it("round trips delimiter-heavy non-Linux POSIX command identities", () => {
|
|
42
|
+
const command = "/opt/opencode 'pipe|value'\t\"quoted\" café";
|
|
43
|
+
const start = "Fri Jul 10 12:34:56 2026";
|
|
44
|
+
const probe = probePosixProcesses((() => result(`42 1 42 ${start} ${command}\n`)), 25, "darwin");
|
|
45
|
+
assert.equal(probe.ok && probe.processes.get(42)?.startTime, `${start}\t${command}`);
|
|
46
|
+
});
|
|
47
|
+
it("uses delimiter-safe identities during non-Linux POSIX escalation", () => {
|
|
48
|
+
const command = "/opt/opencode pipe|value\nnext\t'quoted'";
|
|
49
|
+
const startTime = `Fri Jul 10 12:34:56 2026\t${command}`;
|
|
50
|
+
const identity = { pid: 42, parentPid: 1, groupId: 42, startTime };
|
|
51
|
+
const guarded = signalPosixProcesses((() => result(`CODENOMAD_TARGET_B64|42|1|42|${b64("Fri Jul 10 12:34:56 2026")}|${b64(command)}\nCODENOMAD_RESULT|1||1\n`)), { leader: identity, groupId: 42, members: [identity], signal: "SIGKILL" }, 25, "darwin");
|
|
52
|
+
assert.equal(guarded.ok, true);
|
|
53
|
+
assert.equal(guarded.ok && guarded.signaled[0]?.startTime, startTime);
|
|
54
|
+
});
|
|
55
|
+
it("signals and rescans a still-owned portable POSIX process group", () => {
|
|
56
|
+
const command = "/opt/opencode pipe|value\nchild";
|
|
57
|
+
let script = "";
|
|
58
|
+
const cleanup = signalOwnedPosixProcessGroup(((_command, args) => {
|
|
59
|
+
script = args[1] ?? "";
|
|
60
|
+
return result(`CODENOMAD_TARGET_B64|42|1|42|${b64("Fri Jul 10 12:34:56 2026")}|${b64(command)}\n` +
|
|
61
|
+
"CODENOMAD_RESULT|1||1\n");
|
|
62
|
+
}), 42, "SIGTERM", 25);
|
|
63
|
+
assert.equal(cleanup.ok && cleanup.matched, true);
|
|
64
|
+
assert.equal(cleanup.ok && cleanup.signaled[0]?.startTime, `Fri Jul 10 12:34:56 2026\t${command}`);
|
|
65
|
+
assert.ok(script.indexOf('kill "-$requested_signal"') < script.lastIndexOf("for current_pid"));
|
|
66
|
+
});
|
|
67
|
+
it("rejects malformed portable process rows", () => {
|
|
68
|
+
const probe = probePosixProcesses((() => result("42 malformed process row\n")), 25, "darwin");
|
|
69
|
+
assert.deepEqual(probe, { ok: false, error: "process identity query returned no parseable processes" });
|
|
70
|
+
});
|
|
71
|
+
it("queries WSL identities inside the selected distro", () => {
|
|
72
|
+
let invocation;
|
|
73
|
+
const probe = probeWslProcesses(((command, args) => {
|
|
74
|
+
invocation = { command, args };
|
|
75
|
+
return result("99|1|99|123456|boot-a|123456\n101|99|99|123460|boot-a|123460\n");
|
|
76
|
+
}), "Ubuntu Test", 25);
|
|
77
|
+
assert.equal(invocation?.command, "wsl.exe");
|
|
78
|
+
assert.deepEqual(invocation?.args.slice(0, 4), ["--distribution", "Ubuntu Test", "--exec", "sh"]);
|
|
79
|
+
assert.ok(invocation?.args.includes("codenomad-wsl-identity"));
|
|
80
|
+
assert.equal(probe.ok && probe.processes.get(101)?.startTime, "123460");
|
|
81
|
+
});
|
|
82
|
+
it("parses Windows CIM CreationDate as the immutable identity", () => {
|
|
83
|
+
let script = "";
|
|
84
|
+
const probe = probeWindowsProcesses(((_command, args) => {
|
|
85
|
+
script = args.at(-1) ?? "";
|
|
86
|
+
return result("4242|100|0|20260710123456.123456+000||20260710123456\n");
|
|
87
|
+
}), 25);
|
|
88
|
+
assert.match(script, /Get-CimInstance Win32_Process/);
|
|
89
|
+
assert.match(script, /ProcessId -gt 0/);
|
|
90
|
+
assert.equal(probe.ok && probe.processes.get(4242)?.startTime, "20260710123456.123456+000");
|
|
91
|
+
});
|
|
92
|
+
it("matches only the same numeric PID and start identity", () => {
|
|
93
|
+
const original = { pid: 42, parentPid: 1, groupId: 42, startTime: "start-a" };
|
|
94
|
+
assert.equal(sameProcess(original, { ...original }), true);
|
|
95
|
+
assert.equal(sameProcess(original, { ...original, startTime: "start-b" }), false);
|
|
96
|
+
assert.equal(sameProcess(original, { ...original, pid: 43 }), false);
|
|
97
|
+
});
|
|
98
|
+
it("compares Linux start ticks numerically and rejects non-numeric fallbacks", () => {
|
|
99
|
+
const identity = { pid: 42, parentPid: 1, groupId: 42, startTime: "9", startOrder: "9" };
|
|
100
|
+
assert.equal(startedNoLaterThan(identity, "10"), true);
|
|
101
|
+
assert.equal(startedNoLaterThan({ ...identity, startOrder: "11" }, "10"), false);
|
|
102
|
+
assert.equal(startedNoLaterThan({ ...identity, startOrder: "Fri Jul 10" }, "10"), false);
|
|
103
|
+
});
|
|
104
|
+
it("returns mismatch without scheduling a second POSIX signal command", () => {
|
|
105
|
+
const invocations = [];
|
|
106
|
+
const identity = {
|
|
107
|
+
pid: 42,
|
|
108
|
+
parentPid: 1,
|
|
109
|
+
groupId: 42,
|
|
110
|
+
startTime: "123456",
|
|
111
|
+
bootId: "boot-a",
|
|
112
|
+
startOrder: "123456",
|
|
113
|
+
};
|
|
114
|
+
const guarded = signalPosixProcesses(((command, args) => {
|
|
115
|
+
invocations.push({ command, args });
|
|
116
|
+
return result("CODENOMAD_RESULT|0||0\n");
|
|
117
|
+
}), { leader: identity, groupId: 42, members: [identity], signal: "SIGTERM" }, 25, "linux");
|
|
118
|
+
assert.deepEqual(guarded, { ok: true, matched: false, signalSent: false, signaled: [] });
|
|
119
|
+
assert.equal(invocations.length, 1);
|
|
120
|
+
assert.equal(invocations[0]?.command, "sh");
|
|
121
|
+
assert.match(invocations[0]?.args[2] ?? "", /codenomad-guarded-signal/);
|
|
122
|
+
assert.ok(invocations[0]?.args.includes("123456"));
|
|
123
|
+
const script = invocations[0]?.args[1] ?? "";
|
|
124
|
+
assert.ok(script.indexOf('kill "-$requested_signal"') < script.indexOf("uptime=$(cut"));
|
|
125
|
+
});
|
|
126
|
+
it("uses one guarded Windows CIM selection and termination invocation", () => {
|
|
127
|
+
const invocations = [];
|
|
128
|
+
const identity = { pid: 4242, parentPid: 1, groupId: 4242, startTime: "created" };
|
|
129
|
+
const guarded = signalWindowsProcesses(((command, args) => {
|
|
130
|
+
invocations.push({ command, args });
|
|
131
|
+
return result("CODENOMAD_TARGET|4242|1|0|created||99\nCODENOMAD_RESULT|1||1\n");
|
|
132
|
+
}), { leader: identity, groupId: 4242, members: [identity], signal: "SIGKILL" }, 25);
|
|
133
|
+
assert.equal(guarded.ok && guarded.matched, true);
|
|
134
|
+
assert.equal(invocations.length, 1);
|
|
135
|
+
assert.equal(invocations[0]?.command, "powershell.exe");
|
|
136
|
+
const script = invocations[0]?.args.at(-1) ?? "";
|
|
137
|
+
assert.match(script, /CreationDate/);
|
|
138
|
+
assert.match(script, /Invoke-CimMethod -InputObject/);
|
|
139
|
+
assert.equal(script.match(/foreach \(\$process in \$selected\)/g)?.length, 2);
|
|
140
|
+
assert.ok(script.indexOf("CODENOMAD_TARGET|") < script.indexOf("Invoke-CimMethod"));
|
|
141
|
+
assert.doesNotMatch(script, /taskkill/i);
|
|
142
|
+
});
|
|
143
|
+
it("preserves observed Windows targets when guarded termination fails partway", () => {
|
|
144
|
+
const identity = { pid: 4242, parentPid: 1, groupId: 4242, startTime: "created" };
|
|
145
|
+
const guarded = signalWindowsProcesses((() => result([
|
|
146
|
+
"CODENOMAD_TARGET|4242|1|0|created||99",
|
|
147
|
+
"CODENOMAD_TARGET|4243|4242|0|descendant||100",
|
|
148
|
+
].join("\n"), 1, "termination failed")), {
|
|
149
|
+
leader: identity,
|
|
150
|
+
groupId: 4242,
|
|
151
|
+
members: [identity],
|
|
152
|
+
signal: "SIGTERM",
|
|
153
|
+
}, 25);
|
|
154
|
+
assert.equal(guarded.ok, false);
|
|
155
|
+
assert.deepEqual(!guarded.ok && guarded.observed?.map((target) => target.pid), [4242, 4243]);
|
|
156
|
+
});
|
|
157
|
+
it("reports command failures without fabricating identities", () => {
|
|
158
|
+
const probe = probeWindowsProcesses((() => result("", 1, "CIM unavailable")), 25);
|
|
159
|
+
assert.deepEqual(probe, { ok: false, error: "CIM unavailable" });
|
|
160
|
+
});
|
|
161
|
+
it("rejects successful commands with unparseable or empty output", () => {
|
|
162
|
+
const probe = probeWslProcesses((() => result("not an identity")), "Ubuntu", 25);
|
|
163
|
+
assert.deepEqual(probe, { ok: false, error: "process identity query returned no parseable processes" });
|
|
164
|
+
});
|
|
165
|
+
});
|