@reason-machines/cli-linux-arm64-musl 0.2.277 → 0.2.278

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/bin/ara +1 -1
  2. package/bin/vm-hands/connect-cli-qphdm1f1.js +2 -0
  3. package/bin/vm-hands/custom-endpoint-device-92914wyp.js +2 -0
  4. package/bin/vm-hands/desktop-account-vsrtgkk9.js +2 -0
  5. package/bin/vm-hands/desktop-wallpaper-0pjhgyfk.jpg +0 -0
  6. package/bin/vm-hands/device-workspace-zk5xwd10.js +2 -0
  7. package/bin/vm-hands/dock-browser-afp005mt.png +0 -0
  8. package/bin/vm-hands/dock-files-rywdm96b.png +0 -0
  9. package/bin/vm-hands/dock-terminal-s41cj1jp.png +0 -0
  10. package/bin/vm-hands/hands-worker-source-da93x0qx.js +2 -0
  11. package/bin/vm-hands/index-w9nq2f0p.js +12818 -0
  12. package/bin/vm-hands/local-automation-tvf2dbfq.js +2 -0
  13. package/bin/vm-hands/local-capabilities-p579j21r.js +2 -0
  14. package/bin/vm-hands/local-desktop-users-3j79jbyp.js +2 -0
  15. package/bin/vm-hands/local-display-mac-j1v3mcgx.js +2 -0
  16. package/bin/vm-hands/local-display-session-jwdq3jb4.js +2 -0
  17. package/bin/vm-hands/network-contract-41qanjhp.js +2 -0
  18. package/bin/vm-hands/package-rx9y4bwe.js +2 -0
  19. package/bin/vm-hands/permissions-2s0qyrg0.js +2 -0
  20. package/bin/vm-hands/reason-hands-protocol-51gs8j7v.js +2 -0
  21. package/bin/vm-hands/status-2d21kwsn.js +96 -0
  22. package/bin/vm-hands/version-1qgvgegp.js +2 -0
  23. package/bin/vm-hands/vm-hands-entry-lite-1bvbhwbb.js +3 -0
  24. package/bin/vm-hands/vm-hands-entry-lite-1hwdab4e.js +3 -0
  25. package/bin/vm-hands/vm-hands-entry-lite-2xas56hm.js +43 -0
  26. package/bin/vm-hands/vm-hands-entry-lite-51hcyapr.js +529 -0
  27. package/bin/vm-hands/vm-hands-entry-lite-5pxbx31d.js +179 -0
  28. package/bin/vm-hands/vm-hands-entry-lite-65pbrs1x.js +3 -0
  29. package/bin/vm-hands/vm-hands-entry-lite-7gh1rbn7.js +3 -0
  30. package/bin/vm-hands/vm-hands-entry-lite-84aq8d12.js +3 -0
  31. package/bin/vm-hands/vm-hands-entry-lite-8wyfep1z.js +343 -0
  32. package/bin/vm-hands/vm-hands-entry-lite-9pmag2qk.js +3 -0
  33. package/bin/vm-hands/vm-hands-entry-lite-anaydk9m.js +4 -0
  34. package/bin/vm-hands/vm-hands-entry-lite-bfwf82rw.js +5 -0
  35. package/bin/vm-hands/vm-hands-entry-lite-bwt3y6wz.js +3 -0
  36. package/bin/vm-hands/vm-hands-entry-lite-c2nrar90.js +4046 -0
  37. package/bin/vm-hands/vm-hands-entry-lite-ffg7rf02.js +3 -0
  38. package/bin/vm-hands/vm-hands-entry-lite-fraghb8k.js +5 -0
  39. package/bin/vm-hands/vm-hands-entry-lite-gqncr2j0.js +42 -0
  40. package/bin/vm-hands/vm-hands-entry-lite-htn1genx.js +459 -0
  41. package/bin/vm-hands/vm-hands-entry-lite-jwqqfjba.js +759 -0
  42. package/bin/vm-hands/vm-hands-entry-lite-k4c504n7.js +3 -0
  43. package/bin/vm-hands/vm-hands-entry-lite-n07kxrd1.js +3 -0
  44. package/bin/vm-hands/vm-hands-entry-lite-ptmqg51y.js +4 -0
  45. package/bin/vm-hands/vm-hands-entry-lite-qvrnx0vk.js +4 -0
  46. package/bin/vm-hands/vm-hands-entry-lite-s2xydt0n.js +112 -0
  47. package/bin/vm-hands/vm-hands-entry-lite-s5qn1k80.js +4 -0
  48. package/bin/vm-hands/vm-hands-entry-lite-sv6xwe0h.js +66 -0
  49. package/bin/vm-hands/vm-hands-entry-lite-y4sjgfqz.js +3 -0
  50. package/bin/vm-hands/vm-hands-entry-lite.js +2 -0
  51. package/bin/vm-hands/vm-hands-executor-v2qjnrb5.js +9 -0
  52. package/bin/vm-hands/vm-hands-op-runner-ej1c20a6.js +2 -0
  53. package/bin/vm-hands/vm-hands-runtime-yyy4ad4z.js +4 -0
  54. package/bin/vm-hands/worker-sandbox-nnce49w0.js +2 -0
  55. package/package.json +1 -1
@@ -0,0 +1,4046 @@
1
+ // @bun
2
+ import{Vb as i,Wb as l,Xb as g,Yb as y}from"./vm-hands-entry-lite-n07kxrd1.js";import{Af as I,Cf as O,ff as Q,gf as J,hf as z,if as Z,jf as K,kf as $,lf as k,nf as G,pf as X,qf as M,rf as Y,sf as H,tf as V,vf as S,wf as _,yf as P,zf as C}from"./vm-hands-entry-lite-ptmqg51y.js";import{lh as L,mh as W,nh as F,oh as j}from"./vm-hands-entry-lite-84aq8d12.js";var d=["cgroup_membership_read_failed","cgroup_v2_membership_absent","cgroup_scope_invalid","cgroup_root_unavailable","cgroup_directory_unavailable","cgroup_limit_read_failed","cgroup_events_read_failed","cgroup_current_read_failed","cgroup_stat_read_failed","boot_id_read_failed","boot_id_invalid","guest_meminfo_read_failed","guest_vmstat_read_failed","memory_counter_invalid","memory_field_missing","memory_capacity_invalid"];var a=String.raw`(function(fs, path) {
3
+ const fail = (message, reasonCode) => { throw Object.assign(Error(message), {reasonCode}); };
4
+ const read = (p, reasonCode, trim=true) => { try { const text=fs.readFileSync(p, "utf8"); return trim?text.trim():text; }
5
+ catch { fail("memory_read", reasonCode); } };
6
+ const checkPath = (p, reasonCode) => { let resolved; try { resolved=fs.realpathSync(p); }
7
+ catch { fail("memory_scope", reasonCode); }
8
+ if(resolved!==p) fail("memory_scope", "cgroup_scope_invalid"); };
9
+ const number = v => { if (!/^\d+$/.test(v)) fail("memory_count", "memory_counter_invalid"); const n=Number(v);
10
+ if (!Number.isSafeInteger(n)) fail("memory_count", "memory_counter_invalid"); return n; };
11
+ const field = (text, key) => { const row=text.split("\n").find(line=>line.split(/\s+/)[0]===key);
12
+ if (!row) fail("memory_field", "memory_field_missing"); return number(row.trim().split(/\s+/)[1]); };
13
+ const membership=read("/proc/self/cgroup", "cgroup_membership_read_failed", false);
14
+ const bareGuest=membership==="";
15
+ let p=bareGuest?null:/^0::([^\n]+)\n?$/.exec(membership)?.[1];
16
+ if (!bareGuest && !p) fail("memory_scope", "cgroup_v2_membership_absent");
17
+ if (p && (!/^\/(?:[A-Za-z0-9_.:@-]+\/?)*$/.test(p) || p.split("/").includes(".."))) fail("memory_scope", "cgroup_scope_invalid");
18
+ if (bareGuest) {
19
+ // Empty membership alone is not proof of a whole VM. Require a complete,
20
+ // bounded kernel mount table with no cgroup hierarchy before using /proc.
21
+ // Reuse the qualified diagnostic enum so rolling clients keep parsing it.
22
+ const rawMounts=read("/proc/self/mountinfo", "cgroup_v2_membership_absent", false);
23
+ const refuse=()=>fail("memory_scope", "cgroup_v2_membership_absent");
24
+ if(!rawMounts || rawMounts.length>262144 || Buffer.byteLength(rawMounts,"utf8")>262144) refuse();
25
+ // A kernel table may end in one LF; never erase malformed whitespace rows.
26
+ const mounts=rawMounts.endsWith("\n")?rawMounts.slice(0,-1):rawMounts;
27
+ const rows=mounts.split("\n"); if(rows.length>4096) refuse();
28
+ const mountIds=new Set();
29
+ const id=v=>/^\d+$/.test(v)&&Number.isSafeInteger(Number(v))&&Number(v)>0;
30
+ const device=v=>/^\d+:\d+$/.test(v)&&v.split(":").every(n=>Number.isSafeInteger(Number(n)));
31
+ for(const row of rows) {
32
+ if(row.length>8192 || Buffer.byteLength(row,"utf8")>8192) refuse();
33
+ const fields=row.split(" "), separator=fields.indexOf("-");
34
+ if(fields.some(v=>!v||/\s/.test(v)) || separator<6 || fields.lastIndexOf("-")!==separator || fields.length!==separator+4
35
+ || !id(fields[0]) || !id(fields[1]) || !device(fields[2])
36
+ || !fields[3].startsWith("/") || !fields[4].startsWith("/")
37
+ || fields[5].split(",").some(v=>!v) || fields[separator+3].split(",").some(v=>!v)
38
+ || !/^[A-Za-z0-9_.+-]+$/.test(fields[separator+1])
39
+ || fields.slice(6,separator).some(v=>! /^[a-z][a-z0-9_]*(?::\d+)?$/.test(v))) refuse();
40
+ if(mountIds.has(Number(fields[0]))) refuse(); mountIds.add(Number(fields[0]));
41
+ if(fields[separator+1]==="cgroup" || fields[separator+1]==="cgroup2") refuse();
42
+ }
43
+ }
44
+ const root="/sys/fs/cgroup"; let selected=null;
45
+ const readCgroup = (p, reasonCode) => { checkPath(p, reasonCode); return read(p, reasonCode); };
46
+ if (!bareGuest) {
47
+ checkPath(root, "cgroup_root_unavailable");
48
+ for (let depth=0; depth<128; depth++) {
49
+ const dir=root+(p==="/"?"":p);
50
+ checkPath(dir, "cgroup_directory_unavailable");
51
+ if (fs.existsSync(dir+"/memory.events") && fs.existsSync(dir+"/memory.max")) {
52
+ const limit=readCgroup(dir+"/memory.max", "cgroup_limit_read_failed"); if(limit!=="max") selected={dir,limit:number(limit)};
53
+ }
54
+ if(p==="/") break;
55
+ p=path.posix.dirname(p); if(depth===127) fail("memory_scope", "cgroup_scope_invalid");
56
+ }
57
+ }
58
+ const bootId=read("/proc/sys/kernel/random/boot_id", "boot_id_read_failed");
59
+ if(!UUID_RE.test(bootId)) fail("memory_boot", "boot_id_invalid");
60
+ if(selected) return {bootId,oomKills:field(readCgroup(selected.dir+"/memory.events", "cgroup_events_read_failed"),"oom_kill"),
61
+ currentBytes:number(readCgroup(selected.dir+"/memory.current", "cgroup_current_read_failed")),limitBytes:selected.limit,
62
+ inactiveFileBytes:field(readCgroup(selected.dir+"/memory.stat", "cgroup_stat_read_failed"),"inactive_file"),capacitySource:"cgroup",memoryScope:selected.dir};
63
+ const info=read("/proc/meminfo", "guest_meminfo_read_failed"); const limitBytes=field(info,"MemTotal:")*1024,available=field(info,"MemAvailable:")*1024;
64
+ if(!Number.isSafeInteger(limitBytes)||limitBytes<=0||!Number.isSafeInteger(available)||available>limitBytes) fail("memory_capacity", "memory_capacity_invalid");
65
+ return {bootId,oomKills:field(read("/proc/vmstat", "guest_vmstat_read_failed"),"oom_kill"),currentBytes:limitBytes-available,
66
+ limitBytes,inactiveFileBytes:0,capacitySource:"guest",memoryScope:"/proc"};
67
+ })`;var r=(t)=>t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/[a-z]/gi,(e)=>`[${e.toLowerCase()}${e.toUpperCase()}]`),U=[...i.map((t)=>`/(${t.map(r).join("/")})(/|$)`),...l.map((t)=>`/(${t.map(r).join("/")})(/|$)`),...[...g].map((t)=>`/(${r(t)})$`),`/${r(".env.")}[^/]*$`,...y.map((t)=>`/[^/]*${r(t)}$`)],n=`function localGithubSandboxProfile(root, temporary) {
68
+ return ['(version 1)','(deny default)','(import "system.sb")','(allow process-exec process-fork)',
69
+ '(allow file-read* (subpath "/bin") (subpath "/sbin") (subpath "/usr/bin") (subpath "/usr/sbin") (subpath "/usr/libexec") (subpath "/usr/share") (subpath "/private/var/select") (literal "/private/var/db/xcode_select_link"))',
70
+ '(allow file-read-metadata (path-ancestors "/opt/homebrew/bin") (path-ancestors "/usr/local/bin") (path-ancestors "/Library/Developer/CommandLineTools"))',
71
+ '(allow file-read* (subpath "/opt/homebrew/Cellar") (subpath "/opt/homebrew/opt") (subpath "/opt/homebrew/bin") (subpath "/opt/homebrew/lib") (subpath "/opt/homebrew/share") (subpath "/opt/homebrew/include") (subpath "/usr/local/Cellar") (subpath "/usr/local/opt") (subpath "/usr/local/bin") (subpath "/usr/local/lib") (subpath "/usr/local/share") (subpath "/usr/local/include") (subpath "/Library/Developer/CommandLineTools"))',
72
+ '(allow file-read* file-write* (subpath '+JSON.stringify(root)+') (subpath '+JSON.stringify(temporary)+'))',
73
+ '(allow file-read-metadata (path-ancestors '+JSON.stringify(root)+') (path-ancestors '+JSON.stringify(temporary)+'))',
74
+ '(deny file-read* file-write* (subpath "/opt/homebrew/etc") (subpath "/usr/local/etc") (literal "/private/etc/master.passwd"))',
75
+ '(deny file-write* (subpath "/opt/homebrew") (subpath "/usr/local") (subpath "/Library/Developer") (subpath "/cores"))',
76
+ ...${JSON.stringify(U)}.map(pattern => '(deny file-read* file-write* (regex #"'+pattern+'"))'),
77
+ '(deny network*)','(deny mach-lookup)','(deny process-info*)','(allow process-info* (target same-sandbox))','(allow process-info-listpids)','(allow file-read-metadata (subpath "/dev"))','(deny signal)','(allow signal (target same-sandbox))'].join('\\n');
78
+ }`,h="/opt/homebrew/bin:/usr/local/bin:/Library/Developer/CommandLineTools/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin";function qt(t,e){return Function("return ("+n+")")()(t,e)+`
79
+ (allow network-bind network-inbound (local tcp "localhost:*"))`}var x=String.raw`import os,sys,json,stat,fcntl,time,ctypes
80
+ v=json.load(sys.stdin)
81
+ root=v['root']; paths=list(dict.fromkeys([v['source'],v['snapshot']]))
82
+ marker=('# reason-job-env-owner:'+v['runId']+'\n').encode()
83
+ fds=[];parents=[]
84
+ try:
85
+ if os.path.realpath(root)!=root:raise ValueError('root')
86
+ rootfd=os.open(root,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
87
+ try:
88
+ r=os.fstat(rootfd)
89
+ if not stat.S_ISDIR(r.st_mode):raise ValueError('root')
90
+ for p in paths:
91
+ parent=os.path.dirname(p)
92
+ if os.path.realpath(parent)!=parent:raise ValueError('parent')
93
+ pfd=os.open(parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW);parents.append((parent,pfd))
94
+ ps=os.fstat(pfd);pn=os.stat(parent,follow_symlinks=False)
95
+ if ps.st_uid!=os.geteuid() or stat.S_IMODE(ps.st_mode)&0o022 or (ps.st_dev,ps.st_ino)!=(pn.st_dev,pn.st_ino):raise ValueError('parent')
96
+ fd=os.open(os.path.basename(p),os.O_RDWR|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=pfd);fds.append((p,fd))
97
+ s=os.fstat(fd)
98
+ if not stat.S_ISREG(s.st_mode) or s.st_nlink!=1 or s.st_uid!=os.geteuid() or stat.S_IMODE(s.st_mode)!=0o600:raise ValueError('file')
99
+ end=time.monotonic()+2
100
+ while True:
101
+ try:fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB);break
102
+ except BlockingIOError:
103
+ if time.monotonic()>=end:raise ValueError('lock')
104
+ time.sleep(.02)
105
+ n=os.stat(p,follow_symlinks=False)
106
+ if (s.st_dev,s.st_ino)!=(n.st_dev,n.st_ino) or os.pread(fd,len(marker),0)!=marker:raise ValueError('owner')
107
+ libc=ctypes.CDLL(None,use_errno=True)
108
+ if libc.syncfs(rootfd)!=0:raise OSError(ctypes.get_errno(),'sync')
109
+ for parent,pfd in parents:
110
+ ps=os.fstat(pfd);pn=os.stat(parent,follow_symlinks=False)
111
+ if (ps.st_dev,ps.st_ino)!=(pn.st_dev,pn.st_ino):raise ValueError('parent_changed')
112
+ for p,fd in fds:
113
+ s=os.fstat(fd);n=os.stat(p,follow_symlinks=False)
114
+ if (s.st_dev,s.st_ino)!=(n.st_dev,n.st_ino):raise ValueError('changed')
115
+ os.ftruncate(fd,0);os.fsync(fd)
116
+ if libc.syncfs(rootfd)!=0:raise OSError(ctypes.get_errno(),'sync')
117
+ finally:os.close(rootfd)
118
+ finally:
119
+ for _,fd in fds:os.close(fd)
120
+ for _,pfd in parents:os.close(pfd)
121
+ `;var s=String.raw`function workspaceTarget(requested) {
122
+ if (typeof requested !== "string" || requested.length < 1 || requested.includes("\0")) {
123
+ throw new Error("workspace_file_path_invalid");
124
+ }
125
+ const root = fs.realpathSync(config.repoPath);
126
+ const target = path.isAbsolute(requested) ? path.resolve(requested) : path.resolve(root, requested);
127
+ const relative = path.relative(root, target);
128
+ if (relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) {
129
+ throw new Error("workspace_file_path_invalid");
130
+ }
131
+ let cursor = root;
132
+ for (const part of relative.split(path.sep).filter(Boolean)) {
133
+ cursor = path.join(cursor, part);
134
+ if (!fs.existsSync(cursor)) continue;
135
+ if (fs.lstatSync(cursor).isSymbolicLink()) throw new Error("workspace_file_symlink_rejected");
136
+ }
137
+ return { root, target, relative: relative.split(path.sep).join("/") || "." };
138
+ }
139
+ function atomicWrite(target, content) {
140
+ fs.mkdirSync(path.dirname(target), { recursive: true });
141
+ const temporary = target + ".ara-" + crypto.randomUUID() + ".tmp";
142
+ try {
143
+ const mode = fs.existsSync(target) ? fs.statSync(target).mode & 0o777 : 0o644;
144
+ fs.writeFileSync(temporary, content, { encoding: "utf8", mode });
145
+ fs.renameSync(temporary, target);
146
+ } finally {
147
+ try { fs.unlinkSync(temporary); } catch (error) { void error; }
148
+ }
149
+ }
150
+ const fileMutationQueues = new Map();
151
+ function withFileMutationQueue(target, operation) {
152
+ const previous = fileMutationQueues.get(target) || Promise.resolve();
153
+ const queued = previous.catch(() => {}).then(operation);
154
+ fileMutationQueues.set(target, queued);
155
+ return queued.finally(() => {
156
+ if (fileMutationQueues.get(target) === queued) fileMutationQueues.delete(target);
157
+ });
158
+ }
159
+ function startsWith(bytes, signature) {
160
+ return bytes.length >= signature.length
161
+ && signature.every((byte, index) => bytes[index] === byte);
162
+ }
163
+ function startsWithAscii(bytes, offset, value) {
164
+ if (bytes.length < offset + value.length) return false;
165
+ for (let index = 0; index < value.length; index += 1) {
166
+ if (bytes[offset + index] !== value.charCodeAt(index)) return false;
167
+ }
168
+ return true;
169
+ }
170
+ function imageMimeType(bytes) {
171
+ if (startsWith(bytes, [0xff, 0xd8, 0xff]) && bytes[3] !== 0xf7) return "image/jpeg";
172
+ if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png";
173
+ if (startsWithAscii(bytes, 0, "GIF")) return "image/gif";
174
+ if (startsWithAscii(bytes, 0, "RIFF") && startsWithAscii(bytes, 8, "WEBP")) return "image/webp";
175
+ if (startsWithAscii(bytes, 0, "BM")) return "image/bmp";
176
+ return null;
177
+ }
178
+ function linesForCounting(content) {
179
+ if (content.length === 0) return [];
180
+ const lines = content.split("\n");
181
+ if (content.endsWith("\n")) lines.pop();
182
+ return lines;
183
+ }
184
+ function truncateReadHead(content) {
185
+ const maxLines = 2000;
186
+ const maxBytes = 50 * 1024;
187
+ const lines = linesForCounting(content);
188
+ const totalBytes = Buffer.byteLength(content, "utf8");
189
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
190
+ return {
191
+ content,
192
+ truncated: false,
193
+ truncatedBy: null,
194
+ outputLines: lines.length,
195
+ firstLineExceedsLimit: false,
196
+ };
197
+ }
198
+ const firstLineBytes = Buffer.byteLength(lines[0] || "", "utf8");
199
+ if (firstLineBytes > maxBytes) {
200
+ return {
201
+ content: "",
202
+ truncated: true,
203
+ truncatedBy: "bytes",
204
+ outputLines: 0,
205
+ firstLineExceedsLimit: true,
206
+ };
207
+ }
208
+ const output = [];
209
+ let outputBytes = 0;
210
+ let truncatedBy = "lines";
211
+ for (let index = 0; index < lines.length && index < maxLines; index += 1) {
212
+ const line = lines[index];
213
+ const lineBytes = Buffer.byteLength(line, "utf8") + (index > 0 ? 1 : 0);
214
+ if (outputBytes + lineBytes > maxBytes) {
215
+ truncatedBy = "bytes";
216
+ break;
217
+ }
218
+ output.push(line);
219
+ outputBytes += lineBytes;
220
+ }
221
+ return {
222
+ content: output.join("\n"),
223
+ truncated: true,
224
+ truncatedBy,
225
+ outputLines: output.length,
226
+ firstLineExceedsLimit: false,
227
+ };
228
+ }
229
+ function fileOperation(request) {
230
+ const promise = Promise.resolve().then(async () => {
231
+ let resolved;
232
+ try {
233
+ resolved = workspaceTarget(request.input.path);
234
+ } catch (error) {
235
+ return failure(request.id, 1, "command_failed", "", error instanceof Error ? error.message : "workspace_file_failed");
236
+ }
237
+ const execute = () => {
238
+ try {
239
+ if (request.tool === "read") {
240
+ if (!fs.existsSync(resolved.target) || !fs.statSync(resolved.target).isFile()) throw new Error("workspace_file_not_found");
241
+ const bytes = fs.readFileSync(resolved.target);
242
+ const mimeType = imageMimeType(bytes.subarray(0, 4100));
243
+ if (mimeType) {
244
+ const output = JSON.stringify({
245
+ type: "image",
246
+ path: resolved.relative,
247
+ mimeType,
248
+ data: bytes.toString("base64"),
249
+ });
250
+ if (output.length > MAX_OUTPUT_CHARS) throw new Error("workspace_image_too_large");
251
+ return success(request.id, output, "");
252
+ }
253
+ const original = bytes.toString("utf8");
254
+ const lines = original.split("\n");
255
+ const start = request.input.offset || 1;
256
+ const limit = request.input.limit === undefined
257
+ ? lines.length
258
+ : request.input.limit;
259
+ const startIndex = start - 1;
260
+ if (startIndex >= lines.length) {
261
+ throw new Error("Offset " + start + " is beyond end of file (" + lines.length + " lines total)");
262
+ }
263
+ const selected = lines.slice(startIndex, startIndex + limit);
264
+ const selectedContent = selected.join("\n");
265
+ const truncated = truncateReadHead(selectedContent);
266
+ const hasMore = startIndex + selected.length < lines.length;
267
+ const end = truncated.truncated
268
+ ? start + truncated.outputLines - 1
269
+ : start + selected.length - 1;
270
+ const output = JSON.stringify({
271
+ type: "text",
272
+ path: resolved.relative,
273
+ content: truncated.content,
274
+ startLine: start,
275
+ endLine: end,
276
+ totalLines: lines.length,
277
+ truncated: truncated.truncated || hasMore,
278
+ truncatedBy: truncated.truncatedBy,
279
+ firstLineExceedsLimit: truncated.firstLineExceedsLimit,
280
+ firstLineBytes: Buffer.byteLength(lines[startIndex] || "", "utf8"),
281
+ });
282
+ return success(request.id, output, "");
283
+ }
284
+ if (request.tool === "write") {
285
+ atomicWrite(resolved.target, request.input.content);
286
+ return success(request.id, JSON.stringify({
287
+ type: "write",
288
+ path: resolved.relative,
289
+ bytes: Buffer.byteLength(request.input.content, "utf8"),
290
+ }), "");
291
+ }
292
+ const original = fs.readFileSync(resolved.target, "utf8");
293
+ const matches = request.input.edits.map((edit) => {
294
+ const first = original.indexOf(edit.oldText);
295
+ if (first < 0) throw new Error("workspace_file_old_text_not_found");
296
+ if (original.indexOf(edit.oldText, first + edit.oldText.length) >= 0) throw new Error("workspace_file_old_text_not_unique");
297
+ return { ...edit, start: first, end: first + edit.oldText.length };
298
+ }).sort((left, right) => left.start - right.start);
299
+ for (let index = 1; index < matches.length; index += 1) {
300
+ if (matches[index].start < matches[index - 1].end) throw new Error("workspace_file_edits_overlap");
301
+ }
302
+ let updated = original;
303
+ for (const match of [...matches].reverse()) {
304
+ updated = updated.slice(0, match.start) + match.newText + updated.slice(match.end);
305
+ }
306
+ atomicWrite(resolved.target, updated);
307
+ return success(request.id, JSON.stringify({
308
+ type: "edit",
309
+ path: resolved.relative,
310
+ replacements: matches.length,
311
+ beforeSha256: crypto.createHash("sha256").update(original).digest("hex"),
312
+ afterSha256: crypto.createHash("sha256").update(updated).digest("hex"),
313
+ }), "");
314
+ } catch (error) {
315
+ return failure(request.id, 1, "command_failed", "", error instanceof Error ? error.message : "workspace_file_failed");
316
+ }
317
+ };
318
+ return request.tool === "read"
319
+ ? execute()
320
+ : await withFileMutationQueue(resolved.target, execute);
321
+ });
322
+ return { promise, cancel() {} };
323
+ }
324
+ `;var p="a3e39eb55160acfc116fa9871ccc594c68be7a642d2331b24baa707c8f601ec0";import{createHash as ot}from"crypto";var Ft=Object.freeze({"api.ara.so":"api.reasonmachines.com","api.reasonmachines.ai":"api.reasonmachines.com","api-staging.ara.so":"staging-api.reasonmachines.com","staging-api.reasonmachines.ai":"staging-api.reasonmachines.com"});var w=Object.freeze({trailingSlashPattern:/\/+$/.source,keyPrefix:"http.",keySuffix:"/.extraheader",valuePrefix:"Authorization: Bearer "});var q=Object.freeze({tokenEnv:"ARA_GIT_BROKER_TOKEN",reasonTokenEnv:"REASON_GIT_BROKER_TOKEN",urlEnv:"ARA_GIT_BROKER_URL",keyPrefix:"credential.",keySuffix:"/.helper",value:'!f(){ printf "username=%s\\npassword=%s\\n" "ara-run" "${REASON_GIT_BROKER_TOKEN:-$ARA_GIT_BROKER_TOKEN}"; };f'});var o=/^[A-Za-z_][A-Za-z0-9_]*$/;var N=new Set(["ARA_INFERENCE_TOKEN","AI_GATEWAY_API_KEY","CLOUD_AGENT_AI_GATEWAY_API_KEY","INFERENCE_V2_GATEWAY_API_KEY","VERCEL_AI_GATEWAY_API_KEY","INFERENCE_V2_CONCENTRATE_API_KEY","INFERENCE_V2_OPENAI_API_KEY","INFERENCE_V2_ANTHROPIC_API_KEY","INFERENCE_V2_FINGERPRINT_SECRET","OPENAI_API_KEY","OPENROUTER_API_KEY","ANTHROPIC_API_KEY","CODEX_ACCESS_TOKEN","GITHUB_COPILOT_API_KEY","AZURE_OPENAI_API_KEY","AZURE_OPENAI_CLIENT_SECRET"]),R=new Set(["ARA_OPERATOR_KEY","BL_API_KEY","BLAXEL_API_KEY","NSC_TOKEN","NSC_TOKEN_FILE","NAMESPACE_API_KEY","NAMESPACE_ACCESS_TOKEN","NAMESPACE_TOKEN","NAMESPACE_TOKEN_JSON","NAMESPACE_SANDBOX_TOKEN_JSON","ARA_LOCAL_QA_NAMESPACE_WORKLOAD_TOKEN_FILE","NAMESPACE_SANDBOX_USE_LOCAL_USER_TOKEN","COMPUTE_SERVICE_TOKEN"]),v=["INFISICAL_","NSC_"],E=new Set(["ARA_JOB_ENV","ARA_DELIVERY_BASE_SHA","WORK_ROOT","REPO_DIR","ARA_CONTROL_DIR","ARA_PRESERVE_WORKSPACE","ARA_PINNED_CHECKOUT_REUSE","FOLLOWUP_BRANCH","REPO_URL","TOKENLESS_URL","REPOSITORY_IDENTITY_URL","DEFAULT_BRANCH","BRANCH","BRANCH_GENERATED","BASE_REF","REPO_EMPTY","REPO_WRITABLE","ARA_DELIVERY_BASE_BRANCH","ARA_SERVER_GIT_PUBLICATION","ARA_EMPTY_REPOSITORY_COMMIT","REASON_MODEL","REASON_DELIVERY_BASE_BRANCH","REASON_DELIVERY_BASE_SHA","REASON_EMPTY_REPOSITORY_COMMIT","REASON_SCRATCH_SESSION_ID","REASON_SERVER_GIT_PUBLICATION","REASON_ALLOW_INSTALL_SCRIPTS","SCM_TOKEN","SCM_TOKEN_USER","REASON_OIDC_TOKEN_URL","REASON_OIDC_REQUEST_TOKEN","AWS_CONTAINER_CREDENTIALS_FULL_URI","AWS_CONTAINER_AUTHORIZATION_TOKEN","AWS_CONTAINER_CREDENTIALS_RELATIVE_URI","AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE","BASH_ENV","ENV","BASHOPTS","SHELLOPTS","PS4","IFS","CDPATH","GLOBIGNORE","PROMPT_COMMAND","PATH","HOME","TMPDIR","TMP","TEMP","XDG_CONFIG_HOME","GIT_DIR","GIT_WORK_TREE","GIT_INDEX_FILE","GIT_OBJECT_DIRECTORY","GIT_COMMON_DIR","GIT_NAMESPACE","GIT_CEILING_DIRECTORIES","GIT_DISCOVERY_ACROSS_FILESYSTEM","GIT_SHALLOW_FILE","GIT_ALLOW_PROTOCOL","GIT_PROTOCOL_FROM_USER","GIT_REPLACE_REF_BASE","NODE_OPTIONS","NODE_PATH","NODE_REPL_EXTERNAL_MODULES","CLASSPATH","PYTHONPATH","PYTHONSTARTUP","PYTHONHOME","RUBYOPT","RUBYLIB","PERL5LIB","PERL5OPT","JAVA_TOOL_OPTIONS","_JAVA_OPTIONS","JDK_JAVA_OPTIONS","GEM_PATH","GEM_HOME","GIT_SSH","GIT_SSH_COMMAND","GIT_EXEC_PATH","GIT_TEMPLATE_DIR","GIT_ALTERNATE_OBJECT_DIRECTORIES","GIT_PROXY_COMMAND","GIT_EXTERNAL_DIFF","GIT_PAGER","GIT_EDITOR","GIT_ASKPASS","SSH_ASKPASS","PAGER","EDITOR","TAR_OPTIONS","CURL_HOME","WGETRC","APT_CONFIG","PERLLIB","GIT_GRAFT_FILE","POSIXLY_CORRECT"]),tt=["GIT_TRACE","CONTEXT_REPO_","ARA_CHECKPOINT_BOOTSTRAP_","_","ARA_HANDS_","ARA_DEVICE_","ARA_GIT_BROKER_","REASON_HANDS_","REASON_DEVICE_","REASON_GIT_BROKER_","LD_","DYLD_","GIT_CONFIG","BASH_FUNC_"];var u="^(?!(?:"+[...N,...R,...E].join("|")+")$)(?!(?:"+[...v,...tt].join("|")+"))"+o.source.slice(1),zt=new RegExp(u);var et=["HOME","PWD","OLDPWD","TMPDIR","TMP","TEMP","SHELL","USER","LOGNAME","SCM_TOKEN","SCM_TOKEN_USER","GH_TOKEN","GITHUB_TOKEN","GH_ENTERPRISE_TOKEN","GITHUB_ENTERPRISE_TOKEN","GITLAB_TOKEN"],rt=["ARA_HANDS_","ARA_DEVICE_","ARA_GIT_BROKER_","REASON_HANDS_","REASON_DEVICE_","REASON_GIT_BROKER_","BUN_"],it="^(?!(?:"+et.join("|")+")$)(?!(?:"+rt.join("|")+"))"+u.slice(1),nt="ARA_DELIVERY_BASE_SHA|WORK_ROOT|FOLLOWUP_BRANCH|ARA_PRESERVE_WORKSPACE|ARA_PINNED_CHECKOUT_REUSE|CONTEXT_REPO_[A-Z0-9_]+|REPO_URL|TOKENLESS_URL|REPOSITORY_IDENTITY_URL|DEFAULT_BRANCH|BRANCH|BRANCH_GENERATED|BASE_REF|REPO_EMPTY|REPO_WRITABLE|ARA_DELIVERY_BASE_BRANCH|ARA_SERVER_GIT_PUBLICATION|ARA_EMPTY_REPOSITORY_COMMIT|REASON_MODEL|REASON_DELIVERY_BASE_BRANCH|REASON_DELIVERY_BASE_SHA|REASON_EMPTY_REPOSITORY_COMMIT|REASON_SCRATCH_SESSION_ID|REASON_SERVER_GIT_PUBLICATION|REASON_ALLOW_INSTALL_SCRIPTS",c="^(?:"+nt+"|"+it.slice(1,-1)+")$",st=new RegExp(c);function kt(t){return st.test(t)}var b=Object.freeze({tokenPath:"/identity/token",awsPath:"/identity/aws",tokenPattern:/^[A-Za-z0-9_.~-]+$/.source}),m=Object.freeze({variables:256,valueBytes:130816,snapshotBytes:1048576}),Gt=Object.freeze({valueChars:16384,snapshotBytes:65536});var B=["set -g history-limit 50000","set -g status off","set -g window-size smallest","set -gq fill-character ' '","set -g escape-time 10","set -g prefix None","set -g prefix2 None","set -g terminal-overrides ',*:smcup@:rmcup@'"];var ut=2,ct="ara-hands-worker.cjs",A=["/usr/local/go/bin","/opt/node/bin","/usr/local/bin","/usr/sbin","/usr/bin","/sbin","/bin","/usr/local/cargo/bin"],ft=A.join(":"),dt=["/opt/homebrew/bin",...A].join(":"),at='(platform, linuxPath, macosPath) => platform === "darwin" ? macosPath : linuxPath';function Nt(t,e,f){return t==="darwin"?f:e}var lt=String.raw`(function(anchor, pid, native) {
325
+ const token = native.identity(pid);
326
+ const session = native.session(pid);
327
+ if (!native.same(anchor)) throw new Error("pty_session_identity_lost");
328
+ if (session.id === -1 && session.missing) return { retry: true, token: null };
329
+ if (session.id !== anchor[5]) throw new Error("pty_member_session_changed");
330
+ if (!token) return { retry: true, token: null };
331
+ return { retry: false, token };
332
+ })`,gt=String.raw`(function(native) {
333
+ let probe;
334
+ try {
335
+ try {
336
+ probe = native.identity();
337
+ if (!probe) throw new Error("pty_pidfd_unavailable");
338
+ native.children();
339
+ native.signal(probe);
340
+ } finally { if (probe) native.release(probe); }
341
+ } catch (error) { native.close(); throw error; }
342
+ })`,yt=String.raw`(function(bootstrap) {
343
+ const { dlopen, ptr } = require("bun:ffi");
344
+ const api = dlopen("libc.so.6", {
345
+ prctl: { args: ["i32", "u64", "u64", "u64", "u64"], returns: "i32" },
346
+ execve: { args: ["ptr", "ptr", "ptr"], returns: "i32" },
347
+ }).symbols;
348
+ const bit = new Int32Array(1);
349
+ if (api.prctl(36, 1, 0, 0, 0) !== 0 || api.prctl(37, ptr(bit), 0, 0, 0) !== 0 || bit[0] !== 1) {
350
+ throw new Error("pty_subreaper_unavailable");
351
+ }
352
+ const args = ["/bin/bash", "--noprofile", "--norc", "-c", bootstrap].map(value => Buffer.from(value + "\0"));
353
+ // Keep the serialized program distinct from native runtime lookup bytes:
354
+ // Mac release hardening removes those native lookups before signing.
355
+ const runtimeModeKey = ["BUN", "BE", "BUN"].join("_");
356
+ const env = Object.entries(process.env).filter(([name]) => name !== runtimeModeKey)
357
+ .map(([name, value]) => Buffer.from(name + "=" + value + "\0"));
358
+ const argv = new BigUint64Array([...args.map(value => BigInt(ptr(value))), 0n]);
359
+ const envp = new BigUint64Array([...env.map(value => BigInt(ptr(value))), 0n]);
360
+ require("node:fs").writeSync(3, "subreaper\n");
361
+ api.execve(ptr(args[0]), ptr(argv), ptr(envp));
362
+ throw new Error("pty_exec_failed");
363
+ })`,ht=String.raw`
364
+ "use strict";
365
+ const crypto = require("node:crypto");
366
+ const fs = require("node:fs");
367
+ const net = require("node:net");
368
+ const path = require("node:path");
369
+ const { spawn, spawnSync } = require("node:child_process");
370
+ const { StringDecoder } = require("node:string_decoder");
371
+ const GIT_BROKER_HTTP_HEADER_POLICY = ${JSON.stringify(w)};
372
+ const GIT_BROKER_CREDENTIAL_HELPER_POLICY = ${JSON.stringify(q)};
373
+ const DEVICE_ENVIRONMENT_NAME_RE = new RegExp(${JSON.stringify(c)});
374
+ const DEVICE_CLOUD_ACCESS_POLICY = ${JSON.stringify(b)};
375
+ const DEVICE_ENVIRONMENT_LIMITS = ${JSON.stringify(m)};
376
+
377
+ const VERSION = ${z};
378
+ const WORKER_VERSION = ${ut};
379
+ const WORKER_ENTRYPOINT = ${JSON.stringify(ct)};
380
+ const GUEST_MEMORY_UNAVAILABLE_REASONS = ${JSON.stringify(d)};
381
+ const sampleGuestMemory = ${a};
382
+ const inspectMacPtyMember = ${lt};
383
+ const probeLinuxPtyLibrary = ${gt};
384
+ const LINUX_PTY_EXEC_SOURCE = ${JSON.stringify(yt)};
385
+ const CLOSE_CODES = ${JSON.stringify(O)};
386
+ const SHA256_RE = /^[0-9a-f]{64}$/;
387
+ const MAX_FRAME_BYTES = ${Z};
388
+ const MAX_INPUT_CHARS = ${K};
389
+ const MAX_STRUCTURED_INPUT_CHARS = ${$};
390
+ const MAX_OUTPUT_CHARS = ${k};
391
+ const MAX_ERROR_CHARS = ${G};
392
+ const MAX_LEDGER_IDS = 16384;
393
+ const MAX_CACHED_RESULTS = 256;
394
+ const MAX_JOB_INPUT_CHARS = ${X};
395
+ const MAX_JOB_OUTPUT_CHARS = ${M};
396
+ const MAX_JOB_OUTPUT_CHUNKS = ${Y};
397
+ const MAX_JOBS = ${H};
398
+ const MAX_JOB_RECORD_BYTES = 262144;
399
+ const MAX_JOB_JOURNAL_BYTES = MAX_JOBS * MAX_JOB_RECORD_BYTES;
400
+ const MAX_JOB_CONTROL_FRAME_BYTES = ${V};
401
+ const MAX_PTY_INPUT_BYTES = ${S};
402
+ const MAX_PTY_OUTPUT_BYTES = ${_};
403
+ const MAX_PTY_DIMENSION = ${P};
404
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/;
405
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
406
+ const FIXED_BASH = "/bin/bash";
407
+ /** A Reason cloud machine's memory ceiling, which guestd records on every
408
+ * start (/run/sc/memory-max-mib), or 0 elsewhere. Read per command: a resize
409
+ * changes it. */
410
+ function machineMemoryMaxMiB() {
411
+ try {
412
+ const file = validPath(config.machineMemoryMaxPath) ? config.machineMemoryMaxPath : "/run/sc/memory-max-mib";
413
+ const text = fs.readFileSync(file, "utf8").trim();
414
+ return /^[1-9][0-9]{2,6}$/.test(text) ? Number(text) : 0;
415
+ } catch {
416
+ return 0;
417
+ }
418
+ }
419
+ /** The shell prelude that sizes the command for the memory the machine can
420
+ * grow to. Node takes a quarter of the memory it sees at start as its heap
421
+ * limit, and a cloud machine starts small and grows on demand, so without
422
+ * this a TypeScript build stops at a 512 MiB heap while gigabytes could be
423
+ * plugged in. NODE_OPTIONS is otherwise unset for every command; this sets
424
+ * only the heap size, a quarter of the ceiling as on a machine that size,
425
+ * and publishes the ceiling as REASON_MEMORY_MAX_MIB. A command's own
426
+ * NODE_OPTIONS still wins. Empty off a cloud machine. */
427
+ function machineMemoryPrelude() {
428
+ const max = machineMemoryMaxMiB();
429
+ return max ? "export NODE_OPTIONS=--max-old-space-size=" + Math.max(2048, Math.floor(max / 4)) + " REASON_MEMORY_MAX_MIB=" + max + "; " : "";
430
+ }
431
+ /** config.jobsUser: the argv prefix that runs a job as the payload account, never root.
432
+ * A script job (model: true) takes the egress guard's model-jobs group when there is one. */
433
+ function jobsUserPrefix(model) {
434
+ const user = config.jobsUser;
435
+ const gid = model && user && user.modelGid !== undefined ? user.modelGid : user && user.gid;
436
+ return user ? ["/usr/bin/setpriv", "--reuid=" + user.uid, "--regid=" + gid, "--clear-groups", "--no-new-privs", "--",
437
+ "/usr/bin/env", "HOME=" + user.home, "USER=" + user.username, "LOGNAME=" + user.username] : [];
438
+ }
439
+ const FIXED_CLOUD_CHILD_PATH = ${JSON.stringify(ft)};
440
+ const FIXED_MACOS_CLOUD_CHILD_PATH = ${JSON.stringify(dt)};
441
+ const selectCloudChildPath = ${at};
442
+ const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
443
+ const allowedTools = config.allowedTools === undefined
444
+ ? ["bash", "repl", "read", "edit", "write"]
445
+ : config.allowedTools;
446
+ // Local GitHub authority confines a Session's Bash and file access under
447
+ // sandbox-exec on macOS. A personal Linux worker runs every Session's tools
448
+ // with the person's own permissions (host mode, which already reach their gh
449
+ // login and the network), so there it keeps the ordinary tool paths.
450
+ const localGithubSandbox = config.localGithubConfinement === true && process.platform === "darwin";
451
+
452
+ function validPath(value) {
453
+ return typeof value === "string" && path.isAbsolute(value) && !value.includes("\0");
454
+ }
455
+ function validTimeout(value) {
456
+ return Number.isSafeInteger(value) && value >= 50 && value <= 600000;
457
+ }
458
+ function validHostVariables(variables) {
459
+ if (!variables || typeof variables !== "object" || Array.isArray(variables)) return false;
460
+ const entries = Object.entries(variables);
461
+ return entries.length <= 512 && entries.every(([name, value]) => name.length <= 128 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)
462
+ && typeof value === "string" && value.length <= 32768 && !value.includes("\0"));
463
+ }
464
+ function validEnvironmentConfiguration() {
465
+ if (config.localGithubConfinement === true && config.environmentSnapshotPath === undefined) return false;
466
+ if (config.gitCredentialEnvironment !== undefined && (config.gitCredentialEnvironment !== true
467
+ || config.environmentSnapshotPath === undefined || config.localGithubConfinement === true)) return false;
468
+ const baseline = config.executionEnvironment;
469
+ if (baseline !== undefined && (config.environmentSnapshotPath === undefined
470
+ || !hasOnlyKeys(baseline, ["home", "path", "temporaryDirectory", "variables"])
471
+ || (baseline.variables !== undefined && !validHostVariables(baseline.variables))
472
+ || ![baseline.home, baseline.temporaryDirectory].every(value => validPath(value) && value.length <= 32768 && !/[\r\n]/.test(value))
473
+ || typeof baseline.path !== "string" || baseline.path.length > 32768
474
+ || !baseline.path.split(":").every(value => validPath(value) && !/[\r\n]/.test(value)))) return false;
475
+ if (config.environmentSnapshotPath === undefined) return config.environmentExpectedBinding === undefined;
476
+ const authority = config.environmentExpectedBinding;
477
+ if (!validPath(config.environmentSnapshotPath) || path.basename(config.environmentSnapshotPath) !== "environment.json"
478
+ || config.jobEnvSourcePath !== undefined
479
+ || !hasOnlyKeys(authority, ["runId", "deviceId", "rootId", "apiOrigin", "binding"])
480
+ || typeof authority.runId !== "string" || authority.runId.length < 1 || authority.runId.length > 160
481
+ || !UUID_RE.test(authority.deviceId) || !UUID_RE.test(authority.rootId)
482
+ || !hasOnlyKeys(authority.binding, ["kind", "target_revision", "root_grant_revision"])
483
+ || !["workspace_source", "mcp_control"].includes(authority.binding.kind)
484
+ || !Number.isSafeInteger(authority.binding.target_revision) || authority.binding.target_revision < 0
485
+ || !Number.isSafeInteger(authority.binding.root_grant_revision) || authority.binding.root_grant_revision < 1) return false;
486
+ try {
487
+ const origin = new URL(authority.apiOrigin);
488
+ return ["https:", "http:"].includes(origin.protocol) && !origin.username && !origin.password
489
+ && origin.pathname === "/" && !origin.search && !origin.hash;
490
+ } catch { return false; }
491
+ }
492
+ let parsedUrl;
493
+ try { parsedUrl = new URL(config.url); } catch { parsedUrl = null; }
494
+ const validLoopbackRelay = config.transport === "sandbox_loopback"
495
+ && parsedUrl && parsedUrl.protocol === "ws:"
496
+ && parsedUrl.hostname === "127.0.0.1"
497
+ && !parsedUrl.username && !parsedUrl.password && !parsedUrl.search && !parsedUrl.hash;
498
+ const validDeviceRelay = config.transport === "ara_device_wss"
499
+ && parsedUrl
500
+ && ((parsedUrl.protocol === "wss:"
501
+ && parsedUrl.hostname !== "127.0.0.1" && parsedUrl.hostname !== "localhost")
502
+ || (parsedUrl.protocol === "ws:"
503
+ && (parsedUrl.hostname === "127.0.0.1" || parsedUrl.hostname === "localhost")))
504
+ && !parsedUrl.username && !parsedUrl.password && !parsedUrl.hash;
505
+ const validPrivateJob = config.transport === "local_job_ipc" && config.url === ""
506
+ && config.proxyUrl === undefined && config.managedJobs && config.jobControlSocketPath
507
+ && config.allowedTools?.length === 1 && config.allowedTools[0] === "bash"
508
+ && config.workRoot === config.workspaceAliasPath && config.environmentSnapshotPath === undefined
509
+ && config.jobEnvSourcePath === undefined && config.spawnPolicy === undefined && config.managedPty === undefined
510
+ && hasOnlyKeys(config.directShell, ["mode", "executable", "jobId"]) && UUID_RE.test(config.directShell.jobId)
511
+ && validPath(config.directShell.executable) && ["bash", "zsh"].includes(path.basename(config.directShell.executable))
512
+ && (config.directShell.mode === "host" ? config.bashCommandPrefix === undefined
513
+ : config.directShell.mode === "confined" && process.platform === "darwin"
514
+ && config.bashCommandPrefix?.length === 3 && config.bashCommandPrefix[0] === "/usr/bin/sandbox-exec"
515
+ && config.bashCommandPrefix[1] === "-p");
516
+ let validProxyUrl = config.proxyUrl === undefined;
517
+ if (!validProxyUrl && config.transport === "ara_device_wss" && typeof config.proxyUrl === "string") {
518
+ try {
519
+ const proxyParsed = new URL(config.proxyUrl);
520
+ validProxyUrl = (proxyParsed.protocol === "https:" || proxyParsed.protocol === "http:")
521
+ && proxyParsed.hostname !== "";
522
+ } catch { validProxyUrl = false; }
523
+ }
524
+ function validJobEnvSourcePath() {
525
+ if (config.jobEnvSourcePath === undefined || config.jobEnvSourcePath === path.join(config.workRoot, "job.env")) return true;
526
+ // A cloud workspace's job.env is in its Session's root-only control
527
+ // directory (shared/device-workspace.ts), derived from the root alone.
528
+ const cloud = new RegExp(${JSON.stringify(j.source)}).exec(config.workRoot);
529
+ if (process.platform === "linux" && cloud) {
530
+ const workspaceId = cloud[3];
531
+ const owner = (new RegExp(${JSON.stringify(W.source)}).exec(workspaceId) || [])[1]
532
+ ?? (new RegExp(${JSON.stringify(F.source)}).exec(workspaceId) || [])[1] ?? workspaceId;
533
+ return config.jobEnvSourcePath === ${JSON.stringify(L)} + "/" + owner + "/job.env";
534
+ }
535
+ // Personal Cloud controls are root-owned and outside the writable grant.
536
+ // Admit only the same canonical session, never a caller-selected private path.
537
+ const session = /^\/var\/lib\/reason\/sessions\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(config.workRoot);
538
+ return process.platform === "linux" && session !== null
539
+ && config.jobEnvSourcePath === "/var/lib/reason/session-controls/" + session[1] + "/job.env";
540
+ }
541
+ if (
542
+ typeof WebSocket !== "function" ||
543
+ (!validLoopbackRelay && !validDeviceRelay && !validPrivateJob) || !validProxyUrl ||
544
+ (config.directShell !== undefined && !validPrivateJob) ||
545
+ (config.directTerminalReceiptPath !== undefined && (!validPrivateJob
546
+ || config.directTerminalReceiptPath !== path.join(path.dirname(config.readyPath), "terminal.json"))) ||
547
+ (!validPrivateJob && !/^\/device\/[A-Za-z0-9-]+\/[A-Za-z0-9]+$/.test(parsedUrl.pathname)) ||
548
+ config.workerVersion !== WORKER_VERSION || config.protocolVersion !== VERSION ||
549
+ typeof config.workerArtifactSha256 !== "string" || !SHA256_RE.test(config.workerArtifactSha256) ||
550
+ config.workerEntrypoint !== WORKER_ENTRYPOINT ||
551
+ !Number.isSafeInteger(config.targetGeneration) || config.targetGeneration < 1 ||
552
+ typeof config.fenceId !== "string" || !UUID_RE.test(config.fenceId) ||
553
+ !validPath(config.workRoot) || !validPath(config.workspaceAliasPath) ||
554
+ (config.deviceRootPath !== undefined && !validPath(config.deviceRootPath)) ||
555
+ (config.workspacePoolPath !== undefined && !validPath(config.workspacePoolPath)) ||
556
+ !validPath(config.jobEnvPath) || !validPath(config.repoPath) ||
557
+ !validEnvironmentConfiguration() ||
558
+ !validJobEnvSourcePath() ||
559
+ !validPath(config.replSocketPath) || !validPath(config.readyPath) ||
560
+ (config.terminalCleanupRunId !== undefined && (!UUID_RE.test(config.terminalCleanupRunId) || config.terminalCleanupRunId !== config.browserRunId)) ||
561
+ (config.terminalDaemonNonce !== undefined && (!UUID_RE.test(config.terminalDaemonNonce) || !config.terminalCleanupRunId)) ||
562
+ (config.browserRunId !== undefined && (typeof config.browserRunId !== "string" || !UUID_RE.test(config.browserRunId))) ||
563
+ config.workRoot === "/" || config.workspaceAliasPath === "/" ||
564
+ (config.workRoot !== config.workspaceAliasPath && (
565
+ config.workRoot.startsWith(config.workspaceAliasPath + "/") ||
566
+ config.workspaceAliasPath.startsWith(config.workRoot + "/")
567
+ )) ||
568
+ typeof config.launchId !== "string" || !/^[a-f0-9-]{36}$/.test(config.launchId) ||
569
+ !Number.isSafeInteger(config.expiresAtMs) || config.expiresAtMs <= Date.now() ||
570
+ !validTimeout(config.bashTimeoutMs) || !validTimeout(config.replTimeoutMs) ||
571
+ // An argument may be a whole sandbox-exec profile: a local GitHub Session's
572
+ // dev server's (shared/local-gh-confinement.ts) is about 7.5 KB.
573
+ (config.bashCommandPrefix !== undefined && (
574
+ !Array.isArray(config.bashCommandPrefix) || config.bashCommandPrefix.length < 1 ||
575
+ config.bashCommandPrefix.length > 32 || !validPath(config.bashCommandPrefix[0]) ||
576
+ config.bashCommandPrefix.some((arg) => typeof arg !== "string" || arg.length > 32768 || arg.includes("\0"))
577
+ )) ||
578
+ (config.managedJobs !== undefined && (
579
+ !config.managedJobs || !validPath(config.managedJobs.journalDirectory) ||
580
+ typeof config.managedJobs.scopeId !== "string" || !ID_RE.test(config.managedJobs.scopeId)
581
+ )) ||
582
+ (config.jobsUser !== undefined && (!hasOnlyKeys(config.jobsUser, ["uid", "gid", "home", "username", "modelGid"])
583
+ || (config.jobsUser.modelGid !== undefined && (!Number.isSafeInteger(config.jobsUser.modelGid) || config.jobsUser.modelGid < 1))
584
+ || process.platform !== "linux" || process.getuid() !== 0
585
+ || !Number.isSafeInteger(config.jobsUser.uid) || config.jobsUser.uid < 1
586
+ || !Number.isSafeInteger(config.jobsUser.gid) || config.jobsUser.gid < 1
587
+ || typeof config.jobsUser.home !== "string" || !/^\/[A-Za-z0-9._\/-]+$/.test(config.jobsUser.home) || config.jobsUser.home.includes("..")
588
+ || typeof config.jobsUser.username !== "string" || !/^[a-z_][a-z0-9_-]{0,31}$/i.test(config.jobsUser.username))) ||
589
+ (config.managedPty !== undefined && (!hasOnlyKeys(config.managedPty, ["version", "persistent", "shell"])
590
+ || config.managedPty.version !== 1 || ![undefined, true].includes(config.managedPty.persistent) || !config.managedJobs || config.environmentSnapshotPath === undefined
591
+ || (config.managedPty.shell !== undefined && (process.platform !== "darwin" || typeof config.managedPty.shell !== "string"
592
+ || !/^\/[A-Za-z0-9._\/-]*\/(?:bash|zsh)$/.test(config.managedPty.shell))))) ||
593
+ (config.spawnPolicy !== undefined && (
594
+ !config.spawnPolicy || !validPath(config.spawnPolicy.socketPath) ||
595
+ path.basename(config.spawnPolicy.socketPath) !== "spawn.sock" || Buffer.byteLength(config.spawnPolicy.socketPath) > 100 ||
596
+ !ID_RE.test(config.spawnPolicy.scopeId) || !ID_RE.test(config.spawnPolicy.targetRevision) ||
597
+ config.bashCommandPrefix !== undefined || config.jobEnvSourcePath !== undefined
598
+ )) ||
599
+ (config.jobControlSocketPath !== undefined && (
600
+ !config.managedJobs || !validPath(config.jobControlSocketPath) ||
601
+ path.basename(config.jobControlSocketPath) !== "control.sock" || Buffer.byteLength(config.jobControlSocketPath) > 100
602
+ )) ||
603
+ !Array.isArray(allowedTools) || allowedTools.length < 1 || allowedTools.length > 5 ||
604
+ new Set(allowedTools).size !== allowedTools.length ||
605
+ allowedTools.some((tool) => !["bash", "repl", "read", "edit", "write"].includes(tool))
606
+ ) process.exit(42);
607
+
608
+ function workspaceAliasMatches() {
609
+ try {
610
+ const alias = fs.lstatSync(config.workspaceAliasPath);
611
+ if (!alias.isSymbolicLink()) return false;
612
+ if (fs.readlinkSync(config.workspaceAliasPath) !== config.workRoot) return false;
613
+ return fs.realpathSync(config.workspaceAliasPath) === fs.realpathSync(config.workRoot);
614
+ } catch {
615
+ return false;
616
+ }
617
+ }
618
+ function ensureWorkspaceAlias() {
619
+ try {
620
+ if (!fs.statSync(config.workRoot).isDirectory()) return false;
621
+ if (config.workRoot === config.workspaceAliasPath) return true;
622
+ try {
623
+ fs.lstatSync(config.workspaceAliasPath);
624
+ return workspaceAliasMatches();
625
+ } catch (error) {
626
+ if (!error || error.code !== "ENOENT") return false;
627
+ }
628
+ try {
629
+ fs.symlinkSync(config.workRoot, config.workspaceAliasPath, "dir");
630
+ } catch (error) {
631
+ if (!error || error.code !== "EEXIST") return false;
632
+ }
633
+ return workspaceAliasMatches();
634
+ } catch {
635
+ return false;
636
+ }
637
+ }
638
+ if (!ensureWorkspaceAlias()) process.exit(43);
639
+
640
+ let stopped = false;
641
+ let terminalCleanupStarted = false;
642
+ let terminalCleanupPromise = null;
643
+ let terminalParentPending = null;
644
+ let terminalParentBuffer = "";
645
+ let terminalParentFailed = false;
646
+ if (config.terminalDaemonNonce) {
647
+ const failParent = () => { terminalParentFailed = true; terminalParentPending?.finish(false); };
648
+ process.stdin.on("data", chunk => {
649
+ terminalParentBuffer += chunk.toString();
650
+ if (Buffer.byteLength(terminalParentBuffer) > 4096) return failParent();
651
+ let at;
652
+ while ((at = terminalParentBuffer.indexOf("\n")) >= 0) {
653
+ const line = terminalParentBuffer.slice(0, at); terminalParentBuffer = terminalParentBuffer.slice(at + 1);
654
+ let value; try { value = JSON.parse(line); } catch { return failParent(); }
655
+ if (!terminalParentPending || !hasOnlyKeys(value, ["type", "version", "nonce", "id", "accepted"])
656
+ || value.type !== "terminal_daemon_ack" || value.version !== 1 || value.nonce !== config.terminalDaemonNonce
657
+ || value.id !== terminalParentPending.id || typeof value.accepted !== "boolean") return failParent();
658
+ terminalParentPending.finish(value.accepted);
659
+ }
660
+ });
661
+ process.stdin.on("error", failParent); process.stdin.on("end", failParent);
662
+ }
663
+ function terminalParentRequest(action, claim) {
664
+ if (!config.terminalDaemonNonce) return Promise.resolve(true);
665
+ if (terminalParentFailed || terminalParentPending) return Promise.resolve(false);
666
+ return new Promise(resolve => {
667
+ const id = crypto.randomUUID();
668
+ const timer = setTimeout(() => { terminalParentFailed = true; finish(false); }, 2000);
669
+ let settled = false;
670
+ const finish = accepted => { if (settled) return; settled = true; clearTimeout(timer); terminalParentPending = null; resolve(accepted); };
671
+ terminalParentPending = { id, finish };
672
+ process.stdout.write(JSON.stringify({ type: "terminal_daemon", version: 1, nonce: config.terminalDaemonNonce,
673
+ runId: config.terminalCleanupRunId, executorLaunchId: config.launchId, generation: claim.generation, action, id }) + "\n",
674
+ error => { if (error) { terminalParentFailed = true; finish(false); } });
675
+ });
676
+ }
677
+ let stoppingPromise = null;
678
+ let socket = null;
679
+ let generation = 0;
680
+ let retryMs = 25;
681
+ let connectionAttempt = 0;
682
+ let targetGeneration = config.targetGeneration;
683
+ let targetFenceId = config.fenceId;
684
+ const active = new Map();
685
+ const ledger = new Map();
686
+ const cachedResultIds = [];
687
+ // Managed records extend this worker's Bash supervisor, not the backend queue.
688
+ // No command/environment/stdin text or recoverable PID is stored in the journal.
689
+ const jobs = new Map();
690
+ let journalFailed = false;
691
+ let cleanupUnconfirmed = false;
692
+ let journalDebrisBytes = 0;
693
+ let journalDirectory = null;
694
+ let journalOwnership = null;
695
+ let jobControlServer = null;
696
+ const jobControlClients = new Set();
697
+ let pendingJobRequests = 0;
698
+ let maintenanceLease = null;
699
+ // Process groups of settled retainBackground Bash requests and Pi shells whose descendants
700
+ // were still running when the command exited, keyed by process group ID. They
701
+ // are live work until the group is empty, and stop() ends them with the worker.
702
+ const backgroundGroups = new Map();
703
+ let backgroundGroupTimer = null;
704
+ // reason-sdk state (see machineSdkEndpoint): the claim of the current socket,
705
+ // the worker's private directory with the shim, the live per-request
706
+ // endpoints and the calls waiting for the Brain's sdk_response.
707
+ let socketClaim = null;
708
+ let sdkBase = null;
709
+ let sdkEndpointCount = 0;
710
+ const sdkEndpoints = new Set();
711
+ const sdkPending = new Map();
712
+
713
+ // Fixed-shape diagnostics: never include URLs, peer reasons, errors, request IDs,
714
+ // command text, paths, or credential-bearing configuration. Bound reconnect storms.
715
+ let diagnosticWindowMs = 0;
716
+ let diagnosticCount = 0;
717
+ let suppressedDiagnostics = 0;
718
+ function diagnostic(event, reason = null, peerGeneration = null, closeCode = null) {
719
+ const now = Date.now();
720
+ if (now - diagnosticWindowMs >= 60000) {
721
+ diagnosticWindowMs = now;
722
+ diagnosticCount = 0;
723
+ }
724
+ if (diagnosticCount >= 120 && event !== "stopping" && event !== "stopped") {
725
+ suppressedDiagnostics += 1;
726
+ return;
727
+ }
728
+ diagnosticCount += 1;
729
+ try {
730
+ process.stderr.write(JSON.stringify({
731
+ event: "hands.worker." + event,
732
+ timestampMs: now,
733
+ launchId: config.launchId,
734
+ connectionAttempt,
735
+ connectionGeneration: generation,
736
+ targetGeneration,
737
+ peerGeneration,
738
+ activeRequestCount: active.size,
739
+ socketState: socket ? socket.readyState : null,
740
+ remainingLifetimeMs: Math.max(0, config.expiresAtMs - now),
741
+ closeCode,
742
+ reason,
743
+ suppressedDiagnostics,
744
+ }) + "\n");
745
+ suppressedDiagnostics = 0;
746
+ } catch { /* Diagnostics must never affect execution. */ }
747
+ }
748
+
749
+ function hasOnlyKeys(value, allowed) {
750
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
751
+ const keys = Object.keys(value);
752
+ return keys.every((key) => allowed.includes(key));
753
+ }
754
+ function safeId(value) {
755
+ return typeof value === "string" && ID_RE.test(value) ? value : "invalid";
756
+ }
757
+ function clip(value, maximum) {
758
+ value = typeof value === "string" ? value : String(value || "");
759
+ if (value.length <= maximum) return value;
760
+ const marker = "\n...[executor output truncated]";
761
+ return value.slice(0, Math.max(0, maximum - marker.length)) + marker;
762
+ }
763
+ function failure(id, exitCode, error, stdout, stderr, responseGeneration = targetGeneration) {
764
+ return {
765
+ version: VERSION,
766
+ generation: responseGeneration,
767
+ id: safeId(id),
768
+ ok: false,
769
+ exitCode: Number.isSafeInteger(exitCode) && exitCode > 0 && exitCode <= 255 ? exitCode : 1,
770
+ stdout: clip(stdout || "", MAX_OUTPUT_CHARS),
771
+ stderr: clip(stderr || "", MAX_OUTPUT_CHARS),
772
+ cwd: config.repoPath,
773
+ error: clip(error, MAX_ERROR_CHARS),
774
+ };
775
+ }
776
+ function success(id, stdout, stderr, responseGeneration = targetGeneration) {
777
+ return {
778
+ version: VERSION,
779
+ generation: responseGeneration,
780
+ id,
781
+ ok: true,
782
+ exitCode: 0,
783
+ stdout: clip(stdout || "", MAX_OUTPUT_CHARS),
784
+ stderr: clip(stderr || "", MAX_OUTPUT_CHARS),
785
+ cwd: config.repoPath,
786
+ };
787
+ }
788
+ // The code alone names the reason: Bun's WebSocket client drops close text.
789
+ function closeSocket(target, reason) {
790
+ try {
791
+ if (target.readyState < WebSocket.CLOSING) target.close(CLOSE_CODES[reason], reason);
792
+ } catch {
793
+ try { target.close(); } catch { return; }
794
+ }
795
+ }
796
+ function parseRequest(raw, expectedGeneration) {
797
+ if (Buffer.byteLength(raw, "utf8") > MAX_FRAME_BYTES) {
798
+ const error = new Error("frame_too_large"); error.code = "frame_too_large"; throw error;
799
+ }
800
+ let value;
801
+ try { value = JSON.parse(raw); } catch {
802
+ const error = new Error("invalid_json"); error.code = "invalid_json"; throw error;
803
+ }
804
+ const id = safeId(value && value.id);
805
+ if (
806
+ !hasOnlyKeys(value, ["version", "generation", "id", "tool", "input", "semanticDigest", "lifecycleLeaseId"]) ||
807
+ (value.lifecycleLeaseId !== undefined && (value.tool !== "bash" || typeof value.lifecycleLeaseId !== "string" || !UUID_RE.test(value.lifecycleLeaseId))) ||
808
+ value.version !== VERSION || !Number.isSafeInteger(value.generation) || value.generation < 1 || id === "invalid" ||
809
+ (value.tool === "pi_tool" ? !validPiToolRequest(value) : value.tool !== "resume" && !allowedTools.includes(value.tool)) ||
810
+ !value.input || typeof value.input !== "object" || Array.isArray(value.input)
811
+ ) {
812
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
813
+ }
814
+ if (value.generation !== expectedGeneration) {
815
+ const error = new Error("stale_generation"); error.code = "stale_generation"; error.id = id; throw error;
816
+ }
817
+ if (
818
+ value.tool === "bash" &&
819
+ (!hasOnlyKeys(value.input, ["script", "retainBackground", "machineSdk"]) || typeof value.input.script !== "string" || value.input.script.length > MAX_INPUT_CHARS ||
820
+ (value.input.retainBackground !== undefined && value.input.retainBackground !== true) ||
821
+ (value.input.machineSdk !== undefined && value.input.machineSdk !== true) ||
822
+ (value.semanticDigest !== undefined && (typeof value.semanticDigest !== "string" || !SHA256_RE.test(value.semanticDigest))))
823
+ ) {
824
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
825
+ }
826
+ if (
827
+ value.tool === "repl" &&
828
+ (!hasOnlyKeys(value.input, ["code"]) || typeof value.input.code !== "string" ||
829
+ value.input.code.length > MAX_INPUT_CHARS || value.semanticDigest !== undefined)
830
+ ) {
831
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
832
+ }
833
+ if (value.tool === "resume" && (
834
+ value.semanticDigest !== undefined || !hasOnlyKeys(value.input, ["semanticDigest"]) ||
835
+ typeof value.input.semanticDigest !== "string" || !SHA256_RE.test(value.input.semanticDigest)
836
+ )) {
837
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
838
+ }
839
+ if (
840
+ value.tool === "read" &&
841
+ (!hasOnlyKeys(value.input, ["path", "offset", "limit"]) ||
842
+ typeof value.input.path !== "string" || value.input.path.length < 1 || value.input.path.length > MAX_INPUT_CHARS ||
843
+ value.semanticDigest !== undefined ||
844
+ (value.input.offset !== undefined && (!Number.isSafeInteger(value.input.offset) || value.input.offset < 1)) ||
845
+ (value.input.limit !== undefined && (!Number.isSafeInteger(value.input.limit) || value.input.limit < 1 || value.input.limit > 2000)))
846
+ ) {
847
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
848
+ }
849
+ if (value.tool === "edit") {
850
+ let chars = typeof value.input.path === "string"
851
+ ? value.input.path.length
852
+ : MAX_STRUCTURED_INPUT_CHARS + 1;
853
+ const validEdits = Array.isArray(value.input.edits) && value.input.edits.length >= 1 && value.input.edits.length <= 128
854
+ && value.input.edits.every((edit) => {
855
+ if (!hasOnlyKeys(edit, ["oldText", "newText"]) || typeof edit.oldText !== "string" || edit.oldText.length < 1 || typeof edit.newText !== "string") return false;
856
+ chars += edit.oldText.length + edit.newText.length;
857
+ return chars <= MAX_STRUCTURED_INPUT_CHARS;
858
+ });
859
+ if (!hasOnlyKeys(value.input, ["path", "edits"]) || typeof value.input.path !== "string" || value.input.path.length < 1 || value.semanticDigest !== undefined || !validEdits) {
860
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
861
+ }
862
+ }
863
+ if (
864
+ value.tool === "write" &&
865
+ (!hasOnlyKeys(value.input, ["path", "content"]) || typeof value.input.path !== "string" || value.input.path.length < 1 ||
866
+ typeof value.input.content !== "string"
867
+ || value.input.path.length + value.input.content.length > MAX_STRUCTURED_INPUT_CHARS
868
+ || value.semanticDigest !== undefined)
869
+ ) {
870
+ const error = new Error("invalid_request"); error.code = "invalid_request"; error.id = id; throw error;
871
+ }
872
+ return value;
873
+ }
874
+ function sendResult(target, result) {
875
+ if (target.readyState !== WebSocket.OPEN) return;
876
+ const encoded = JSON.stringify(result);
877
+ if (Buffer.byteLength(encoded, "utf8") > MAX_FRAME_BYTES) {
878
+ closeSocket(target, "result_too_large");
879
+ return;
880
+ }
881
+ target.send(encoded);
882
+ }
883
+ function sendProgress(target, id, sequence, stdout, stderr, responseGeneration = targetGeneration) {
884
+ if (target.readyState !== WebSocket.OPEN) return;
885
+ const encoded = JSON.stringify({
886
+ type: "progress",
887
+ version: VERSION,
888
+ generation: responseGeneration,
889
+ id,
890
+ sequence,
891
+ stdout: clip(stdout || "", MAX_OUTPUT_CHARS),
892
+ stderr: clip(stderr || "", MAX_OUTPUT_CHARS),
893
+ });
894
+ if (Buffer.byteLength(encoded, "utf8") > MAX_FRAME_BYTES) {
895
+ closeSocket(target, "progress_too_large");
896
+ return;
897
+ }
898
+ target.send(encoded);
899
+ }
900
+ function bindResultGeneration(result, responseGeneration) {
901
+ return { ...result, generation: responseGeneration };
902
+ }
903
+ function bindResumeResult(result, responseGeneration) {
904
+ return { ...result, generation: responseGeneration, resumed: true };
905
+ }
906
+ function requestHash(request, input = request.input) {
907
+ return crypto.createHash("sha256").update(JSON.stringify({
908
+ version: request.version,
909
+ id: request.id,
910
+ tool: request.tool,
911
+ input,
912
+ semanticDigest: request.semanticDigest || null,
913
+ })).digest("hex");
914
+ }
915
+ function digest(value) {
916
+ return crypto.createHash("sha256").update(value).digest("hex");
917
+ }
918
+ function privateJournalRead(name, maxBytes = MAX_JOB_RECORD_BYTES) {
919
+ const fd = fs.openSync(path.join(journalDirectory, name), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
920
+ try {
921
+ const stat = fs.fstatSync(fd);
922
+ if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0 || stat.size > maxBytes) {
923
+ throw new Error("invalid_job_journal");
924
+ }
925
+ return { value: JSON.parse(fs.readFileSync(fd, "utf8")), bytes: stat.size };
926
+ } finally { fs.closeSync(fd); }
927
+ }
928
+ // Atomic replacement always survives a worker crash. Only a durable write also
929
+ // survives a host crash: fsync costs ~17 ms per call on slow container disks,
930
+ // so hot paths (output, stdin, snapshots) skip it; transitions keep it.
931
+ function privateJournalWrite(name, encoded, maxBytes = MAX_JOB_RECORD_BYTES, durable = true) {
932
+ const target = path.join(journalDirectory, name);
933
+ const temp = target + ".tmp";
934
+ const bytes = Buffer.byteLength(encoded);
935
+ if (bytes > maxBytes) throw new Error("job_journal_full");
936
+ // A fixed temp name bounds crash debris. The composition root must give a
937
+ // single live worker exclusive ownership of this scope's private directory.
938
+ try { fs.unlinkSync(temp); } catch (error) { if (error.code !== "ENOENT") throw error; }
939
+ const fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
940
+ try { fs.writeFileSync(fd, encoded); if (durable) fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
941
+ fs.renameSync(temp, target);
942
+ if (!durable) return bytes;
943
+ const directoryFd = fs.openSync(path.dirname(target), fs.constants.O_RDONLY);
944
+ try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
945
+ return bytes;
946
+ }
947
+ function journalScope(generation, fenceId) {
948
+ return { schema: 1, scopeId: config.managedJobs.scopeId, repoPath: fs.realpathSync(config.repoPath), generation, fenceId };
949
+ }
950
+ function isLiveJob(record) { return record.state === "starting" || record.state === "running"; }
951
+ function jobJournalReservation(job) {
952
+ return isLiveJob(job.record) || job.record.state === "uncertain" || job.operation
953
+ ? MAX_JOB_RECORD_BYTES : job.journalBytes;
954
+ }
955
+ function hasJobCapacity() {
956
+ // Identity metadata uses the same bounded lifetime ceiling as request replay.
957
+ // The byte budget remains the original 64 full records, including reservations
958
+ // for every process whose future output or cleanup is still unresolved.
959
+ if (jobs.size >= MAX_LEDGER_IDS) return false;
960
+ let reserved = MAX_JOB_RECORD_BYTES + journalDebrisBytes;
961
+ for (const job of jobs.values()) reserved += jobJournalReservation(job);
962
+ return reserved <= MAX_JOB_JOURNAL_BYTES;
963
+ }
964
+ function jobFile(jobId) { return digest(jobId) + ".job.json"; }
965
+ function initializeJobs() {
966
+ if (!config.managedJobs) return;
967
+ fs.mkdirSync(config.managedJobs.journalDirectory, { mode: 0o700 });
968
+ }
969
+ // Worker state (the job journal, the private environment) stays out of the
970
+ // Session's folder, except under a protected directory
971
+ // (shared/local-protected-paths.ts) that the file tools and every confinement
972
+ // refuse. A home-folder worker keeps it in ~/.config/reason, so its Session may
973
+ // work in the home folder itself, as a Mac's home grant does.
974
+ const PROTECTED_STATE_DIRECTORIES = ${JSON.stringify(i)};
975
+ function stateOverlapsWorkspace(state, root) {
976
+ if (state === root || root.startsWith(state + path.sep)) return true;
977
+ if (!state.startsWith(root + path.sep)) return false;
978
+ const segments = state.slice(root.length + 1).toLowerCase().split(path.sep);
979
+ return !PROTECTED_STATE_DIRECTORIES.some((sequence) => segments.some((_, start) =>
980
+ sequence.every((part, offset) => segments[start + offset] === part)));
981
+ }
982
+ function prepareJournalDirectory() {
983
+ if (!config.managedJobs) return;
984
+ const candidate = config.managedJobs.journalDirectory;
985
+ try { initializeJobs(); } catch (error) { if (error.code !== "EEXIST") throw error; }
986
+ const stat = fs.lstatSync(candidate);
987
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0) throw new Error("invalid_job_journal");
988
+ journalDirectory = fs.realpathSync(candidate);
989
+ for (const root of [config.repoPath, config.workRoot]) {
990
+ const canonical = fs.realpathSync(root);
991
+ if (stateOverlapsWorkspace(journalDirectory, canonical)) throw new Error("job_journal_inside_workspace");
992
+ }
993
+ }
994
+ function acquireJournalOwnership() {
995
+ if (!journalDirectory) return;
996
+ const lockPath = path.join(journalDirectory, "owner.lock");
997
+ // Atomic across distinct launch sockets and surviving daemon generations.
998
+ // Crash markers are deliberately NOT reclaimed by PID, expiry or reconnect.
999
+ // Only the serialized daemon recovery owner may retire a marker after exact
1000
+ // prior-launch and descendant-stop proof. Never race two recovery cleaners.
1001
+ const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600);
1002
+ journalOwnership = { fd, path: lockPath, stat: fs.fstatSync(fd) };
1003
+ fs.writeFileSync(fd, JSON.stringify({ version: 1, scopeId: config.managedJobs.scopeId,
1004
+ executorLaunchId: config.launchId, workerArtifactSha256: config.workerArtifactSha256,
1005
+ pid: process.pid, createdAtMs: Date.now() }));
1006
+ fs.fsyncSync(fd);
1007
+ const directoryFd = fs.openSync(journalDirectory, fs.constants.O_RDONLY);
1008
+ try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
1009
+ }
1010
+ function releaseJournalOwnership() {
1011
+ if (!journalOwnership) return;
1012
+ const owned = journalOwnership;
1013
+ const current = fs.lstatSync(owned.path);
1014
+ if (current.dev !== owned.stat.dev || current.ino !== owned.stat.ino || current.nlink !== 1) throw new Error("job_ownership_changed");
1015
+ fs.unlinkSync(owned.path);
1016
+ fs.closeSync(owned.fd);
1017
+ journalOwnership = null;
1018
+ const directoryFd = fs.openSync(journalDirectory, fs.constants.O_RDONLY);
1019
+ try { fs.fsyncSync(directoryFd); } finally { fs.closeSync(directoryFd); }
1020
+ }
1021
+ function loadJobs() {
1022
+ if (!config.managedJobs) return;
1023
+ const names = fs.readdirSync(journalDirectory);
1024
+ if (names.length > MAX_LEDGER_IDS * 2 + 4) throw new Error("job_journal_full");
1025
+ if (names.includes("scope.json")) {
1026
+ const { value: scope } = privateJournalRead("scope.json");
1027
+ if (scope.schema !== 1 || scope.scopeId !== config.managedJobs.scopeId || scope.repoPath !== fs.realpathSync(config.repoPath)
1028
+ || !Number.isSafeInteger(scope.generation) || scope.generation < 1 || !UUID_RE.test(scope.fenceId)) throw new Error("job_scope_mismatch");
1029
+ if (scope.generation > targetGeneration || (scope.generation === targetGeneration && scope.fenceId !== targetFenceId)) {
1030
+ targetGeneration = scope.generation;
1031
+ targetFenceId = scope.fenceId;
1032
+ }
1033
+ } else if (names.some((name) => name !== "scope.json.tmp" && name !== "control.sock" && name !== "owner.lock")) throw new Error("job_scope_missing");
1034
+ privateJournalWrite("scope.json", JSON.stringify(journalScope(targetGeneration, targetFenceId)));
1035
+ let reservedBytes = 0;
1036
+ for (const name of names) {
1037
+ if (name === "scope.json" || name === "scope.json.tmp" || name === "owner.lock" || name === "requests") continue;
1038
+ if (name === "control.sock" && fs.lstatSync(path.join(journalDirectory, name)).isSocket()) continue;
1039
+ if (/^[0-9a-f]{64}\.job\.json\.tmp$/.test(name)) continue;
1040
+ if (!/^[0-9a-f]{64}\.job\.json$/.test(name)) throw new Error("invalid_job_journal");
1041
+ let { value: record, bytes } = privateJournalRead(name);
1042
+ if (!hasOnlyKeys(record, ["schema", "jobId", "digest", "state", "startedAtMs", "finishedAtMs", "exitCode",
1043
+ "leaseExpiresAtMs", "inputSequence", "inputDigest", "output", "nextCursor", "outputLost", "environmentRevision", "environmentSnapshotId", "pty"])
1044
+ || record.schema !== 1 || typeof record.jobId !== "string" || !ID_RE.test(record.jobId)
1045
+ || jobFile(record.jobId) !== name || !SHA256_RE.test(record.digest)
1046
+ || !["starting", "running", "exited", "cancelled", "expired", "uncertain"].includes(record.state)
1047
+ || !Number.isSafeInteger(record.startedAtMs) || record.startedAtMs < 1
1048
+ || !Number.isSafeInteger(record.leaseExpiresAtMs) || record.leaseExpiresAtMs < 1
1049
+ || (record.finishedAtMs !== null && (!Number.isSafeInteger(record.finishedAtMs) || record.finishedAtMs < record.startedAtMs))
1050
+ || (record.exitCode !== null && (!Number.isSafeInteger(record.exitCode) || record.exitCode < 0 || record.exitCode > 255))
1051
+ || !Number.isSafeInteger(record.inputSequence) || record.inputSequence < 0
1052
+ || (record.inputDigest !== null && !SHA256_RE.test(record.inputDigest))
1053
+ || !Number.isSafeInteger(record.nextCursor) || record.nextCursor < 0 || typeof record.outputLost !== "boolean"
1054
+ || !Array.isArray(record.output) || record.output.length > MAX_JOB_OUTPUT_CHUNKS
1055
+ || (record.environmentRevision === undefined) !== (record.environmentSnapshotId === undefined)
1056
+ || (record.environmentRevision !== undefined && (!Number.isSafeInteger(record.environmentRevision)
1057
+ || record.environmentRevision < 1 || !UUID_RE.test(record.environmentSnapshotId)))) throw new Error("invalid_job_journal");
1058
+ let cursor = 0;
1059
+ let chars = 0;
1060
+ for (const chunk of record.output) {
1061
+ if (!chunk || !Number.isSafeInteger(chunk.cursor) || chunk.cursor <= cursor || chunk.cursor > record.nextCursor
1062
+ || !["stdout", "stderr"].includes(chunk.stream) || typeof chunk.text !== "string") throw new Error("invalid_job_journal");
1063
+ cursor = chunk.cursor;
1064
+ chars += chunk.text.length;
1065
+ }
1066
+ if (chars > MAX_JOB_OUTPUT_CHARS || jobs.size >= MAX_LEDGER_IDS) throw new Error("job_journal_full");
1067
+ if (record.pty !== undefined) {
1068
+ const pty = record.pty;
1069
+ if (!isPtyJobId(record.jobId) || !hasOnlyKeys(pty, ["version", "cols", "rows", "output", "cleanup"])
1070
+ || pty.version !== 1 || !validPtyDimensions(pty) || record.output.length !== 0
1071
+ || !Array.isArray(pty.output) || pty.output.length > MAX_JOB_OUTPUT_CHUNKS
1072
+ || (isLiveJob(record) ? pty.cleanup !== "pending" : record.state === "uncertain"
1073
+ ? pty.cleanup !== "unconfirmed" : pty.cleanup !== "confirmed")) throw new Error("invalid_job_journal");
1074
+ let previous = 0, bytes = 0;
1075
+ for (const chunk of pty.output) {
1076
+ if (!hasOnlyKeys(chunk, ["cursor", "dataBase64"]) || !Number.isSafeInteger(chunk.cursor)
1077
+ || chunk.cursor <= previous || chunk.cursor > record.nextCursor || !validPtyBytes(chunk.dataBase64)) throw new Error("invalid_job_journal");
1078
+ previous = chunk.cursor;
1079
+ bytes += Buffer.from(chunk.dataBase64, "base64").length;
1080
+ }
1081
+ if (bytes > MAX_PTY_OUTPUT_BYTES) throw new Error("job_journal_full");
1082
+ }
1083
+ if (isLiveJob(record)) {
1084
+ record.state = "uncertain";
1085
+ if (record.pty) record.pty.cleanup = "unconfirmed";
1086
+ record.finishedAtMs = Date.now();
1087
+ record.exitCode = null;
1088
+ bytes = privateJournalWrite(name, JSON.stringify(record));
1089
+ }
1090
+ const job = { record, operation: null, flushTimer: null, journalBytes: bytes };
1091
+ reservedBytes += jobJournalReservation(job);
1092
+ if (reservedBytes > MAX_JOB_JOURNAL_BYTES) throw new Error("job_journal_full");
1093
+ jobs.set(record.jobId, job);
1094
+ }
1095
+ // Recovery rewrites remove matching temp files. Scan again afterward so
1096
+ // neither missing entries nor already-deleted bytes depend on listing order.
1097
+ for (const name of fs.readdirSync(journalDirectory)) {
1098
+ if (!/^[0-9a-f]{64}\.job\.json\.tmp$/.test(name)) continue;
1099
+ const stat = fs.lstatSync(path.join(journalDirectory, name));
1100
+ if (!stat.isFile() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0 || stat.size > MAX_JOB_RECORD_BYTES) throw new Error("invalid_job_journal");
1101
+ journalDebrisBytes += stat.size;
1102
+ reservedBytes += stat.size;
1103
+ if (reservedBytes > MAX_JOB_JOURNAL_BYTES) throw new Error("job_journal_full");
1104
+ }
1105
+ }
1106
+ try { prepareJournalDirectory(); } catch { process.exit(44); }
1107
+ try { acquireJournalOwnership(); } catch { process.exit(46); }
1108
+ // An unchanged record is not rewritten; a durable request still fsyncs one
1109
+ // that was last written without it.
1110
+ function persistJob(job, durable = true) {
1111
+ if (journalFailed) throw new Error("job_journal_unavailable");
1112
+ const encoded = JSON.stringify(job.record), written = digest(encoded);
1113
+ if (written === job.journalDigest && (job.journalDurable || !durable)) return;
1114
+ try {
1115
+ job.journalBytes = privateJournalWrite(jobFile(job.record.jobId), encoded, MAX_JOB_RECORD_BYTES, durable);
1116
+ job.journalDigest = written;
1117
+ job.journalDurable = durable;
1118
+ }
1119
+ catch {
1120
+ journalFailed = true;
1121
+ // Once durable evidence is unavailable no new effects are admitted. Only
1122
+ // our live ChildProcess handles are cancelled; journaled PIDs are never used.
1123
+ for (const candidate of jobs.values()) {
1124
+ if (!isLiveJob(candidate.record)) continue;
1125
+ candidate.record.state = "uncertain";
1126
+ if (candidate.record.pty) candidate.record.pty.cleanup = "unconfirmed";
1127
+ candidate.record.exitCode = null;
1128
+ if (candidate.operation) void candidate.operation.cancel().catch(() => {});
1129
+ }
1130
+ throw new Error("job_journal_unavailable");
1131
+ }
1132
+ }
1133
+ function appendJobOutput(job, stream, text) {
1134
+ if (!text || journalFailed) return;
1135
+ const record = job.record;
1136
+ // Split oversized reads rather than silently losing the prefix inside one
1137
+ // cursor. Cursor gaps then describe every discarded output chunk precisely.
1138
+ if (text.length > MAX_JOB_OUTPUT_CHARS) {
1139
+ for (let offset = 0; offset < text.length; offset += MAX_JOB_OUTPUT_CHARS) {
1140
+ appendJobOutput(job, stream, text.slice(offset, offset + MAX_JOB_OUTPUT_CHARS));
1141
+ }
1142
+ return;
1143
+ }
1144
+ record.output.push({ cursor: ++record.nextCursor, stream, text });
1145
+ let chars = record.output.reduce((sum, chunk) => sum + chunk.text.length, 0);
1146
+ while (record.output.length > MAX_JOB_OUTPUT_CHUNKS || chars > MAX_JOB_OUTPUT_CHARS) {
1147
+ chars -= record.output.shift().text.length;
1148
+ record.outputLost = true;
1149
+ }
1150
+ scheduleJobFlush(job);
1151
+ }
1152
+ function scheduleJobFlush(job) {
1153
+ if (!job.flushTimer) job.flushTimer = setTimeout(() => {
1154
+ job.flushTimer = null;
1155
+ try { persistJob(job, false); } catch { /* failure already stops live jobs */ }
1156
+ }, 50);
1157
+ }
1158
+ function appendPtyOutput(job, data) {
1159
+ if (!data.length || journalFailed) return;
1160
+ const record = job.record, output = record.pty.output;
1161
+ for (let offset = 0; offset < data.length; offset += MAX_PTY_INPUT_BYTES) {
1162
+ output.push({ cursor: ++record.nextCursor, dataBase64: Buffer.from(data.subarray(offset, offset + MAX_PTY_INPUT_BYTES)).toString("base64") });
1163
+ }
1164
+ let bytes = output.reduce((sum, chunk) => sum + Buffer.from(chunk.dataBase64, "base64").length, 0);
1165
+ while (output.length > MAX_JOB_OUTPUT_CHUNKS || bytes > MAX_PTY_OUTPUT_BYTES) {
1166
+ bytes -= Buffer.from(output.shift().dataBase64, "base64").length;
1167
+ record.outputLost = true;
1168
+ }
1169
+ scheduleJobFlush(job);
1170
+ }
1171
+ async function jobSnapshot(job, after = 0) {
1172
+ const record = job.record;
1173
+ const operation = job.operation;
1174
+ let processIdentity = isLiveJob(record) && operation ? operation.processIdentity() : null;
1175
+ if (record.pty && isLiveJob(record) && !processIdentity && operation) {
1176
+ await operation.started;
1177
+ processIdentity = isLiveJob(record) ? operation.processIdentity() : null;
1178
+ // The anchor can exit before its promise publishes the terminal record.
1179
+ // Wait for the owning cleanup proof, not a client retry or a fabricated
1180
+ // running PID. Unexpected identity loss still resolves uncertain.
1181
+ if (!processIdentity) { await operation.settle(); await operation.promise; }
1182
+ }
1183
+ const firstCursor = (record.pty ? record.pty.output : record.output)[0]?.cursor || record.nextCursor + 1;
1184
+ return {
1185
+ jobId: record.jobId, state: record.state, startedAtMs: record.startedAtMs,
1186
+ finishedAtMs: record.finishedAtMs, leaseExpiresAtMs: record.leaseExpiresAtMs,
1187
+ exitCode: record.exitCode, inputSequence: record.inputSequence,
1188
+ firstCursor, nextCursor: record.nextCursor,
1189
+ truncated: after < firstCursor - 1 || (record.outputLost && after === 0),
1190
+ output: record.output.filter((chunk) => chunk.cursor > after),
1191
+ ...(record.pty ? { pty: { ...record.pty, output: record.pty.output.filter((chunk) => chunk.cursor > after) } } : {}),
1192
+ ...(processIdentity ? { process: processIdentity } : {}),
1193
+ ...(record.environmentRevision === undefined ? {} : {
1194
+ environmentRevision: record.environmentRevision, environmentSnapshotId: record.environmentSnapshotId,
1195
+ }),
1196
+ };
1197
+ }
1198
+ function isPtyJobId(value) { return typeof value === "string" && value.startsWith("pty-") && UUID_RE.test(value.slice(4)); }
1199
+ function validPtyDimensions(input) {
1200
+ return [input.cols, input.rows].every((n) => Number.isSafeInteger(n) && n >= 1 && n <= MAX_PTY_DIMENSION);
1201
+ }
1202
+ function validPtyBytes(value) {
1203
+ if (typeof value !== "string" || !value.length || value.length > Math.ceil(MAX_PTY_INPUT_BYTES / 3) * 4) return false;
1204
+ const bytes = Buffer.from(value, "base64");
1205
+ return bytes.length > 0 && bytes.length <= MAX_PTY_INPUT_BYTES && bytes.toString("base64") === value;
1206
+ }
1207
+ function validJobRequest(value) {
1208
+ if (!hasOnlyKeys(value, ["type", "version", "generation", "id", "jobId", "action", "input"])
1209
+ || value.type !== "job" || value.version !== VERSION || !Number.isSafeInteger(value.generation) || value.generation < 1
1210
+ || typeof value.id !== "string" || !ID_RE.test(value.id) || typeof value.jobId !== "string" || !ID_RE.test(value.jobId)
1211
+ || !hasOnlyKeys(value.input, ["script", "leaseExpiresAtMs", "after", "sequence", "data", "eof", "mode", "cols", "rows", "dataBase64"])) return false;
1212
+ const input = value.input;
1213
+ const validLease = Number.isSafeInteger(input.leaseExpiresAtMs) && input.leaseExpiresAtMs > 0;
1214
+ if (value.action === "start") return input.mode === "pty"
1215
+ ? hasOnlyKeys(input, ["mode", "cols", "rows", "leaseExpiresAtMs"]) && isPtyJobId(value.jobId) && validPtyDimensions(input) && validLease
1216
+ : hasOnlyKeys(input, ["script", "leaseExpiresAtMs"]) && typeof input.script === "string" && input.script.length <= MAX_INPUT_CHARS && validLease;
1217
+ if (value.action === "inspect") return hasOnlyKeys(input, ["after"]) && Number.isSafeInteger(input.after) && input.after >= 0;
1218
+ if (value.action === "resize") return hasOnlyKeys(input, ["sequence", "cols", "rows"])
1219
+ && isPtyJobId(value.jobId) && Number.isSafeInteger(input.sequence) && input.sequence >= 1 && validPtyDimensions(input);
1220
+ if (value.action === "input" && "dataBase64" in input) return hasOnlyKeys(input, ["sequence", "dataBase64"])
1221
+ && isPtyJobId(value.jobId) && Number.isSafeInteger(input.sequence) && input.sequence >= 1 && validPtyBytes(input.dataBase64);
1222
+ if (value.action === "input") return hasOnlyKeys(input, ["sequence", "data", "eof"])
1223
+ && Number.isSafeInteger(input.sequence) && input.sequence >= 1 && typeof input.data === "string"
1224
+ && input.data.length <= MAX_JOB_INPUT_CHARS && typeof input.eof === "boolean";
1225
+ if (value.action === "renew") return hasOnlyKeys(input, ["leaseExpiresAtMs"]) && validLease;
1226
+ return value.action === "cancel" && hasOnlyKeys(input, []);
1227
+ }
1228
+ function currentMaintenance() {
1229
+ if (maintenanceLease && maintenanceLease.expiresAtMs <= Date.now() && active.size === 0) maintenanceLease = null;
1230
+ return maintenanceLease;
1231
+ }
1232
+ function handleLifecycle(target, claim, raw) {
1233
+ let value;
1234
+ try { value = JSON.parse(raw); } catch { return; }
1235
+ if (!hasOnlyKeys(value, ["type", "version", "generation", "id", "action", "leaseId"])
1236
+ || value.type !== "lifecycle" || value.version !== VERSION || safeId(value.id) === "invalid"
1237
+ || typeof value.leaseId !== "string" || !UUID_RE.test(value.leaseId)
1238
+ || !["inspect", "quiesce", "release", "retire"].includes(value.action)) return;
1239
+ // A running managed job also holds a slot in active (so it is cancelled with
1240
+ // the worker) but is counted as a live job. Reporting it as an active request
1241
+ // too made a running dev server look like an in-flight request to callers
1242
+ // that proceed when only live jobs remain. Admission below still uses active.
1243
+ let liveJobs = liveBackgroundGroups(), uncertainJobs = 0, activeJobs = 0;
1244
+ for (const job of jobs.values()) {
1245
+ if (["starting", "running"].includes(job.record.state) || job.operation) liveJobs += 1;
1246
+ if (job.record.state === "uncertain") uncertainJobs += 1;
1247
+ if (active.has(job)) activeJobs += 1;
1248
+ }
1249
+ const held = currentMaintenance();
1250
+ let status = held ? "held" : "idle";
1251
+ if (stopped || value.generation !== claim.generation || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId) status = "fenced";
1252
+ else if (value.action === "retire") {
1253
+ if (!held || held.id !== value.leaseId) status = "fenced";
1254
+ else if (active.size || pendingJobRequests || liveJobs || uncertainJobs || journalFailed || requestJournalFailed || uncertainRequestIds.size) status = "busy";
1255
+ else { stopped = true; status = "retired"; }
1256
+ } else if (value.action === "release") {
1257
+ if (held && held.id === value.leaseId && active.size > 0) status = "busy";
1258
+ else if (held && held.id === value.leaseId) { maintenanceLease = null; status = "released"; }
1259
+ else status = "fenced";
1260
+ } else if (value.action === "quiesce") {
1261
+ if (held && held.id !== value.leaseId) status = "busy";
1262
+ else if (active.size || pendingJobRequests || liveJobs || uncertainJobs || journalFailed || requestJournalFailed || uncertainRequestIds.size) status = "busy";
1263
+ else {
1264
+ maintenanceLease = held || { id: value.leaseId, expiresAtMs: Math.min(config.expiresAtMs, Date.now() + 10 * 60 * 1000) };
1265
+ status = "acquired";
1266
+ }
1267
+ }
1268
+ sendResult(target, { type: "lifecycle_result", version: VERSION, generation: claim.generation,
1269
+ id: value.id, leaseId: value.leaseId, status, activeRequests: active.size - activeJobs,
1270
+ pendingJobRequests, liveJobs, uncertainJobs, uncertainRequests: uncertainRequestIds.size, expiresAtMs: maintenanceLease ? maintenanceLease.expiresAtMs : null });
1271
+ // Retirement revokes admission synchronously. Give the response a bounded
1272
+ // flush window, then exit cleanly and release the durable journal owner.
1273
+ // A lost acknowledgement is recoverable as worker loss, never a live replay.
1274
+ if (status === "retired") setTimeout(() => { void stop("planned_retirement"); }, 50);
1275
+ }
1276
+ function handleGuestMemory(target, claim, raw) {
1277
+ let v; try { v=JSON.parse(raw); } catch { return; }
1278
+ if (!hasOnlyKeys(v,["type","version","generation","fenceId","id","runId","executorLaunchId"])
1279
+ || v.type!=="guest_memory" || v.version!==VERSION || safeId(v.id)==="invalid"
1280
+ || !UUID_RE.test(v.runId) || !UUID_RE.test(v.executorLaunchId) || !UUID_RE.test(v.fenceId)) return;
1281
+ const reply=(status,sample,reasonCode) => sendResult(target,{...v,type:"guest_memory_result",status,observedAtMs:Date.now(),...(sample?{sample}:{}),...(claim.guestMemoryDiagnosticsVersion===1 && reasonCode?{reasonCode}:{})});
1282
+ if(claim.guestMemoryVersion!==1 || process.platform!=="linux" || !config.terminalCleanupRunId) return reply("unsupported");
1283
+ if(stopped || terminalCleanupStarted || target!==socket || v.generation!==claim.generation
1284
+ || claim.generation!==targetGeneration || claim.fenceId!==targetFenceId || v.fenceId!==claim.fenceId
1285
+ || v.executorLaunchId!==config.launchId || v.runId!==config.terminalCleanupRunId
1286
+ || Date.now()>=config.expiresAtMs || currentMaintenance()) return reply("fenced");
1287
+ try { const sample=sampleGuestMemory(fs,path); reply("observed",sample); }
1288
+ catch (error) {
1289
+ const reasonCode=error && typeof error=== "object" && typeof error.reasonCode=== "string"
1290
+ && GUEST_MEMORY_UNAVAILABLE_REASONS.includes(error.reasonCode) ? error.reasonCode : undefined;
1291
+ reply("unavailable",undefined,reasonCode);
1292
+ }
1293
+ }
1294
+ async function handleTerminalCleanup(target, claim, raw) {
1295
+ let v;
1296
+ try { v = JSON.parse(raw); } catch { return; }
1297
+ if (!hasOnlyKeys(v, ["type", "version", "generation", "id", "runId", "executorLaunchId"])
1298
+ || v.type !== "terminal_cleanup" || v.version !== VERSION || safeId(v.id) === "invalid"
1299
+ || !UUID_RE.test(v.runId) || !UUID_RE.test(v.executorLaunchId)) return;
1300
+ const reply = status => sendResult(target, { ...v, type: "terminal_cleanup_result", status, observedAtMs: Date.now() });
1301
+ if (claim.terminalCleanupVersion !== 1 || process.platform !== "linux" || !config.jobEnvSourcePath || !config.terminalCleanupRunId) return reply("unsupported");
1302
+ if (stopped || terminalCleanupStarted || target !== socket || v.generation !== claim.generation
1303
+ || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId
1304
+ || v.executorLaunchId !== config.launchId || v.runId !== config.terminalCleanupRunId || Date.now() >= config.expiresAtMs) return reply("fenced");
1305
+ if (currentMaintenance() || active.size || pendingJobRequests || journalFailed || requestJournalFailed || uncertainRequestIds.size
1306
+ || [...jobs.values()].some(job => job.operation || ["starting", "running", "uncertain"].includes(job.record.state))) return reply("busy");
1307
+ // No new claim, model request, private job or reconnect may race the fixed
1308
+ // privileged operation. This fence is never released, even on failure.
1309
+ terminalCleanupStarted = true;
1310
+ terminalCleanupPromise = (async () => {
1311
+ // The run is over: its retained background commands end before cleanup.
1312
+ if ((await Promise.all(stopBackgroundGroups())).some(Boolean)) { cleanupUnconfirmed = true; return false; }
1313
+ if (!(await terminalParentRequest("begin", claim)) || stopped || target !== socket
1314
+ || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId || Date.now() >= config.expiresAtMs) {
1315
+ cleanupUnconfirmed = true; return false;
1316
+ }
1317
+ const ok = await new Promise(resolve => {
1318
+ let child, timer, killTimer;
1319
+ const finish = ok => { clearTimeout(timer); clearTimeout(killTimer); if (!ok) cleanupUnconfirmed = true; resolve(ok); };
1320
+ try {
1321
+ child = spawn("/usr/bin/python3", ["-I", "-c", ${JSON.stringify(x)}],
1322
+ { stdio: ["pipe", "ignore", "ignore"], env: { PATH: "/usr/bin:/bin" }, detached: true });
1323
+ let timedOut = false;
1324
+ timer = setTimeout(() => { timedOut = true; try { process.kill(-child.pid, "SIGTERM"); } catch { cleanupUnconfirmed = true; }
1325
+ killTimer = setTimeout(() => { try { process.kill(-child.pid, "SIGKILL"); } catch { cleanupUnconfirmed = true; } }, 500); }, 10_000);
1326
+ child.once("error", () => finish(false));
1327
+ child.once("close", code => finish(!timedOut && code === 0));
1328
+ child.stdin.on("error", () => {});
1329
+ child.stdin.end(JSON.stringify({ root: config.workRoot, source: config.jobEnvSourcePath,
1330
+ snapshot: config.jobEnvPath, runId: config.terminalCleanupRunId }));
1331
+ } catch { finish(false); }
1332
+ });
1333
+ const accepted = ok && !stopped && target === socket && claim.generation === targetGeneration
1334
+ && claim.fenceId === targetFenceId && await terminalParentRequest("cleaned", claim);
1335
+ if (!accepted) cleanupUnconfirmed = true;
1336
+ return accepted;
1337
+ })();
1338
+ const parentAccepted = await terminalCleanupPromise;
1339
+ reply(parentAccepted && target === socket && claim.generation === targetGeneration && claim.fenceId === targetFenceId ? "cleaned" : "unconfirmed");
1340
+ }
1341
+ function validJobLease(deadline) {
1342
+ return deadline > Date.now() && deadline <= config.expiresAtMs && deadline <= Date.now() + 12 * 60 * 60 * 1000;
1343
+ }
1344
+ async function handleJob(claim, raw, sendReply, isCurrent, fromControl = false) {
1345
+ let request;
1346
+ try { request = JSON.parse(raw); } catch { return; }
1347
+ const reply = (body) => {
1348
+ if (!isCurrent() || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId) return;
1349
+ sendReply({ type: "job_result", version: VERSION, generation: claim.generation, id: safeId(request.id), ...body });
1350
+ };
1351
+ const fail = (error) => reply({ ok: false, error });
1352
+ if (!isCurrent() || terminalCleanupStarted || stopped || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId) return;
1353
+ if (!validJobRequest(request)) return fail("invalid_job_request");
1354
+ if (validPrivateJob && (request.jobId !== config.directShell.jobId
1355
+ || !["start", "inspect", "cancel"].includes(request.action) || request.input.mode !== undefined)) return fail("invalid_job_request");
1356
+ if (request.generation !== claim.generation) return fail("stale_generation");
1357
+ if (!config.managedJobs || !allowedTools.includes("bash")) return fail("jobs_disabled");
1358
+ if (journalFailed) return fail("job_journal_unavailable");
1359
+ if (currentMaintenance() && !["inspect", "poll", "cancel"].includes(request.action)) return fail("maintenance_held");
1360
+ if (pendingJobRequests >= 128) return fail("job_capacity");
1361
+ pendingJobRequests += 1;
1362
+ let job = jobs.get(request.jobId);
1363
+ try {
1364
+ if (request.action === "start") {
1365
+ const pty = request.input.mode === "pty";
1366
+ if (pty && !ptySupported()) return fail("pty_unsupported");
1367
+ if (!pty && request.input.script.includes("\0")) return fail("invalid_job_request");
1368
+ const hash = digest(pty ? JSON.stringify(["pty", request.input.cols, request.input.rows]) : request.input.script);
1369
+ if (job) {
1370
+ if (Boolean(job.record.pty) !== pty) return fail("job_mode_mismatch");
1371
+ if (job.record.digest !== hash) return fail("job_conflict");
1372
+ // A duplicate start observes the original admission, including its
1373
+ // consumed environment receipt; "starting" is only an inspect result.
1374
+ if (job.operation) await job.operation.started;
1375
+ } else {
1376
+ if (!validJobLease(request.input.leaseExpiresAtMs)) return fail("job_lease_expired");
1377
+ if (!hasJobCapacity()) return fail("job_capacity");
1378
+ // Chosen before the durable record, so nothing can cancel a job whose spawn is pending.
1379
+ const terminal = pty ? await terminalProgram() : null;
1380
+ if (jobs.has(request.jobId) || !hasJobCapacity()) {
1381
+ if (terminal) terminal.release();
1382
+ return fail(jobs.has(request.jobId) ? "job_conflict" : "job_capacity");
1383
+ }
1384
+ job = { record: {
1385
+ schema: 1, jobId: request.jobId, digest: hash, state: "starting", startedAtMs: Date.now(),
1386
+ finishedAtMs: null, exitCode: null, leaseExpiresAtMs: request.input.leaseExpiresAtMs,
1387
+ inputSequence: 0, inputDigest: null, output: [], nextCursor: 0, outputLost: false,
1388
+ ...(pty ? { pty: { version: 1, cols: request.input.cols, rows: request.input.rows, output: [], cleanup: "pending" } } : {}),
1389
+ }, operation: null, flushTimer: null, journalBytes: 0 };
1390
+ jobs.set(request.jobId, job);
1391
+ persistJob(job); // Durable intent before any process is spawned.
1392
+ if (!validJobLease(job.record.leaseExpiresAtMs)) {
1393
+ job.record.state = "expired";
1394
+ job.record.finishedAtMs = Date.now();
1395
+ job.record.exitCode = 124;
1396
+ if (pty) job.record.pty.cleanup = "confirmed";
1397
+ if (terminal) terminal.release();
1398
+ persistJob(job);
1399
+ return fail("job_lease_expired");
1400
+ }
1401
+ const managed = {
1402
+ // A job a person started over the control socket runs as the payload account (config.jobsUser).
1403
+ asJobsUser: fromControl && Boolean(config.jobsUser),
1404
+ leaseExpiresAtMs: job.record.leaseExpiresAtMs,
1405
+ onOutput: (stream, text) => appendJobOutput(job, stream, text),
1406
+ onBytes: (bytes) => appendPtyOutput(job, bytes),
1407
+ onEnvironment: (environment) => {
1408
+ job.record.environmentRevision = environment.revision;
1409
+ job.record.environmentSnapshotId = environment.snapshot_id;
1410
+ persistJob(job);
1411
+ },
1412
+ };
1413
+ const operation = pty ? ptyOperation(request, managed, terminal.program)
1414
+ : bashOperation({ id: request.jobId, input: { script: request.input.script } }, null, managed);
1415
+ job.operation = operation;
1416
+ active.set(job, operation);
1417
+ void operation.promise.then((result) => {
1418
+ if (terminal) terminal.release();
1419
+ active.delete(job);
1420
+ job.operation = null;
1421
+ clearTimeout(job.flushTimer);
1422
+ job.flushTimer = null;
1423
+ if (job.record.state !== "uncertain") {
1424
+ job.record.state = result.error === "timeout" ? "expired"
1425
+ : result.error === "cancelled" ? "cancelled"
1426
+ : result.error === "process_group_cleanup_unconfirmed" ? "uncertain" : "exited";
1427
+ job.record.exitCode = job.record.state === "uncertain" ? null : result.exitCode;
1428
+ }
1429
+ job.record.finishedAtMs = Date.now();
1430
+ if (job.record.pty) job.record.pty.cleanup = job.record.state === "uncertain" ? "unconfirmed" : "confirmed";
1431
+ try { persistJob(job); } catch { /* remains uncertain on durable failure */ }
1432
+ });
1433
+ if (await operation.started && job.record.state === "starting") job.record.state = "running";
1434
+ persistJob(job);
1435
+ }
1436
+ } else if (!job) return fail("job_not_found");
1437
+ else if (request.action === "cancel") {
1438
+ if (job.operation && isLiveJob(job.record)) {
1439
+ const operation = job.operation;
1440
+ await operation.cancel();
1441
+ await operation.promise;
1442
+ }
1443
+ } else if (request.action === "input" || request.action === "resize") {
1444
+ const input = request.input;
1445
+ const pty = request.action === "resize" || "dataBase64" in input;
1446
+ if (Boolean(job.record.pty) !== pty) return fail("job_mode_mismatch");
1447
+ const hash = digest(JSON.stringify(pty ? [request.action, input.dataBase64, input.cols, input.rows] : [input.data, input.eof]));
1448
+ const data = pty ? Buffer.from(input.dataBase64 || "", "base64") : input.data;
1449
+ if (input.sequence === job.record.inputSequence && hash === job.record.inputDigest) {
1450
+ // Duplicate intent: returning state is not a promise that stdin was consumed.
1451
+ } else {
1452
+ if (!job.operation || !isLiveJob(job.record)) return fail("job_not_running");
1453
+ if (Date.now() >= job.record.leaseExpiresAtMs) return fail("job_lease_expired");
1454
+ if (input.sequence !== job.record.inputSequence + 1) return fail("job_input_sequence");
1455
+ const inputError = job.operation.canInput(data);
1456
+ if (inputError) return fail(inputError);
1457
+ job.record.inputSequence = input.sequence;
1458
+ job.record.inputDigest = hash;
1459
+ persistJob(job, false); // Record accepted intent before stdin effects; never replay on recovery.
1460
+ const error = request.action === "resize" ? job.operation.resize(input.cols, input.rows) : job.operation.input(data, input.eof);
1461
+ if (error) return fail(error);
1462
+ if (request.action === "resize") { job.record.pty.cols = input.cols; job.record.pty.rows = input.rows; }
1463
+ }
1464
+ } else if (request.action === "renew") {
1465
+ if (!job.operation || !isLiveJob(job.record)) return fail("job_not_running");
1466
+ // Preparation holds its own immutable deadline. Do not acknowledge a
1467
+ // pipe-job extension until that authority has admitted the child.
1468
+ if (!job.record.pty && job.record.state !== "running") return fail("job_not_running");
1469
+ if (Date.now() >= job.record.leaseExpiresAtMs || !validJobLease(request.input.leaseExpiresAtMs)) return fail("job_lease_expired");
1470
+ // Retried or out-of-order renewals never shorten an already accepted lease.
1471
+ job.record.leaseExpiresAtMs = Math.max(job.record.leaseExpiresAtMs, request.input.leaseExpiresAtMs);
1472
+ persistJob(job);
1473
+ if (!job.operation.renew(job.record.leaseExpiresAtMs)) return fail("job_not_running");
1474
+ }
1475
+ const snapshot = await jobSnapshot(job, request.action === "inspect" ? request.input.after : 0);
1476
+ persistJob(job, false); // The acknowledged output cursor survives a worker restart.
1477
+ reply({ ok: true, job: snapshot });
1478
+ } catch { fail(journalFailed ? "job_journal_unavailable" : "job_not_running"); }
1479
+ finally { pendingJobRequests -= 1; }
1480
+ }
1481
+ function startJobControl() {
1482
+ if (!config.jobControlSocketPath) return Promise.resolve();
1483
+ return new Promise((resolve, reject) => {
1484
+ const directory = path.dirname(config.jobControlSocketPath);
1485
+ const stat = fs.lstatSync(directory);
1486
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid()
1487
+ || (stat.mode & 0o777) !== 0o700 || fs.realpathSync(directory) !== directory
1488
+ || path.join(directory, "control.sock") !== config.jobControlSocketPath) throw new Error("invalid_job_control_directory");
1489
+ for (const root of [config.repoPath, config.workRoot]) {
1490
+ const canonical = fs.realpathSync(root);
1491
+ if (directory === canonical || directory.startsWith(canonical + path.sep) || canonical.startsWith(directory + path.sep)) {
1492
+ throw new Error("job_control_overlaps_workspace");
1493
+ }
1494
+ }
1495
+ // lstat also rejects dangling symlinks; never unlink or adopt an existing
1496
+ // endpoint. An exclusive per-launch directory is composition-owned.
1497
+ try { fs.lstatSync(config.jobControlSocketPath); throw new Error("job_control_path_exists"); }
1498
+ catch (error) { if (error.code !== "ENOENT") throw error; }
1499
+ // Bind before connecting the relay so a duplicate/stale socket cannot leave
1500
+ // a second serving worker. Single framed request per connection, bounded
1501
+ // clients/body/time; the existing job handler owns all mutations.
1502
+ const server = net.createServer((client) => {
1503
+ jobControlClients.add(client);
1504
+ client.once("close", () => jobControlClients.delete(client));
1505
+ client.on("error", () => {});
1506
+ client.setTimeout(10000, () => client.destroy());
1507
+ client.setEncoding("utf8");
1508
+ let raw = "";
1509
+ let received = false;
1510
+ client.on("data", (chunk) => {
1511
+ if (received) { client.destroy(); return; }
1512
+ raw += chunk;
1513
+ if (Buffer.byteLength(raw) > MAX_JOB_CONTROL_FRAME_BYTES) { client.destroy(); return; }
1514
+ const end = raw.indexOf("\n");
1515
+ if (end < 0) return;
1516
+ received = true;
1517
+ if (end !== raw.length - 1) { client.destroy(); return; }
1518
+ let envelope;
1519
+ try { envelope = JSON.parse(raw.slice(0, end)); } catch { client.destroy(); return; }
1520
+ if (!hasOnlyKeys(envelope, ["version", "scopeId", "fenceId", "executorLaunchId", "request"])
1521
+ || envelope.version !== 1 || envelope.scopeId !== config.managedJobs.scopeId
1522
+ || envelope.fenceId !== targetFenceId || envelope.executorLaunchId !== config.launchId
1523
+ || !validJobRequest(envelope.request) || envelope.request.generation !== targetGeneration || stopped) {
1524
+ client.destroy(); return;
1525
+ }
1526
+ void handleJob({ generation: envelope.request.generation, fenceId: envelope.fenceId }, JSON.stringify(envelope.request),
1527
+ (result) => client.end(JSON.stringify(result) + "\n"), () => !client.destroyed, true);
1528
+ });
1529
+ });
1530
+ server.maxConnections = 16;
1531
+ server.once("error", reject);
1532
+ server.listen(config.jobControlSocketPath, () => {
1533
+ try { fs.chmodSync(config.jobControlSocketPath, 0o600); }
1534
+ catch (error) { server.close(); reject(error); return; }
1535
+ jobControlServer = server;
1536
+ resolve();
1537
+ });
1538
+ });
1539
+ }
1540
+ async function handleRequestCancel(target, claim, raw) {
1541
+ let request;
1542
+ try { request = JSON.parse(raw); } catch { return; }
1543
+ if (!hasOnlyKeys(request, ["type", "version", "generation", "id", "targetId"])
1544
+ || request.type !== "cancel_request" || request.version !== VERSION || request.generation !== claim.generation
1545
+ || typeof request.id !== "string" || !ID_RE.test(request.id)
1546
+ || typeof request.targetId !== "string" || !ID_RE.test(request.targetId)) {
1547
+ closeSocket(target, "invalid_cancel_request"); return;
1548
+ }
1549
+ const operation = active.get(request.targetId);
1550
+ let status = ledger.has(request.targetId) ? "settled" : "not_found";
1551
+ if (operation) {
1552
+ // The CLI owns REPL child termination on socket abort. Closing that RPC
1553
+ // requests reset but cannot itself attest child death over a closed socket.
1554
+ status = operation.supportsCancellation ? "cancelled" : "unsupported";
1555
+ if (operation.supportsCancellation) {
1556
+ try {
1557
+ await operation.cancel(true);
1558
+ const result = await operation.promise;
1559
+ if (result.error === "process_group_cleanup_unconfirmed") status = "unconfirmed";
1560
+ else if (operation.cancellationRequiresExternalProof) status = "unconfirmed";
1561
+ else if (result.error !== "cancelled") status = "settled";
1562
+ } catch { status = "unconfirmed"; }
1563
+ }
1564
+ }
1565
+ if (target === socket && claim.generation === targetGeneration && claim.fenceId === targetFenceId) {
1566
+ sendResult(target, { type: "cancel_ack", version: VERSION, generation: claim.generation,
1567
+ id: request.id, targetId: request.targetId, status });
1568
+ }
1569
+ }
1570
+ function processGroupAbsent(pgid) {
1571
+ try {
1572
+ process.kill(-pgid, 0);
1573
+ if (process.platform === "linux") {
1574
+ try {
1575
+ const entries = fs.readdirSync("/proc");
1576
+ for (let i = 0; i < entries.length; i++) {
1577
+ const entry = entries[i];
1578
+ if (entry.charCodeAt(0) < 48 || entry.charCodeAt(0) > 57) continue;
1579
+ try {
1580
+ const stat = fs.readFileSync("/proc/" + entry + "/stat", "utf8");
1581
+ const closeParen = stat.lastIndexOf(")");
1582
+ if (closeParen < 0) continue;
1583
+ const fields = stat.slice(closeParen + 2).split(" ");
1584
+ const state = fields[0];
1585
+ const pgrp = Number(fields[2]);
1586
+ if (pgrp === pgid && state !== "Z") {
1587
+ return false;
1588
+ }
1589
+ } catch {
1590
+ // Process exited between readdir and stat read
1591
+ }
1592
+ }
1593
+ return true;
1594
+ } catch {
1595
+ // Fallback to process.kill result
1596
+ }
1597
+ }
1598
+ return false;
1599
+ } catch (error) {
1600
+ return Boolean(error && error.code === "ESRCH");
1601
+ }
1602
+ }
1603
+ function waitForProcessGroupAbsent(pgid, timeoutMs) {
1604
+ const deadline = Date.now() + timeoutMs;
1605
+ return new Promise((resolve) => {
1606
+ const inspect = () => {
1607
+ if (processGroupAbsent(pgid)) {
1608
+ resolve(true);
1609
+ return;
1610
+ }
1611
+ if (Date.now() >= deadline) {
1612
+ resolve(false);
1613
+ return;
1614
+ }
1615
+ // This referenced timer is part of the worker's exact stop proof. The
1616
+ // worker must remain alive until every tracked process group is absent.
1617
+ setTimeout(inspect, 20);
1618
+ };
1619
+ inspect();
1620
+ });
1621
+ }
1622
+ async function killGroup(child) {
1623
+ if (!child || !Number.isSafeInteger(child.pid)) return;
1624
+ const pgid = child.pid;
1625
+ if (processGroupAbsent(pgid)) return;
1626
+ try { process.kill(-pgid, "SIGTERM"); } catch (error) {
1627
+ if (error && error.code !== "ESRCH") throw error;
1628
+ }
1629
+ if (await waitForProcessGroupAbsent(pgid, 500)) return;
1630
+ try { process.kill(-pgid, "SIGKILL"); } catch (error) {
1631
+ if (error && error.code !== "ESRCH") throw error;
1632
+ }
1633
+ if (await waitForProcessGroupAbsent(pgid, 2_000)) return;
1634
+ throw new Error("process_group_cleanup_unconfirmed");
1635
+ }
1636
+ // A retainBackground Bash request or Pi shell settles when its command exits; descendants
1637
+ // still in its process group keep running and stay tracked here. A group ID
1638
+ // cannot be reused while any member lives, and the group is forgotten once
1639
+ // empty (polled every 500 ms, and when a Bash leader is spawned with that PID).
1640
+ function liveBackgroundGroups() {
1641
+ for (const pgid of backgroundGroups.keys()) if (processGroupAbsent(pgid)) backgroundGroups.delete(pgid);
1642
+ if (!backgroundGroups.size && backgroundGroupTimer) { clearInterval(backgroundGroupTimer); backgroundGroupTimer = null; }
1643
+ return backgroundGroups.size;
1644
+ }
1645
+ function retainBackgroundGroup(child) {
1646
+ if (processGroupAbsent(child.pid)) return;
1647
+ backgroundGroups.set(child.pid, child);
1648
+ if (!backgroundGroupTimer) {
1649
+ backgroundGroupTimer = setInterval(liveBackgroundGroups, 500);
1650
+ backgroundGroupTimer.unref();
1651
+ }
1652
+ }
1653
+ function stopBackgroundGroups() {
1654
+ const groups = [...backgroundGroups.values()];
1655
+ backgroundGroups.clear();
1656
+ return groups.map((child) => killGroup(child).then(() => null,
1657
+ () => ({ error: "process_group_cleanup_unconfirmed" })));
1658
+ }
1659
+ // reason-sdk (runtime/reason-v7-sync-yield-machine-sdk-v1): Reason's SDK as a
1660
+ // command on this machine, composable in shell. A Bash request sent with
1661
+ // machineSdk gets a private Unix socket for as long as the request runs; the
1662
+ // reason-sdk shim on its PATH sends each call there, and the worker forwards it
1663
+ // to the Brain as an sdk_request bound to that request's id. The Brain runs the
1664
+ // call and answers; no Reason credential is ever on the machine. Once the
1665
+ // request settles its socket is gone, so a process it left running in the
1666
+ // background cannot call Reason.
1667
+ const SDK_USAGE = [
1668
+ "reason-sdk search WORDS [--namespace NAME] [--limit N]",
1669
+ "reason-sdk namespace NAME",
1670
+ "reason-sdk describe NAME",
1671
+ "reason-sdk call NAME [--json '{...}' | --json -]",
1672
+ ].join("\n");
1673
+ const SDK_NOT_LIVE = "reason-sdk: unavailable: it works only while the Bash call that ran it is running (within its first 60 s), and that call has ended.";
1674
+ const SDK_DISCONNECTED = "reason-sdk: unavailable: Reason is not connected to this machine right now. Try again.";
1675
+ const SDK_MAX_LINE_CHARS = 4 * MAX_INPUT_CHARS;
1676
+ const SDK_MAX_PENDING = 64;
1677
+ // The shim's socket client: macOS's own Perl (its nc exits at the end of its
1678
+ // input, before the answer), or this worker's runtime on Linux (the compiled CLI
1679
+ // runs as Bun there, as the Linux PTY launcher does). Either sends the shim's
1680
+ // line, prints the answer and prints nothing when the socket is gone.
1681
+ const SDK_RUNTIME_CLIENT = 'const s=require("node:net").createConnection(process.env.REASON_SDK_SOCKET);'
1682
+ + 'const c=[];process.stdin.on("data",d=>c.push(d));process.stdin.on("end",()=>s.write(Buffer.concat(c)));'
1683
+ + 's.pipe(process.stdout);s.on("error",()=>process.exit(0));';
1684
+ const SDK_PERL_CLIENT = 'use IO::Socket::UNIX;my $s=IO::Socket::UNIX->new(Type=>SOCK_STREAM(),Peer=>$ENV{REASON_SDK_SOCKET}) or exit 0;'
1685
+ + 'local $/;my $in=<STDIN>;print $s $in;$s->flush;my $b;while(sysread($s,$b,65536)){syswrite(STDOUT,$b)}';
1686
+ function shellQuote(value) { return "'" + String(value).replace(/'/g, "'\\''") + "'"; }
1687
+ function machineSdkClientCommand() {
1688
+ if (process.platform === "darwin") return fs.existsSync("/usr/bin/perl") ? "/usr/bin/perl -e " + shellQuote(SDK_PERL_CLIENT) : null;
1689
+ if (process.platform !== "linux") return null;
1690
+ return typeof Bun === "undefined"
1691
+ ? shellQuote(process.execPath) + " -e " + shellQuote(SDK_RUNTIME_CLIENT)
1692
+ : "env " + ["BUN", "BE", "BUN"].join("_") + "=1 " + shellQuote(process.execPath)
1693
+ + " --no-env-file --config=/dev/null -e " + shellQuote(SDK_RUNTIME_CLIENT);
1694
+ }
1695
+ function machineSdkShim(client) {
1696
+ return [
1697
+ "#!/bin/bash",
1698
+ "# reason-sdk: Reason's tools from this machine, while the Bash call that runs it is running.",
1699
+ 'socket="' + "$" + '{REASON_SDK_SOCKET:-}"',
1700
+ 'if [ -z "$socket" ] || [ ! -S "$socket" ]; then',
1701
+ " printf '%s\\n' " + shellQuote(SDK_NOT_LIVE) + " >&2",
1702
+ " exit 69",
1703
+ "fi",
1704
+ "stdin=-",
1705
+ "previous=",
1706
+ 'for arg in "$@"; do',
1707
+ ' if { [ "$previous" = --json ] && [ "$arg" = - ]; } || [ "$arg" = --json=- ]; then stdin="a$(base64 | tr -d \'\\n\')"; fi',
1708
+ ' previous=$arg',
1709
+ "done",
1710
+ 'line="v1 $stdin"',
1711
+ 'for arg in "$@"; do line="$line a$(printf %s "$arg" | base64 | tr -d \'\\n\')"; done',
1712
+ "printf '%s\\n' \"$line\" | " + client + " | {",
1713
+ " IFS= read -r status || { printf '%s\\n' " + shellQuote(SDK_NOT_LIVE) + " >&2; exit 69; }",
1714
+ ' case "$status" in',
1715
+ " 0) cat ;;",
1716
+ ' 1|2|69|75) cat >&2; exit "$status" ;;',
1717
+ " *) printf '%s\\n' 'reason-sdk: invalid reply.' >&2; exit 75 ;;",
1718
+ " esac",
1719
+ "}",
1720
+ "",
1721
+ ].join("\n");
1722
+ }
1723
+ // Asked at every claim, so it touches nothing on disk: the private directory
1724
+ // and shim are created by the first machineSdk request (machineSdkBase).
1725
+ function machineSdkAvailable() {
1726
+ if (!allowedTools.includes("bash") || validPrivateJob || config.spawnPolicy || config.bashCommandPrefix
1727
+ || localGithubSandbox || !machineSdkClientCommand()) return false;
1728
+ return machineSdkTemporaryRoot() !== null;
1729
+ }
1730
+ // Where the private directory goes: the worker's temporary directory when a
1731
+ // socket path under it fits the Unix limit, else /tmp. A headless worker's
1732
+ // TMPDIR is a per-launch directory under its state directory, which commonly
1733
+ // exceeds the limit (as the managed-job control socket's does).
1734
+ function machineSdkTemporaryRoot() {
1735
+ for (const candidate of [require("node:os").tmpdir(), "/tmp"]) {
1736
+ try {
1737
+ const root = fs.realpathSync(candidate);
1738
+ if (fs.statSync(root).isDirectory() && Buffer.byteLength(path.join(root, "rsdk-XXXXXX", "00000000.sock")) <= 100) return root;
1739
+ } catch (error) { void error; }
1740
+ }
1741
+ return null;
1742
+ }
1743
+ // One private directory per worker (owner-only, outside the workspace), created
1744
+ // by the first machineSdk request: the shim, and a socket per live machineSdk
1745
+ // request. Removed when the worker stops.
1746
+ function machineSdkBase() {
1747
+ if (sdkBase) return sdkBase;
1748
+ const client = machineSdkClientCommand();
1749
+ const temporaryRoot = machineSdkTemporaryRoot();
1750
+ if (!client || !temporaryRoot) throw new Error("machine_sdk_unavailable");
1751
+ const root = fs.mkdtempSync(path.join(temporaryRoot, "rsdk-"));
1752
+ try {
1753
+ fs.chmodSync(root, 0o700);
1754
+ const bin = path.join(root, "bin");
1755
+ fs.mkdirSync(bin, { mode: 0o700 });
1756
+ fs.writeFileSync(path.join(bin, "reason-sdk"), machineSdkShim(client), { mode: 0o700, flag: "wx" });
1757
+ if (Buffer.byteLength(path.join(root, "00000000.sock")) > 100) throw new Error("machine_sdk_path_too_long");
1758
+ sdkBase = { root, bin };
1759
+ return sdkBase;
1760
+ } catch (error) {
1761
+ fs.rmSync(root, { recursive: true, force: true });
1762
+ throw error;
1763
+ }
1764
+ }
1765
+ function removeMachineSdk() {
1766
+ for (const endpoint of [...sdkEndpoints]) endpoint.close();
1767
+ if (!sdkBase) return;
1768
+ const root = sdkBase.root;
1769
+ sdkBase = null;
1770
+ // Best effort while stopping: a leftover private directory holds no live socket.
1771
+ try { fs.rmSync(root, { recursive: true, force: true }); } catch (error) { void error; }
1772
+ }
1773
+ function failSdkCalls(target, message) {
1774
+ for (const [id, entry] of sdkPending) {
1775
+ if (target !== null && entry.target !== target) continue;
1776
+ sdkPending.delete(id);
1777
+ entry.reply(69, message);
1778
+ }
1779
+ }
1780
+ // The shim's line: "v1", its stdin (only for --json -) and each argument, each
1781
+ // "a" + base64 or "-". The command is parsed here, so a usage error never
1782
+ // reaches the Brain; a call is forwarded only in a shape the Brain accepts.
1783
+ function machineSdkCall(line) {
1784
+ const tokens = line.split(" ");
1785
+ const decode = (token) => /^a[A-Za-z0-9+/]*={0,2}$/.test(token) ? Buffer.from(token.slice(1), "base64").toString("utf8") : null;
1786
+ if (tokens.length < 2 || tokens[0] !== "v1" || (tokens[1] !== "-" && decode(tokens[1]) === null)) return { error: "reason-sdk: invalid request." };
1787
+ const stdin = tokens[1] === "-" ? null : decode(tokens[1]);
1788
+ const argv = tokens.slice(2).map(decode);
1789
+ if (argv.some((arg) => arg === null)) return { error: "reason-sdk: invalid request." };
1790
+ const usage = (message) => ({ error: "reason-sdk: " + message + "\nUsage:\n" + SDK_USAGE });
1791
+ const validName = (name) => typeof name === "string" && name.length >= 1 && name.length <= 200 && !name.includes("\0");
1792
+ const [verb, ...rest] = argv;
1793
+ if (verb === undefined || ((verb === "help" || verb === "--help" || verb === "-h") && rest.length === 0)) return { call: { verb: "help" } };
1794
+ if (verb === "search") {
1795
+ const words = [];
1796
+ const args = {};
1797
+ for (let index = 0; index < rest.length;) {
1798
+ const word = rest[index];
1799
+ const flag = ["--namespace", "--limit"].find((name) => word === name || word.startsWith(name + "="));
1800
+ if (!flag) { words.push(word); index += 1; continue; }
1801
+ let value;
1802
+ if (word === flag) {
1803
+ if (index + 1 >= rest.length) return usage(flag + " needs a value.");
1804
+ value = rest[index + 1]; index += 2;
1805
+ } else { value = word.slice(flag.length + 1); index += 1; }
1806
+ if (flag === "--namespace") {
1807
+ if (!validName(value)) return usage("--namespace takes one name.");
1808
+ args.namespace = value;
1809
+ } else {
1810
+ const limit = Number(value);
1811
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) return usage("--limit takes a whole number from 1 to 50.");
1812
+ args.limit = limit;
1813
+ }
1814
+ }
1815
+ const query = words.join(" ");
1816
+ if (!query.trim()) return usage("search needs words to search for.");
1817
+ if (query.length > 2000) return usage("the search words are too long.");
1818
+ return { call: { verb: "search", query, ...(Object.keys(args).length ? { args } : {}) } };
1819
+ }
1820
+ if (verb === "describe" || verb === "namespace") {
1821
+ if (rest.length !== 1 || !validName(rest[0])) return usage(verb + " takes exactly one name.");
1822
+ return { call: { verb, name: rest[0] } };
1823
+ }
1824
+ if (verb !== "call") return usage("unknown command " + JSON.stringify(verb) + ".");
1825
+ const [name, ...options] = rest;
1826
+ if (!validName(name)) return usage("call needs a tool name.");
1827
+ let json;
1828
+ for (let index = 0; index < options.length;) {
1829
+ const option = options[index];
1830
+ let value;
1831
+ if (option.startsWith("--json=")) { value = option.slice(7); index += 1; }
1832
+ else if (option === "--json") {
1833
+ if (index + 1 >= options.length) return usage("--json needs a value.");
1834
+ value = options[index + 1]; index += 2;
1835
+ } else return usage("unexpected argument " + JSON.stringify(option) + "; pass the arguments as one JSON object with --json.");
1836
+ if (json !== undefined) return usage("pass --json once.");
1837
+ json = value === "-" ? (stdin ?? "") : value;
1838
+ }
1839
+ let args = {};
1840
+ if (json !== undefined) {
1841
+ try { args = JSON.parse(json); } catch (error) {
1842
+ return { error: "reason-sdk: the --json arguments are not valid JSON (" + (error && error.message ? error.message : "parse error") + ")." };
1843
+ }
1844
+ if (!args || typeof args !== "object" || Array.isArray(args)) return { error: "reason-sdk: the --json arguments must be one JSON object." };
1845
+ if (JSON.stringify(args).length > MAX_INPUT_CHARS) return { error: "reason-sdk: the --json arguments are too large." };
1846
+ }
1847
+ return { call: { verb: "call", name, args } };
1848
+ }
1849
+ function forwardSdkCall(jobRequestId, call, reply) {
1850
+ const target = socket;
1851
+ const claim = socketClaim;
1852
+ if (!target || target.readyState !== WebSocket.OPEN || !claim || claim.machineSdkVersion !== 1
1853
+ || claim.generation !== targetGeneration || claim.fenceId !== targetFenceId) { reply(69, SDK_DISCONNECTED); return; }
1854
+ if (sdkPending.size >= SDK_MAX_PENDING) { reply(75, "reason-sdk: too many calls at once; wait for some to finish."); return; }
1855
+ const id = "sdk-" + crypto.randomUUID();
1856
+ const frame = JSON.stringify({ type: "sdk_request", version: VERSION, generation: targetGeneration, id, jobRequestId, ...call });
1857
+ if (Buffer.byteLength(frame, "utf8") > MAX_FRAME_BYTES) { reply(2, "reason-sdk: the request is too large."); return; }
1858
+ sdkPending.set(id, { jobRequestId, reply, target });
1859
+ try { target.send(frame); } catch { sdkPending.delete(id); reply(69, SDK_DISCONNECTED); }
1860
+ }
1861
+ function handleSdkResponse(target, raw) {
1862
+ let value;
1863
+ try { value = JSON.parse(raw); } catch { closeSocket(target, "invalid_sdk_response"); return; }
1864
+ const answer = value && value.ok === true ? "result" : "error";
1865
+ if (!hasOnlyKeys(value, ["type", "version", "generation", "id", "ok", answer]) || value.type !== "sdk_response"
1866
+ || value.version !== VERSION || value.generation !== targetGeneration || typeof value.id !== "string" || !ID_RE.test(value.id)
1867
+ || (value.ok !== true && value.ok !== false) || typeof value[answer] !== "string" || value[answer].length > MAX_OUTPUT_CHARS
1868
+ || (value.ok === false && value.error.length < 1)) {
1869
+ closeSocket(target, "invalid_sdk_response");
1870
+ return;
1871
+ }
1872
+ const entry = sdkPending.get(value.id);
1873
+ // A late answer for a call whose request already settled has nowhere to go.
1874
+ if (!entry || entry.target !== target) return;
1875
+ sdkPending.delete(value.id);
1876
+ entry.reply(value.ok ? 0 : 1, value[answer]);
1877
+ }
1878
+ // The socket and PATH entry of one machineSdk Bash request. Calls are accepted
1879
+ // only until close(), which the request's settlement calls.
1880
+ function machineSdkEndpoint(jobRequestId) {
1881
+ const base = machineSdkBase();
1882
+ sdkEndpointCount += 1;
1883
+ const socketPath = path.join(base.root, String(sdkEndpointCount).padStart(8, "0") + ".sock");
1884
+ const clients = new Map();
1885
+ let live = true;
1886
+ const server = net.createServer({ allowHalfOpen: true }, (client) => {
1887
+ if (!live) { client.destroy(); return; }
1888
+ let answered = false;
1889
+ const reply = (status, text) => {
1890
+ if (answered) return;
1891
+ answered = true;
1892
+ clients.delete(client);
1893
+ if (!client.destroyed) client.end(status + "\n" + text + (text.endsWith("\n") ? "" : "\n"));
1894
+ };
1895
+ clients.set(client, reply);
1896
+ client.once("close", () => clients.delete(client));
1897
+ client.on("error", () => {});
1898
+ client.setEncoding("utf8");
1899
+ let raw = "";
1900
+ let received = false;
1901
+ client.on("data", (chunk) => {
1902
+ if (received) return;
1903
+ raw += chunk;
1904
+ if (raw.length > SDK_MAX_LINE_CHARS) { received = true; reply(2, "reason-sdk: the request is too large."); return; }
1905
+ const end = raw.indexOf("\n");
1906
+ if (end < 0) return;
1907
+ received = true;
1908
+ const parsed = machineSdkCall(raw.slice(0, end));
1909
+ if (parsed.error) reply(2, parsed.error);
1910
+ else if (!live) reply(69, SDK_NOT_LIVE);
1911
+ else forwardSdkCall(jobRequestId, parsed.call, reply);
1912
+ });
1913
+ });
1914
+ server.maxConnections = 16;
1915
+ const ready = new Promise((resolve, reject) => {
1916
+ server.once("error", reject);
1917
+ server.listen(socketPath, () => {
1918
+ try { fs.chmodSync(socketPath, 0o600); resolve(); } catch (error) { reject(error); }
1919
+ });
1920
+ });
1921
+ void ready.catch(() => {});
1922
+ const endpoint = {
1923
+ ready,
1924
+ prelude: "export PATH=" + shellQuote(base.bin) + '":$PATH" REASON_SDK_SOCKET=' + shellQuote(socketPath) + "; ",
1925
+ close() {
1926
+ if (!live) return;
1927
+ live = false;
1928
+ sdkEndpoints.delete(endpoint);
1929
+ server.close();
1930
+ for (const [id, entry] of sdkPending) {
1931
+ if (entry.jobRequestId !== jobRequestId) continue;
1932
+ sdkPending.delete(id);
1933
+ entry.reply(69, SDK_NOT_LIVE);
1934
+ }
1935
+ for (const reply of [...clients.values()]) reply(69, SDK_NOT_LIVE);
1936
+ fs.rmSync(socketPath, { force: true });
1937
+ },
1938
+ };
1939
+ sdkEndpoints.add(endpoint);
1940
+ return endpoint;
1941
+ }
1942
+ function refreshJobEnvSnapshot() {
1943
+ if (config.jobEnvSourcePath === undefined) return true;
1944
+ const temporaryPath = config.jobEnvPath + "." + crypto.randomUUID() + ".tmp";
1945
+ try {
1946
+ const source = fs.lstatSync(config.jobEnvSourcePath);
1947
+ if (source.isSymbolicLink() || !source.isFile()) return false;
1948
+ fs.copyFileSync(
1949
+ config.jobEnvSourcePath,
1950
+ temporaryPath,
1951
+ fs.constants.COPYFILE_EXCL,
1952
+ );
1953
+ fs.chmodSync(temporaryPath, 0o600);
1954
+ fs.renameSync(temporaryPath, config.jobEnvPath);
1955
+ return true;
1956
+ } catch {
1957
+ try { fs.unlinkSync(temporaryPath); } catch (error) { void error; }
1958
+ return false;
1959
+ }
1960
+ }
1961
+ // The descriptor is opened and read synchronously immediately before child
1962
+ // spawn, after any asynchronous policy preparation. Atomic replacement is
1963
+ // allowed; links, oversized files and mutable workspace paths are not.
1964
+ let consumedEnvironment = null;
1965
+ function readPrivateEnvironment() {
1966
+ const candidate = config.environmentSnapshotPath;
1967
+ const parent = path.dirname(candidate);
1968
+ if (fs.realpathSync(parent) !== parent) throw new Error("environment_unavailable");
1969
+ const directory = fs.lstatSync(parent);
1970
+ if (!directory.isDirectory() || directory.uid !== process.getuid() || (directory.mode & 0o7777) !== 0o700) throw new Error("environment_unavailable");
1971
+ for (const root of [config.workRoot, config.repoPath]) {
1972
+ const canonical = fs.realpathSync(root);
1973
+ if (stateOverlapsWorkspace(parent, canonical)) throw new Error("environment_unavailable");
1974
+ }
1975
+ const fd = fs.openSync(candidate, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
1976
+ try {
1977
+ const before = fs.fstatSync(fd);
1978
+ if (!before.isFile() || before.uid !== process.getuid() || before.nlink !== 1
1979
+ || ![0o600, 0o400].includes(before.mode & 0o7777) || before.size < 1 || before.size > DEVICE_ENVIRONMENT_LIMITS.snapshotBytes) throw new Error("environment_unavailable");
1980
+ const bytes = Buffer.alloc(before.size + 1);
1981
+ let length = 0;
1982
+ while (length < bytes.length) {
1983
+ const count = fs.readSync(fd, bytes, length, bytes.length - length, null);
1984
+ if (!count) break;
1985
+ length += count;
1986
+ }
1987
+ const after = fs.fstatSync(fd);
1988
+ if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs
1989
+ || after.ctimeMs !== before.ctimeMs || after.nlink !== 1) throw new Error("environment_unavailable");
1990
+ const contents = bytes.subarray(0, length);
1991
+ return { snapshot: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(contents)), fileSha256: digest(contents) };
1992
+ } finally { fs.closeSync(fd); }
1993
+ }
1994
+ function validateEnvironmentSnapshot(snapshot) {
1995
+ const authority = config.environmentExpectedBinding;
1996
+ if (!hasOnlyKeys(snapshot, ["version", "snapshot_id", "revision", "run_id", "device_id", "root_id", "binding", "expires_at", "variables", "git", "github_auth", "cloud_access"])
1997
+ || snapshot.version !== 1 || !UUID_RE.test(snapshot.snapshot_id)
1998
+ || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1
1999
+ || snapshot.run_id !== authority.runId || snapshot.device_id !== authority.deviceId || snapshot.root_id !== authority.rootId
2000
+ || !hasOnlyKeys(snapshot.binding, ["kind", "target_revision", "root_grant_revision", "target_generation", "target_fence_id"])
2001
+ || snapshot.binding.kind !== authority.binding.kind || snapshot.binding.target_revision !== authority.binding.target_revision
2002
+ || snapshot.binding.root_grant_revision !== authority.binding.root_grant_revision
2003
+ || snapshot.binding.target_generation !== targetGeneration || snapshot.binding.target_fence_id !== targetFenceId
2004
+ || !hasOnlyKeys(snapshot.variables, Object.keys(snapshot.variables || {})) || Object.keys(snapshot.variables).length > DEVICE_ENVIRONMENT_LIMITS.variables) throw new Error("environment_unavailable");
2005
+ const expires = Date.parse(snapshot.expires_at);
2006
+ if (!Number.isFinite(expires) || expires <= Date.now() || expires > config.expiresAtMs) throw new Error("environment_unavailable");
2007
+ for (const [name, value] of Object.entries(snapshot.variables)) {
2008
+ if (name.length > 128 || !DEVICE_ENVIRONMENT_NAME_RE.test(name) || typeof value !== "string" || Buffer.byteLength(value, "utf8") > DEVICE_ENVIRONMENT_LIMITS.valueBytes || value.includes("\0")) throw new Error("environment_unavailable");
2009
+ }
2010
+ if (snapshot.github_auth !== undefined && (snapshot.github_auth !== "local_gh" || snapshot.git !== null
2011
+ || !["darwin", "linux"].includes(process.platform) || !config.executionEnvironment || config.environmentExpectedBinding.binding.kind !== "workspace_source")) {
2012
+ throw new Error("environment_unavailable");
2013
+ }
2014
+ if (snapshot.git !== null) {
2015
+ const git = snapshot.git;
2016
+ if (!hasOnlyKeys(git, ["base_url", "token", "expires_at"]) || typeof git.base_url !== "string" || git.base_url.length > 2048
2017
+ || typeof git.token !== "string" || git.token.length < 1 || git.token.length > 4096 || !/^[A-Za-z0-9._~-]+$/.test(git.token)) throw new Error("environment_unavailable");
2018
+ const url = new URL(git.base_url);
2019
+ const gitExpires = Date.parse(git.expires_at);
2020
+ if (url.origin !== new URL(authority.apiOrigin).origin || !["/git", "/git/"].includes(url.pathname)
2021
+ || url.username || url.password || url.search || url.hash || !Number.isFinite(gitExpires)
2022
+ || gitExpires <= Date.now() || gitExpires > expires) throw new Error("environment_unavailable");
2023
+ }
2024
+ if (snapshot.cloud_access !== undefined && !cloudAccessVariables(snapshot.cloud_access, authority.apiOrigin)) throw new Error("environment_unavailable");
2025
+ const hash = digest(JSON.stringify({ ...snapshot,
2026
+ variables: Object.fromEntries(Object.entries(snapshot.variables).sort(([left], [right]) => left.localeCompare(right))),
2027
+ }));
2028
+ if (consumedEnvironment && (snapshot.revision < consumedEnvironment.revision
2029
+ || (snapshot.revision === consumedEnvironment.revision && hash !== consumedEnvironment.hash))) throw new Error("environment_unavailable");
2030
+ return { snapshot, hash };
2031
+ }
2032
+ // The reserved cloud access variables (shared/device-environment.ts
2033
+ // cloudAccessEnvironment), or null unless both URLs are exactly the policy's
2034
+ // endpoints on this worker's API origin.
2035
+ function cloudAccessVariables(access, apiOrigin) {
2036
+ if (!access || typeof access !== "object" || !hasOnlyKeys(access, ["token_url", "request_token", "aws_credentials_url"])
2037
+ || typeof access.request_token !== "string" || access.request_token.length > 8192
2038
+ || !new RegExp(DEVICE_CLOUD_ACCESS_POLICY.tokenPattern).test(access.request_token)) return null;
2039
+ const exact = (value, pathname) => {
2040
+ if (typeof value !== "string" || value.length > 2048) return false;
2041
+ try {
2042
+ const url = new URL(value);
2043
+ return url.origin === new URL(apiOrigin).origin && url.pathname === pathname && !url.username && !url.password && !url.search && !url.hash;
2044
+ } catch { return false; }
2045
+ };
2046
+ if (!exact(access.token_url, DEVICE_CLOUD_ACCESS_POLICY.tokenPath)
2047
+ || (access.aws_credentials_url !== null && !exact(access.aws_credentials_url, DEVICE_CLOUD_ACCESS_POLICY.awsPath))) return null;
2048
+ return { REASON_OIDC_TOKEN_URL: access.token_url, REASON_OIDC_REQUEST_TOKEN: access.request_token,
2049
+ ...(access.aws_credentials_url !== null ? { AWS_CONTAINER_CREDENTIALS_FULL_URI: access.aws_credentials_url,
2050
+ AWS_CONTAINER_AUTHORIZATION_TOKEN: access.request_token } : {}) };
2051
+ }
2052
+ ${n}
2053
+ // Pi's shell's global Git config: a private file beside this worker's config,
2054
+ // so git config --global works for the Hands session, as stock Pi's does. The
2055
+ // path is in the shell's environment, where the person reads it, so the file
2056
+ // carries the product's name. Each worker launch has its own new directory
2057
+ // (cli/local-hands-relay.ts), so no earlier file exists to carry over.
2058
+ const PI_GIT_CONFIG_GLOBAL = path.join(path.dirname(process.argv[2]), "reason-gitconfig");
2059
+ /** Spawns that waited for their environment, by request id: reported once on the result. */
2060
+ const environmentWaits = new Map();
2061
+ /** Set when a claim negotiated environmentWaitVersion (only this build's server, for resident takes): spawns may wait. */
2062
+ let environmentWaitNegotiated = false;
2063
+ /** How long a spawn waits for its generation's environment before it refuses (environment_unavailable). */
2064
+ const SPAWN_ENVIRONMENT_WAIT_MS = 2_000;
2065
+ /** Waits (bounded) until this generation's environment validates; returns the ms waited. Never validates
2066
+ * anything itself: the spawn below still calls environmentForSpawn, which refuses on its own. */
2067
+ async function waitForSpawnEnvironment(piShell, confined) {
2068
+ const startedAt = performance.now();
2069
+ for (;;) {
2070
+ try { environmentForSpawn(piShell, confined); return Math.round(performance.now() - startedAt); }
2071
+ catch (error) {
2072
+ if (!(error instanceof Error && error.message === "environment_unavailable")
2073
+ || performance.now() - startedAt >= SPAWN_ENVIRONMENT_WAIT_MS) return Math.round(performance.now() - startedAt);
2074
+ }
2075
+ await new Promise((resolve) => setTimeout(resolve, 25));
2076
+ }
2077
+ }
2078
+ function environmentForSpawn(piShell = false, confined = false) {
2079
+ const privateFile = readPrivateEnvironment();
2080
+ const validated = validateEnvironmentSnapshot(privateFile.snapshot);
2081
+ const snapshot = validated.snapshot;
2082
+ const deviceRoot = fs.realpathSync(config.deviceRootPath || config.workRoot);
2083
+ const workspace = fs.realpathSync(config.workRoot);
2084
+ const repo = fs.realpathSync(config.repoPath);
2085
+ const within = (candidate, root) => candidate === root || candidate.startsWith(root + path.sep);
2086
+ if (!within(workspace, deviceRoot) || !within(repo, workspace)) throw new Error("environment_unavailable");
2087
+ let pool;
2088
+ if (config.workspacePoolPath !== undefined) {
2089
+ pool = fs.realpathSync(config.workspacePoolPath);
2090
+ if (!within(pool, deviceRoot) || path.dirname(workspace) !== pool) throw new Error("environment_unavailable");
2091
+ }
2092
+ // Never spread process.env here. Explicit local host-mode composition may
2093
+ // preserve toolchain coordinates, and for Pi's shell the host's filtered
2094
+ // variables (an image's GOPATH, LANG...) below the snapshot; secrets/loaders
2095
+ // still come only from the fenced snapshot, and shell/Git controls cannot be
2096
+ // overridden.
2097
+ const baseline = config.executionEnvironment;
2098
+ const temporaryDirectory = baseline?.temporaryDirectory ?? config.workRoot;
2099
+ const hostVariables = piShell && !(snapshot.github_auth === "local_gh" && localGithubSandbox) ? baseline?.variables : undefined;
2100
+ const env = { ...hostVariables, ...snapshot.variables, PATH: baseline?.path ?? "/opt/homebrew/bin:" + FIXED_CLOUD_CHILD_PATH,
2101
+ HOME: baseline?.home ?? config.workRoot, PWD: config.repoPath, TMPDIR: temporaryDirectory, TMP: temporaryDirectory, TEMP: temporaryDirectory,
2102
+ SHELL: FIXED_BASH, BASH_ENV: "/dev/null", ENV: "/dev/null",
2103
+ // Each coordinate under its Reason name and the Ara name every server
2104
+ // script and Session shell before the rename reads.
2105
+ REASON_DEVICE_ROOT: deviceRoot, REASON_HANDS_WORKSPACE: workspace, REASON_HANDS_REPO: repo,
2106
+ ARA_DEVICE_ROOT: deviceRoot, ARA_HANDS_WORKSPACE: workspace, ARA_HANDS_REPO: repo,
2107
+ ...(pool === undefined ? {} : { REASON_HANDS_WORKSPACE_POOL: pool, ARA_HANDS_WORKSPACE_POOL: pool }),
2108
+ GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_SYSTEM: "/dev/null", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0",
2109
+ };
2110
+ const pairs = [["credential.helper", ""], ["http.extraheader", ""]];
2111
+ let confinementPrefix;
2112
+ if (snapshot.github_auth === "local_gh" && config.localGithubConfinement !== true) throw new Error("local_device_gh_confinement_unavailable");
2113
+ if (snapshot.github_auth === "local_gh" && localGithubSandbox) {
2114
+ if (config.spawnPolicy || config.bashCommandPrefix
2115
+ || !fs.existsSync("/usr/bin/sandbox-exec")) throw new Error("local_device_gh_confinement_unavailable");
2116
+ const temporary = path.join(workspace, ".ara-local-gh-tmp");
2117
+ fs.mkdirSync(temporary, { recursive: true, mode: 0o700 });
2118
+ if (fs.lstatSync(temporary).isSymbolicLink() || fs.realpathSync(temporary) !== temporary) throw new Error("local_device_gh_confinement_unavailable");
2119
+ env.HOME = workspace;
2120
+ env.OPENSSL_CONF = "/dev/null";
2121
+ env.TMPDIR = temporary; env.TMP = temporary; env.TEMP = temporary;
2122
+ env.PATH = ${JSON.stringify(h)};
2123
+ for (const name of Object.keys(env)) if (name.startsWith("GH_") || name.startsWith("GITHUB_") || name.startsWith("GIT_TRACE")) delete env[name];
2124
+ confinementPrefix = ["/usr/bin/sandbox-exec", "-p", localGithubSandboxProfile(workspace, temporary)];
2125
+ }
2126
+ if (config.localGithubConfinement === true && snapshot.github_auth !== "local_gh") throw new Error("local_device_gh_authority_changed");
2127
+ if (snapshot.git && config.gitCredentialEnvironment === true) {
2128
+ const helper = GIT_BROKER_CREDENTIAL_HELPER_POLICY;
2129
+ const base = snapshot.git.base_url.replace(new RegExp(GIT_BROKER_HTTP_HEADER_POLICY.trailingSlashPattern), "");
2130
+ env[helper.urlEnv] = base;
2131
+ env[helper.tokenEnv] = snapshot.git.token;
2132
+ env[helper.reasonTokenEnv] = snapshot.git.token;
2133
+ pairs.push([helper.keyPrefix + base + helper.keySuffix, helper.value]);
2134
+ } else if (snapshot.git) {
2135
+ const policy = GIT_BROKER_HTTP_HEADER_POLICY;
2136
+ const base = snapshot.git.base_url.replace(new RegExp(policy.trailingSlashPattern), "");
2137
+ pairs.push([policy.keyPrefix + base + policy.keySuffix, policy.valuePrefix + snapshot.git.token]);
2138
+ }
2139
+ // Server-provided, after the customer variables (which can never carry these names).
2140
+ if (snapshot.cloud_access !== undefined) Object.assign(env, cloudAccessVariables(snapshot.cloud_access, config.environmentExpectedBinding.apiOrigin));
2141
+ env.GIT_CONFIG_COUNT = String(pairs.length);
2142
+ pairs.forEach(([key, value], index) => { env["GIT_CONFIG_KEY_" + index] = key; env["GIT_CONFIG_VALUE_" + index] = value; });
2143
+ // Local GitHub confinement denies files outside the workspace, so it keeps
2144
+ // the empty /dev/null config.
2145
+ // A confined payload cannot read the worker's private files; it keeps the bash tool's empty config.
2146
+ if (piShell && !confinementPrefix && !confined) env.GIT_CONFIG_GLOBAL = PI_GIT_CONFIG_GLOBAL;
2147
+ return { ...validated, env, confinementPrefix, fileSha256: privateFile.fileSha256 };
2148
+ }
2149
+ ${s}const WORKSPACE_FILE_OPERATIONS_RUNTIME_SOURCE = ${JSON.stringify(s)};
2150
+ // One workspace file call in a spawned child (sandbox-exec confined under local
2151
+ // GitHub authority), bounded by workspaceTarget: inside the canonical
2152
+ // repository root, with no symlink path component. The typed file tools and
2153
+ // Pi's confined file operations share this boundary.
2154
+ function workspaceFileScript(request, body, nodeRedirect = "") {
2155
+ const encodedRequest = Buffer.from(JSON.stringify(request)).toString("base64");
2156
+ const program = [
2157
+ '"use strict";',
2158
+ 'const crypto = require("node:crypto");',
2159
+ 'const fs = require("node:fs");',
2160
+ 'const path = require("node:path");',
2161
+ 'const config = { repoPath: process.cwd() };',
2162
+ 'const MAX_OUTPUT_CHARS = ' + MAX_OUTPUT_CHARS + ';',
2163
+ 'const success = (_id, stdout, stderr) => ({ code: 0, stdout, stderr });',
2164
+ 'const failure = (_id, code, _kind, stdout, stderr) => ({ code, stdout, stderr });',
2165
+ WORKSPACE_FILE_OPERATIONS_RUNTIME_SOURCE,
2166
+ 'const request = JSON.parse(Buffer.from("' + encodedRequest + '", "base64").toString("utf8"));',
2167
+ body,
2168
+ ].join("\n");
2169
+ return "node" + nodeRedirect + " <<'ARA_WORKSPACE_FILE_OPERATION'\n" + program + "\nARA_WORKSPACE_FILE_OPERATION\n";
2170
+ }
2171
+ function payloadFileOperation(request) {
2172
+ return bashOperation({ id: request.id, input: { script: workspaceFileScript(request,
2173
+ 'fileOperation(request).promise.then(result => { process.stdout.write(result.stdout); process.stderr.write(result.stderr); process.exitCode = result.code; });'),
2174
+ } }, null);
2175
+ }
2176
+ // Pi's read/edit/write operations under local GitHub confinement. A read
2177
+ // returns raw bytes through the unclipped Pi shell stream (the child's stderr
2178
+ // is discarded so nothing interleaves) and is shared by access, image sniffing
2179
+ // and readFile within one tool call. writeFile is the typed tool's checked
2180
+ // atomic write, which creates parent directories after checking the target, so
2181
+ // mkdir defers to it rather than creating directories ahead of that check.
2182
+ // Cancelling the tool call stops the child like a cancelled typed file tool.
2183
+ function confinedPiFileOperations(id, state, writable, signal) {
2184
+ const run = (operation) => {
2185
+ const stop = () => { void operation.cancel().catch(() => {}); };
2186
+ if (signal.aborted) stop();
2187
+ else signal.addEventListener("abort", stop, { once: true });
2188
+ return operation.promise.then((result) => {
2189
+ signal.removeEventListener("abort", stop);
2190
+ if (result.error === "process_group_cleanup_unconfirmed") state.cleanupUnconfirmed = true;
2191
+ return result;
2192
+ });
2193
+ };
2194
+ const reads = new Map();
2195
+ const read = (file) => {
2196
+ if (!reads.has(file)) {
2197
+ const chunks = [];
2198
+ const script = workspaceFileScript({ path: file, writable }, [
2199
+ 'let reply;',
2200
+ 'try {',
2201
+ ' const { target } = workspaceTarget(request.path);',
2202
+ ' fs.accessSync(target, request.writable ? fs.constants.R_OK | fs.constants.W_OK : fs.constants.R_OK);',
2203
+ ' reply = { data: fs.readFileSync(target).toString("base64") };',
2204
+ '} catch (error) {',
2205
+ ' reply = { error: error instanceof Error ? error.message : "workspace_file_failed", code: typeof error?.code === "string" ? error.code : undefined };',
2206
+ '}',
2207
+ 'process.stdout.write(JSON.stringify(reply));',
2208
+ ].join("\n"), " 2>/dev/null");
2209
+ reads.set(file, run(bashOperation({ id, input: { script } }, null, null, { onData: (chunk) => chunks.push(chunk) }))
2210
+ .then((result) => {
2211
+ if (!result.ok) throw new Error(result.error || "workspace_file_failed");
2212
+ const reply = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2213
+ if (typeof reply.error === "string") throw Object.assign(new Error(reply.error), reply.code ? { code: reply.code } : {});
2214
+ return Buffer.from(reply.data, "base64");
2215
+ }));
2216
+ }
2217
+ return reads.get(file);
2218
+ };
2219
+ return {
2220
+ access: (file) => read(file).then(() => undefined),
2221
+ readFile: read,
2222
+ // Pi's own sniffing over the leading bytes its file-based default reads.
2223
+ detectImageMimeType: (file) => read(file).then((bytes) => loadPiTools().detectSupportedImageMimeType(bytes.subarray(0, 4100))),
2224
+ writeFile: (file, content) => run(payloadFileOperation({ id, tool: "write", input: { path: file, content } })).then((result) => {
2225
+ reads.delete(file);
2226
+ if (!result.ok) throw new Error(result.stderr || result.error || "workspace_file_failed");
2227
+ }),
2228
+ mkdir: () => Promise.resolve(),
2229
+ };
2230
+ }
2231
+ function prepareBashSpawn(argv, extraFileDescriptors, expiresAtMs, signal, onLost) {
2232
+ const policy = config.spawnPolicy;
2233
+ const claimedGeneration = targetGeneration;
2234
+ const claimedFence = targetFenceId;
2235
+ return new Promise((resolve, reject) => {
2236
+ const parent = path.dirname(policy.socketPath);
2237
+ const directory = fs.lstatSync(parent);
2238
+ const endpoint = fs.lstatSync(policy.socketPath);
2239
+ if (fs.realpathSync(parent) !== parent || !directory.isDirectory() || directory.uid !== process.getuid()
2240
+ || (directory.mode & 0o777) !== 0o700 || !endpoint.isSocket() || endpoint.uid !== process.getuid()
2241
+ || (endpoint.mode & 0o077) !== 0) throw new Error("invalid_spawn_socket");
2242
+ const id = crypto.randomUUID();
2243
+ const client = net.createConnection(policy.socketPath);
2244
+ let raw = "";
2245
+ let prepared = null;
2246
+ let preparedEnvironmentSha256;
2247
+ let authorized = false;
2248
+ let released = false;
2249
+ const current = () => !stopped && !signal.aborted && claimedGeneration === targetGeneration
2250
+ && claimedFence === targetFenceId && Date.now() < expiresAtMs;
2251
+ const abort = () => client.destroy();
2252
+ const close = (confirmed) => {
2253
+ if (released) return;
2254
+ released = true;
2255
+ signal.removeEventListener("abort", abort);
2256
+ if (confirmed && !client.destroyed) client.end(JSON.stringify({ version: 1, type: "release", id }) + "\n");
2257
+ else client.destroy();
2258
+ };
2259
+ signal.addEventListener("abort", abort, { once: true });
2260
+ client.setEncoding("utf8");
2261
+ client.setTimeout(5000, abort);
2262
+ client.on("error", () => {});
2263
+ client.once("close", () => {
2264
+ signal.removeEventListener("abort", abort);
2265
+ if (!authorized) reject(new Error("spawn_policy_rejected"));
2266
+ else if (!released) onLost();
2267
+ });
2268
+ client.once("connect", () => {
2269
+ if (!current()) { client.destroy(); return; }
2270
+ client.write(JSON.stringify({ version: 1, type: "prepare", id, scopeId: policy.scopeId,
2271
+ targetRevision: policy.targetRevision, generation: claimedGeneration, fenceId: claimedFence,
2272
+ executorLaunchId: config.launchId, expiresAtMs, cwd: fs.realpathSync(config.repoPath), argv, extraFileDescriptors }) + "\n");
2273
+ });
2274
+ client.on("data", (chunk) => {
2275
+ raw += chunk;
2276
+ if (Buffer.byteLength(raw) > 262144) { client.destroy(); return; }
2277
+ const newline = raw.indexOf("\n");
2278
+ if (newline < 0) return;
2279
+ if (newline !== raw.length - 1 || !current()) { client.destroy(); return; }
2280
+ let frame;
2281
+ try { frame = JSON.parse(raw.slice(0, -1)); } catch { client.destroy(); return; }
2282
+ raw = "";
2283
+ if (frame.version !== 1 || frame.id !== id) { client.destroy(); return; }
2284
+ if (!prepared && frame.type === "prepared" && hasOnlyKeys(frame, ["version", "id", "type", "argv", "cwd", "environmentSnapshotSha256"])
2285
+ && (config.environmentSnapshotPath === undefined ? frame.environmentSnapshotSha256 === undefined
2286
+ : typeof frame.environmentSnapshotSha256 === "string" && SHA256_RE.test(frame.environmentSnapshotSha256))
2287
+ && frame.cwd === fs.realpathSync(config.repoPath) && Array.isArray(frame.argv) && frame.argv.length > 0 && frame.argv.length <= 32
2288
+ && validPath(frame.argv[0]) && frame.argv.every((arg) => typeof arg === "string" && !arg.includes("\0"))) {
2289
+ prepared = frame.argv;
2290
+ preparedEnvironmentSha256 = frame.environmentSnapshotSha256;
2291
+ client.write(JSON.stringify({ version: 1, type: "activate", id }) + "\n");
2292
+ } else if (prepared && !authorized && frame.type === "spawn" && hasOnlyKeys(frame, ["version", "id", "type", "expiresAtMs"])
2293
+ && Number.isSafeInteger(frame.expiresAtMs) && frame.expiresAtMs > Date.now() && frame.expiresAtMs <= expiresAtMs) {
2294
+ authorized = true; client.setTimeout(0);
2295
+ signal.removeEventListener("abort", abort);
2296
+ resolve({ argv: prepared, environmentSnapshotSha256: preparedEnvironmentSha256,
2297
+ current: () => current() && Date.now() < frame.expiresAtMs, release: () => close(true), abandon: () => close(false) });
2298
+ } else client.destroy();
2299
+ });
2300
+ });
2301
+ }
2302
+ let nativePtyOwner = null;
2303
+ function ptySupported() {
2304
+ if (localGithubSandbox || !config.managedPty || config.environmentSnapshotPath === undefined || config.spawnPolicy || config.bashCommandPrefix
2305
+ || !["darwin", "linux"].includes(process.platform) || typeof Bun === "undefined" || typeof Bun.Terminal !== "function") return false;
2306
+ if (process.platform === "linux" && !["x64", "arm64"].includes(process.arch)) return false;
2307
+ try { platformPtyOwner(); return true; } catch { return false; }
2308
+ }
2309
+ function platformPtyOwner() { return process.platform === "linux" ? linuxPtyOwner() : macPtyOwner(); }
2310
+ /** Only live native process identities, never recovered PIDs. Darwin's audit
2311
+ * token includes the kernel PID version; stale versions cannot signal a reused
2312
+ * PID. Keep the trusted session leader alive until its interactive groups are
2313
+ * empty. This is graceful PTY-attached session cleanup, NOT supervisor-crash containment or
2314
+ * confinement against host-mode code that deliberately escapes its session. */
2315
+ function macPtyOwner() {
2316
+ if (nativePtyOwner) return nativePtyOwner;
2317
+ const { dlopen, ptr, read } = require("bun:ffi");
2318
+ const library = dlopen("/usr/lib/libSystem.B.dylib", {
2319
+ mach_task_self: { args: [], returns: "u32" },
2320
+ task_name_for_pid: { args: ["u32", "i32", "ptr"], returns: "i32" },
2321
+ task_info: { args: ["u32", "u32", "ptr", "ptr"], returns: "i32" },
2322
+ mach_port_deallocate: { args: ["u32", "u32"], returns: "i32" },
2323
+ proc_signal_with_audittoken: { args: ["ptr", "i32"], returns: "i32" },
2324
+ proc_listpids: { args: ["u32", "u32", "ptr", "i32"], returns: "i32" },
2325
+ proc_pidinfo: { args: ["i32", "i32", "u64", "ptr", "i32"], returns: "i32" },
2326
+ getsid: { args: ["i32"], returns: "i32" },
2327
+ getpgid: { args: ["i32"], returns: "i32" },
2328
+ socketpair: { args: ["i32", "i32", "i32", "ptr"], returns: "i32" },
2329
+ ioctl: { args: ["i32", "u64"], returns: "i32" },
2330
+ setsockopt: { args: ["i32", "i32", "i32", "ptr", "u32"], returns: "i32" },
2331
+ recv: { args: ["i32", "ptr", "u64", "i32"], returns: "i64_fast" },
2332
+ send: { args: ["i32", "ptr", "u64", "i32"], returns: "i64_fast" },
2333
+ __error: { args: [], returns: "ptr" },
2334
+ });
2335
+ const api = library.symbols, self = api.mach_task_self();
2336
+ function identity(pid) {
2337
+ const port = new Uint32Array(1), count = new Uint32Array([8]), token = new Uint32Array(8);
2338
+ if (api.task_name_for_pid(self, pid, ptr(port)) !== 0) return null;
2339
+ try {
2340
+ // TASK_AUDIT_TOKEN / TASK_AUDIT_TOKEN_COUNT from the macOS SDK.
2341
+ if (api.task_info(port[0], 15, ptr(token), ptr(count)) !== 0 || count[0] !== 8 || token[5] !== pid) return null;
2342
+ return token;
2343
+ } finally { api.mach_port_deallocate(self, port[0]); }
2344
+ }
2345
+ function same(token) {
2346
+ const current = identity(token[5]);
2347
+ return current && current[5] === token[5] && current[7] === token[7];
2348
+ }
2349
+ function ttyDevice(anchor) {
2350
+ // 64-bit proc_bsdinfo / PROC_PIDTBSDINFO from the current macOS SDK.
2351
+ const info = Buffer.alloc(136);
2352
+ if (!same(anchor) || api.proc_pidinfo(anchor[5], 3, 0, ptr(info), info.length) !== info.length
2353
+ || info.readUInt32LE(12) !== anchor[5] || !(info.readUInt32LE(0) & 0x40)) throw new Error("pty_terminal_identity_lost");
2354
+ return info.readUInt32LE(108);
2355
+ }
2356
+ function session(anchor, device) {
2357
+ if (!same(anchor) || api.getsid(anchor[5]) !== anchor[5] || ttyDevice(anchor) !== device) throw new Error("pty_session_identity_lost");
2358
+ const pids = new Int32Array(32768);
2359
+ const bytes = api.proc_listpids(3, device, ptr(pids), pids.byteLength); // PROC_TTY_ONLY
2360
+ const count = bytes / 4;
2361
+ if (!Number.isSafeInteger(count) || count < 1 || count >= pids.length
2362
+ || !pids.subarray(0, count).includes(anchor[5])) throw new Error("pty_session_inventory_unconfirmed");
2363
+ const members = [];
2364
+ for (let index = 0; index < count; index++) {
2365
+ const pid = pids[index];
2366
+ if (pid <= 0 || pid === anchor[5]) continue;
2367
+ const member = inspectMacPtyMember(anchor, pid, { identity, same, session(memberPid) {
2368
+ const id = api.getsid(memberPid);
2369
+ return { id, missing: id === -1 && read.i32(api.__error()) === 3 };
2370
+ } });
2371
+ // No signals from a partially invalidated inventory. The owning bounded
2372
+ // sweep re-inventories while its anchor remains verified; it never turns
2373
+ // a vanished or unreadable member into empty-session proof.
2374
+ if (member.retry) return { members: [], empty: false };
2375
+ members.push(member.token);
2376
+ }
2377
+ // Do not mistake a reused session number for the original live session.
2378
+ if (!same(anchor) || ttyDevice(anchor) !== device) throw new Error("pty_session_identity_lost");
2379
+ // Empty comes from the kernel-filtered snapshot, not from failed per-PID
2380
+ // lookups. Fork/exit races cannot turn vanished identities into empty proof.
2381
+ return { members, empty: count === 1 };
2382
+ }
2383
+ function signal(token, number) {
2384
+ const result = api.proc_signal_with_audittoken(ptr(token), number);
2385
+ if (result !== 0 && result !== 3) throw new Error("pty_signal_unconfirmed"); // ESRCH is already gone.
2386
+ }
2387
+ function controlPair() {
2388
+ const descriptors = new Int32Array(2);
2389
+ if (api.socketpair(1, 1, 0, ptr(descriptors)) !== 0) throw new Error("pty_control_unavailable");
2390
+ try {
2391
+ // AF_UNIX/SOCK_STREAM; FIOCLEX has no ioctl argument. Avoid fcntl's
2392
+ // variadic integer ABI (different from a fixed-argument FFI on arm64).
2393
+ const enabled = new Int32Array([1]);
2394
+ for (const fd of descriptors) {
2395
+ if (api.ioctl(fd, 0x20006601) !== 0 || api.setsockopt(fd, 0xffff, 0x1022, ptr(enabled), 4) !== 0) {
2396
+ throw new Error("pty_control_unavailable");
2397
+ }
2398
+ }
2399
+ return [...descriptors];
2400
+ } catch (error) { for (const fd of descriptors) fs.closeSync(fd); throw error; }
2401
+ }
2402
+ function readControl(fd) {
2403
+ const bytes = Buffer.alloc(256);
2404
+ const size = api.recv(fd, ptr(bytes), bytes.length, 0x80); // MSG_DONTWAIT
2405
+ if (size < 0) {
2406
+ if ([4, 35].includes(read.i32(api.__error()))) return ""; // EINTR/EAGAIN
2407
+ throw new Error("pty_control_read_failed");
2408
+ }
2409
+ if (!size) throw new Error("pty_control_closed");
2410
+ return bytes.subarray(0, size).toString("ascii");
2411
+ }
2412
+ function releaseControl(fd) {
2413
+ const bytes = Buffer.from("release\n");
2414
+ if (api.send(fd, ptr(bytes), bytes.length, 0x80) !== bytes.length) throw new Error("pty_control_release_failed");
2415
+ }
2416
+ nativePtyOwner = { identity, same, session, ttyDevice, signal, controlPair, readControl, releaseControl,
2417
+ identityKey: (token) => token[5] + ":" + token[7], group: api.getpgid, sid: api.getsid };
2418
+ if (!identity(process.pid)) { nativePtyOwner = null; library.close(); throw new Error("pty_identity_unavailable"); }
2419
+ return nativePtyOwner;
2420
+ }
2421
+ function linuxPtyOwner() {
2422
+ if (nativePtyOwner) return nativePtyOwner;
2423
+ const { dlopen, ptr, read } = require("bun:ffi");
2424
+ const library = dlopen("libc.so.6", {
2425
+ pidfd_open: { args: ["i32", "u32"], returns: "i32" },
2426
+ pidfd_send_signal: { args: ["i32", "i32", "ptr", "u32"], returns: "i32" },
2427
+ poll: { args: ["ptr", "u64", "i32"], returns: "i32" },
2428
+ getsid: { args: ["i32"], returns: "i32" },
2429
+ getpgid: { args: ["i32"], returns: "i32" },
2430
+ socketpair: { args: ["i32", "i32", "i32", "ptr"], returns: "i32" },
2431
+ recv: { args: ["i32", "ptr", "u64", "i32"], returns: "i64_fast" },
2432
+ send: { args: ["i32", "ptr", "u64", "i32"], returns: "i64_fast" },
2433
+ __errno_location: { args: [], returns: "ptr" },
2434
+ });
2435
+ const api = library.symbols;
2436
+ const errno = () => read.i32(api.__errno_location());
2437
+ function stat(pid) {
2438
+ const raw = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
2439
+ const end = raw.lastIndexOf(")"), fields = raw.slice(end + 2).trim().split(/\s+/);
2440
+ if (!raw.startsWith(pid + " (") || end < 0 || fields.length < 20 || !/^\d+$/.test(fields[19])) throw new Error("pty_process_identity_unconfirmed");
2441
+ return { parent: Number(fields[1]), tty: Number(fields[4]), birth: fields[19] };
2442
+ }
2443
+ function same(token) {
2444
+ const pollfd = Buffer.alloc(8);
2445
+ pollfd.writeInt32LE(token.fd, 0); pollfd.writeInt16LE(1, 4); // POLLIN reports exit, including an unreaped zombie.
2446
+ const result = api.poll(ptr(pollfd), 1, 0);
2447
+ if (result < 0) throw new Error("pty_identity_poll_failed");
2448
+ return result === 0;
2449
+ }
2450
+ function identity(pid) {
2451
+ const fd = api.pidfd_open(pid, 0);
2452
+ if (fd < 0) {
2453
+ if (errno() === 3) return null;
2454
+ throw new Error("pty_pidfd_unavailable");
2455
+ }
2456
+ try {
2457
+ const record = stat(pid), token = { pid, fd, birth: record.birth };
2458
+ if (!same(token)) { fs.closeSync(fd); return null; }
2459
+ return token;
2460
+ } catch (error) {
2461
+ fs.closeSync(fd);
2462
+ if (error.code === "ENOENT" || error.code === "ESRCH") return null;
2463
+ throw error;
2464
+ }
2465
+ }
2466
+ function ttyDevice(anchor) {
2467
+ if (!same(anchor)) throw new Error("pty_session_identity_lost");
2468
+ const tty = stat(anchor.pid).tty;
2469
+ if (!tty || !same(anchor)) throw new Error("pty_terminal_identity_lost");
2470
+ return tty;
2471
+ }
2472
+ function childrenOf(pid) {
2473
+ const fd = fs.openSync("/proc/" + pid + "/task/" + pid + "/children", "r");
2474
+ try {
2475
+ const bytes = Buffer.alloc(262145), size = fs.readSync(fd, bytes, 0, bytes.length, null);
2476
+ if (size > 262144) throw new Error("pty_children_inventory_unconfirmed");
2477
+ const raw = bytes.subarray(0, size).toString("ascii");
2478
+ if (raw.length && !/^(?:[1-9][0-9]*\s+)*$/.test(raw)) throw new Error("pty_children_inventory_unconfirmed");
2479
+ const children = raw.trim() ? raw.trim().split(/\s+/).map(Number) : [];
2480
+ if (children.length > 32768 || new Set(children).size !== children.length) throw new Error("pty_children_inventory_unconfirmed");
2481
+ return children;
2482
+ } finally { fs.closeSync(fd); }
2483
+ }
2484
+ function session(anchor, device) {
2485
+ if (!same(anchor) || api.getsid(anchor.pid) !== anchor.pid || ttyDevice(anchor) !== device) throw new Error("pty_session_identity_lost");
2486
+ // Fresh open at position zero: the kernel checks the first direct child
2487
+ // under tasklist_lock. Nonempty seq walks can omit exiting children, so
2488
+ // they are ONLY a worklist. Only an empty fresh read, with the subreaper
2489
+ // still pinned and unable to spawn again, proves the tree is empty.
2490
+ const children = childrenOf(anchor.pid);
2491
+ const members = [];
2492
+ try {
2493
+ for (const pid of children) {
2494
+ const token = identity(pid);
2495
+ if (!token) continue;
2496
+ members.push(token);
2497
+ let parent;
2498
+ try { parent = stat(pid).parent; }
2499
+ catch (error) {
2500
+ if (error.code === "ENOENT" && !same(token)) continue;
2501
+ throw error;
2502
+ }
2503
+ if (!same(anchor)) throw new Error("pty_session_identity_lost");
2504
+ if (parent !== anchor.pid && same(token)) throw new Error("pty_child_ownership_changed");
2505
+ }
2506
+ if (!same(anchor)) throw new Error("pty_session_identity_lost");
2507
+ return { members, empty: children.length === 0 };
2508
+ } catch (error) { for (const token of members) fs.closeSync(token.fd); throw error; }
2509
+ }
2510
+ function signal(token, number) {
2511
+ if (api.pidfd_send_signal(token.fd, number, null, 0) !== 0 && errno() !== 3) throw new Error("pty_signal_unconfirmed");
2512
+ }
2513
+ function controlPair() {
2514
+ const descriptors = new Int32Array(2);
2515
+ if (api.socketpair(1, 1 | 0x80000, 0, ptr(descriptors)) !== 0) throw new Error("pty_control_unavailable"); // SOCK_CLOEXEC
2516
+ return [...descriptors];
2517
+ }
2518
+ function readControl(fd) {
2519
+ const bytes = Buffer.alloc(256), size = api.recv(fd, ptr(bytes), bytes.length, 0x40); // MSG_DONTWAIT
2520
+ if (size < 0) {
2521
+ if ([4, 11].includes(errno())) return "";
2522
+ throw new Error("pty_control_read_failed");
2523
+ }
2524
+ if (!size) throw new Error("pty_control_closed");
2525
+ return bytes.subarray(0, size).toString("ascii");
2526
+ }
2527
+ function releaseControl(fd) {
2528
+ const bytes = Buffer.from("release\n");
2529
+ if (api.send(fd, ptr(bytes), bytes.length, 0x40 | 0x4000) !== bytes.length) throw new Error("pty_control_release_failed"); // MSG_NOSIGNAL
2530
+ }
2531
+ // CONFIG_PROC_CHILDREN and pidfd signaling must work before admission.
2532
+ probeLinuxPtyLibrary({ identity: () => identity(process.pid), children: () => childrenOf(process.pid),
2533
+ signal: (probe) => signal(probe, 0), release: (probe) => fs.closeSync(probe.fd), close: () => library.close() });
2534
+ nativePtyOwner = { identity, same, session, ttyDevice, signal, controlPair, readControl, releaseControl,
2535
+ releaseIdentity: (token) => fs.closeSync(token.fd), identityKey: (token) => token.pid + ":" + token.birth,
2536
+ group: api.getpgid, sid: api.getsid };
2537
+ return nativePtyOwner;
2538
+ }
2539
+ // Persistent terminals (config.managedPty.persistent, Linux). Each terminal's
2540
+ // shell lives in its own tmux server, started detached in a private 0700
2541
+ // directory derived from this worker's managed-jobs scope (one Session on one
2542
+ // root), so it outlives this worker. A viewer PTY job runs only a tmux client
2543
+ // attached to a host: closing the viewer, releasing the Session's interaction
2544
+ // (last tab closed, idle, Disconnect) or this worker stopping ends that client
2545
+ // (detach), never the shell, and frees nothing but the client. The next worker
2546
+ // of the same scope (one at a time, as its journal ownership enforces) finds
2547
+ // those shells, so a reopened terminal reattaches through an ordinary pty.start
2548
+ // with that start's authority. A new viewer takes a host no live viewer holds,
2549
+ // so concurrent terminals keep separate shells. The directory comes from the
2550
+ // trusted config, never a payload, so only this Session's terminals reach its
2551
+ // sockets. A persisted shell ends when it exits (exit or Ctrl-D, a Device
2552
+ // terminal's explicit close) or 12 h after it started (a host's lease cap):
2553
+ // tmux then hangs up the shell, which hangs up its jobs. As on cloud session
2554
+ // terminals (same tmux settings, shared/session-terminals.ts), processes the
2555
+ // user detaches from the shell (nohup, setsid, disown, or jobs left at exit)
2556
+ // are not swept. At most TERMINAL_HOST_LIMIT shells per scope.
2557
+ const TERMINAL_SHELL = "/bin/bash --noprofile --norc -i";
2558
+ const TERMINAL_TMUX_CONFIG = ${JSON.stringify([...B,"new-session -d -s main /bin/bash --noprofile --norc -i","set -g exit-empty on","run-shell -b -d 43200 -C kill-server",""].join(`
2559
+ `))};
2560
+ const TERMINAL_HOST_LIMIT = 4;
2561
+ const terminalViewers = new Map();
2562
+ let terminalTmux;
2563
+ let terminalChoice = Promise.resolve();
2564
+ function terminalTmuxPath() {
2565
+ if (terminalTmux !== undefined) return terminalTmux;
2566
+ terminalTmux = null;
2567
+ if (process.platform !== "linux" || !config.managedPty || config.managedPty.persistent !== true || config.jobsUser) return null;
2568
+ for (const candidate of ["/usr/bin/tmux", "/usr/local/bin/tmux", "/bin/tmux"]) {
2569
+ // 3.2 added clients that never start a server (-N) and run-shell -d/-C.
2570
+ const version = spawnSync(candidate, ["-V"], { encoding: "utf8", timeout: 2000, env: { PATH: "/usr/bin:/bin" } });
2571
+ const match = /^tmux (?:next-)?([0-9]+)\.([0-9]+)/.exec(version.status === 0 ? version.stdout : "");
2572
+ if (match && Number(match[1]) * 100 + Number(match[2]) >= 302) { terminalTmux = candidate; break; }
2573
+ }
2574
+ return terminalTmux;
2575
+ }
2576
+ // PTY admission needs the program to lead its own foreground group, as the
2577
+ // interactive shell does. An interactive bash running a compound command
2578
+ // (never a lone exec, which skips job control) leads one and runs tmux as its job.
2579
+ function terminalCommand(command) { return TERMINAL_SHELL + ' -c "' + command + '; exit"'; }
2580
+ function terminalScopeDirectory() {
2581
+ // Terminals a later worker keeps under the Reason name are found there, when that is this
2582
+ // user's own private directory (never followed through a link); else /tmp/ara-terminals-*.
2583
+ const scope = digest(config.managedJobs.scopeId).slice(0, 16);
2584
+ const reasonDirectory = "/tmp/reason-terminals-" + scope;
2585
+ let ownReasonDirectory = false;
2586
+ try {
2587
+ const found = fs.lstatSync(reasonDirectory);
2588
+ ownReasonDirectory = found.isDirectory() && found.uid === process.getuid() && (found.mode & 0o077) === 0;
2589
+ } catch { /* none */ }
2590
+ const directory = ownReasonDirectory ? reasonDirectory : "/tmp/ara-terminals-" + scope;
2591
+ try { fs.mkdirSync(directory, { mode: 0o700 }); }
2592
+ catch (error) { if (!error || error.code !== "EEXIST") throw error; }
2593
+ const stat = fs.lstatSync(directory);
2594
+ if (!stat.isDirectory() || stat.uid !== process.getuid() || (stat.mode & 0o077) !== 0) throw new Error("terminal_directory_invalid");
2595
+ return directory;
2596
+ }
2597
+ function terminalHostServes(socketPath) {
2598
+ return new Promise((resolve) => {
2599
+ const child = spawn(terminalTmux, ["-S", socketPath, "-N", "has-session", "-t", "=main"],
2600
+ { stdio: "ignore", env: { PATH: "/usr/bin:/bin" } });
2601
+ const timer = setTimeout(() => { try { child.kill("SIGKILL"); } catch { /* exited */ } }, 2000);
2602
+ child.once("error", () => { clearTimeout(timer); resolve(false); });
2603
+ child.once("close", (code) => { clearTimeout(timer); resolve(code === 0); });
2604
+ });
2605
+ }
2606
+ /** Starts a detached tmux server with this Session's private environment. A
2607
+ * stale socket at the path is replaced by tmux itself. */
2608
+ function startTerminalHost(socketPath) {
2609
+ if (stopped || !/^[A-Za-z0-9/._-]+$/.test(socketPath)) return false;
2610
+ const configPath = socketPath + ".conf";
2611
+ try { fs.unlinkSync(configPath); } catch { /* absent */ }
2612
+ fs.writeFileSync(configPath, TERMINAL_TMUX_CONFIG, { mode: 0o600, flag: "wx" });
2613
+ const env = { ...environmentForSpawn().env, TERM: "xterm-256color", BASH_SILENCE_DEPRECATION_WARNING: "1" };
2614
+ return spawnSync(terminalTmux, ["-u", "-S", socketPath, "-f", configPath, "start-server"],
2615
+ { cwd: config.repoPath, env, stdio: "ignore", timeout: 5000 }).status === 0;
2616
+ }
2617
+ /** The program a new PTY job runs: a client attached to a free persistent
2618
+ * terminal, or the one-shot shell. release() frees the host once the job ends. */
2619
+ function terminalProgram() {
2620
+ // Serialized, so concurrent starts never pick the same free host.
2621
+ const choice = terminalChoice.then(chooseTerminalProgram);
2622
+ terminalChoice = choice.catch(() => {});
2623
+ return choice;
2624
+ }
2625
+ async function chooseTerminalProgram() {
2626
+ // A Mac terminal runs the user's login shell, as their own terminal would.
2627
+ const shell = config.managedPty && config.managedPty.shell;
2628
+ const oneShot = { program: shell ? shell + " -i -l" : TERMINAL_SHELL, release: () => {} };
2629
+ if (stopped || !terminalTmuxPath()) return oneShot;
2630
+ let directory;
2631
+ try { directory = terminalScopeDirectory(); } catch { return oneShot; }
2632
+ const numbers = fs.readdirSync(directory).filter((name) => /^[1-9][0-9]{0,8}$/.test(name)).map(Number).sort((a, b) => a - b);
2633
+ let socketPath = null, live = 0, unused = null;
2634
+ for (const number of numbers) {
2635
+ const candidate = path.join(directory, String(number));
2636
+ if (await terminalHostServes(candidate)) {
2637
+ live += 1;
2638
+ if (!socketPath && !terminalViewers.get(candidate)) socketPath = candidate;
2639
+ } else if (unused === null && !terminalViewers.get(candidate)) unused = candidate;
2640
+ }
2641
+ if (!socketPath) {
2642
+ if (live >= TERMINAL_HOST_LIMIT) return oneShot;
2643
+ let next = 1;
2644
+ while (numbers.includes(next)) next += 1;
2645
+ socketPath = unused || path.join(directory, String(next));
2646
+ try { if (!startTerminalHost(socketPath)) return oneShot; } catch { return oneShot; }
2647
+ }
2648
+ terminalViewers.set(socketPath, (terminalViewers.get(socketPath) || 0) + 1);
2649
+ let released = false;
2650
+ const release = () => {
2651
+ if (released) return;
2652
+ released = true;
2653
+ const viewers = terminalViewers.get(socketPath) - 1;
2654
+ if (viewers > 0) terminalViewers.set(socketPath, viewers); else terminalViewers.delete(socketPath);
2655
+ };
2656
+ for (const deadline = Date.now() + 3000; !stopped && Date.now() < deadline;) {
2657
+ if (await terminalHostServes(socketPath)) {
2658
+ return { program: terminalCommand(terminalTmux + " -u -S " + socketPath + " -N attach-session -t =main"), release };
2659
+ }
2660
+ await new Promise((resolve) => setTimeout(resolve, 25));
2661
+ }
2662
+ release();
2663
+ return oneShot;
2664
+ }
2665
+ function ptyOperation(request, managed, program = TERMINAL_SHELL) {
2666
+ const native = platformPtyOwner();
2667
+ let child = null, anchor = null, terminal = null, childEnvironment = null, device = null;
2668
+ let done = false, cancelled = false, timedOut = false, cleanup = null, exitCode = null;
2669
+ let timer = null, poll = null, metadata = "", shellPid = null, admitted = false;
2670
+ let controlFd = null;
2671
+ let controlReleased = false;
2672
+ let subreaper = process.platform !== "linux";
2673
+ const spawnGeneration = targetGeneration, spawnFence = targetFenceId;
2674
+ let resolveResult, acknowledgeSpawn;
2675
+ const promise = new Promise((resolve) => { resolveResult = resolve; });
2676
+ const started = new Promise((resolve) => { acknowledgeSpawn = resolve; });
2677
+ const finish = (error, code) => {
2678
+ if (done) return;
2679
+ done = true; acknowledgeSpawn(false);
2680
+ clearTimeout(timer); clearInterval(poll);
2681
+ let closeFailed = false;
2682
+ try { if (terminal) terminal.close(); } catch { closeFailed = true; }
2683
+ try { if (controlFd !== null) fs.closeSync(controlFd); } catch { closeFailed = true; }
2684
+ try { if (anchor) native.releaseIdentity?.(anchor); } catch { closeFailed = true; }
2685
+ controlFd = null;
2686
+ if (closeFailed) { error = "process_group_cleanup_unconfirmed"; code = 1; }
2687
+ resolveResult(error ? failure(request.id, code, error, "", "") : success(request.id, "", ""));
2688
+ };
2689
+ const delay = () => new Promise((resolve) => setTimeout(resolve, 20));
2690
+ function stopSession() {
2691
+ if (cleanup) return cleanup;
2692
+ cleanup = Promise.resolve().then(async () => {
2693
+ if (!child) { finish(timedOut ? "timeout" : "cancelled", timedOut ? 124 : 130); return; }
2694
+ try {
2695
+ if (!anchor || child.exitCode !== null) throw new Error("pty_session_identity_lost");
2696
+ const startedAt = Date.now(), termSent = new Set();
2697
+ for (;;) {
2698
+ const { members, empty } = native.session(anchor, device);
2699
+ try {
2700
+ if (empty && exitCode !== null && subreaper) break;
2701
+ if (Date.now() - startedAt > ${C}) throw new Error("pty_cleanup_timeout");
2702
+ for (const token of members) {
2703
+ if (!native.same(anchor)) throw new Error("pty_session_identity_lost");
2704
+ const id = native.identityKey(token);
2705
+ if (Date.now() - startedAt >= 500) native.signal(token, 9);
2706
+ else if (!termSent.has(id)) { native.signal(token, 15); termSent.add(id); }
2707
+ }
2708
+ } finally { for (const token of members) native.releaseIdentity?.(token); }
2709
+ await delay();
2710
+ }
2711
+ // The fixed bootstrap cannot spawn again after its interactive shell
2712
+ // exits. Release only after the still-owned session has no other tasks.
2713
+ native.releaseControl(controlFd);
2714
+ controlReleased = true;
2715
+ let exitTimer;
2716
+ try { await Promise.race([child.exited, new Promise((_, reject) => {
2717
+ exitTimer = setTimeout(() => reject(new Error("pty_anchor_exit_unconfirmed")), ${I});
2718
+ })]); } finally { clearTimeout(exitTimer); }
2719
+ if (timedOut) finish("timeout", 124);
2720
+ else if (cancelled) finish("cancelled", 130);
2721
+ else finish(exitCode === 0 ? null : "command_failed", exitCode ?? 1);
2722
+ } catch {
2723
+ // Do not signal a reconstructed PID/session after losing the anchor.
2724
+ finish("process_group_cleanup_unconfirmed", 1);
2725
+ }
2726
+ });
2727
+ return cleanup;
2728
+ }
2729
+ function arm(deadline) {
2730
+ clearTimeout(timer);
2731
+ timer = setTimeout(() => { timedOut = true; void stopSession(); }, Math.max(1, deadline - Date.now()));
2732
+ }
2733
+ // Each accepted input is handed to Bun exactly once. Terminal.write() keeps
2734
+ // whatever the PTY cannot take now in Bun's own ordered queue and writes it as
2735
+ // the PTY drains; its return value counts only bytes written synchronously, so
2736
+ // writing the "rest" again duplicates input. Bun 1.3.14 never calls the
2737
+ // Terminal "drain" callback (Terminal.zig wires "onWritable", but
2738
+ // PosixStreamingWriter reads "function_table.onReady") and exposes no queue
2739
+ // size, so that queue cannot be bounded here; each input stays capped by the
2740
+ // protocol's per-request size and the bridge sends one at a time.
2741
+ const canInput = () => done || cleanup || !admitted || !terminal || terminal.closed ? "job_not_running" : null;
2742
+ try {
2743
+ childEnvironment = environmentForSpawn();
2744
+ if (stopped || spawnGeneration !== targetGeneration || spawnFence !== targetFenceId
2745
+ || !validJobLease(managed.leaseExpiresAtMs)) throw new Error("pty_spawn_expired");
2746
+ // A fixed non-login bootstrap pins the session identity. Private fds are
2747
+ // closed before the interactive shell; no workspace startup/env file runs.
2748
+ const bootstrap = 'trap ":" HUP TERM; '
2749
+ + '/bin/bash --noprofile --norc -c \'printf "ready:%s\\n" "$$" >&3; exec 3>&-; exec ' + [...jobsUserPrefix(false), program].join(" ") + '\'; '
2750
+ + 'ara_pty_exit=$?; printf "exit:%s\\n" "$ara_pty_exit" >&3; '
2751
+ + 'while IFS= read -r ara_pty_control <&3; do [ "$ara_pty_control" = release ] && break; done; exit "$ara_pty_exit"';
2752
+ const pair = native.controlPair(); controlFd = pair[0];
2753
+ try {
2754
+ const command = process.platform === "linux"
2755
+ ? [process.execPath, "--no-env-file", "--config=/dev/null", "-e", LINUX_PTY_EXEC_SOURCE + "(" + JSON.stringify(bootstrap) + ")"]
2756
+ : [FIXED_BASH, "--noprofile", "--norc", "-c", bootstrap];
2757
+ const env = { ...childEnvironment.env, TERM: "xterm-256color", BASH_SILENCE_DEPRECATION_WARNING: "1" };
2758
+ if (process.platform === "linux") env[["BUN", "BE", "BUN"].join("_")] = "1";
2759
+ if (process.platform === "darwin") {
2760
+ if (config.managedPty.shell) env.SHELL = config.managedPty.shell;
2761
+ // Without a locale, a Mac shell's line editor reads typed UTF-8 as
2762
+ // meta keys and drops it.
2763
+ if (!env.LANG && !env.LC_ALL) env.LANG = "en_US.UTF-8";
2764
+ }
2765
+ child = Bun.spawn({ cmd: command, cwd: config.repoPath, env,
2766
+ stdio: ["ignore", "ignore", "ignore", pair[1]],
2767
+ terminal: { cols: request.input.cols, rows: request.input.rows, data: (_terminal, bytes) => {
2768
+ if (!done) managed.onBytes(bytes);
2769
+ } },
2770
+ });
2771
+ } finally { fs.closeSync(pair[1]); }
2772
+ terminal = child.terminal;
2773
+ anchor = native.identity(child.pid);
2774
+ if (!terminal || !anchor || native.sid(child.pid) !== child.pid || native.group(child.pid) !== child.pid) throw new Error("pty_spawn_identity_unconfirmed");
2775
+ device = native.ttyDevice(anchor);
2776
+ consumedEnvironment = { revision: childEnvironment.snapshot.revision, hash: childEnvironment.hash };
2777
+ managed.onEnvironment(childEnvironment.snapshot);
2778
+ arm(managed.leaseExpiresAtMs);
2779
+ poll = setInterval(() => {
2780
+ if (done || controlReleased) return;
2781
+ try {
2782
+ metadata += native.readControl(controlFd);
2783
+ if (metadata.length > 512) throw new Error("pty_metadata_invalid");
2784
+ for (;;) {
2785
+ const end = metadata.indexOf("\n");
2786
+ if (end < 0) break;
2787
+ const line = metadata.slice(0, end); metadata = metadata.slice(end + 1);
2788
+ if (line === "subreaper" && !subreaper && shellPid === null) subreaper = true;
2789
+ else if (/^ready:[1-9][0-9]*$/.test(line) && shellPid === null) shellPid = Number(line.slice(6));
2790
+ else if (/^exit:[0-9]{1,3}$/.test(line) && Number(line.slice(5)) <= 255) {
2791
+ exitCode = Number(line.slice(5)); void stopSession();
2792
+ } else throw new Error("pty_metadata_invalid");
2793
+ }
2794
+ if (!admitted && !cleanup && subreaper && shellPid && native.same(anchor) && native.sid(shellPid) === child.pid && native.group(shellPid) === shellPid) {
2795
+ admitted = true; acknowledgeSpawn(true);
2796
+ }
2797
+ } catch { cancelled = true; void stopSession(); }
2798
+ }, 10);
2799
+ void child.exited.then(() => { if (!done && !cleanup) void stopSession(); });
2800
+ } catch {
2801
+ if (child) { cancelled = true; void stopSession(); }
2802
+ else finish("spawn_failed", 1);
2803
+ }
2804
+ return { promise, started, supportsCancellation: true, canInput,
2805
+ settle: stopSession,
2806
+ processIdentity() {
2807
+ return admitted && !done && child.exitCode === null && native.same(anchor)
2808
+ ? { pid: child.pid, processGroupId: child.pid, executorLaunchId: config.launchId } : null;
2809
+ },
2810
+ input(data) {
2811
+ const error = canInput(); if (error) return error;
2812
+ try { terminal.write(data); return null; }
2813
+ catch { cancelled = true; void stopSession(); return "job_not_running"; }
2814
+ },
2815
+ resize(cols, rows) {
2816
+ const error = canInput(); if (error) return error;
2817
+ try { terminal.resize(cols, rows); return null; }
2818
+ catch { cancelled = true; void stopSession(); return "job_not_running"; }
2819
+ },
2820
+ renew(deadline) { if (done || cleanup) return false; arm(deadline); return true; },
2821
+ cancel() { cancelled = true; return stopSession(); },
2822
+ };
2823
+ }
2824
+ function bashOperation(request, onProgress, managed, piShell) {
2825
+ // Only a claimed retainBackground request; never managed jobs. Pi's shell
2826
+ // retains its background processes by stock Pi's rule (settle below).
2827
+ const retainBackground = !managed && !piShell && request.input.retainBackground === true;
2828
+ // Only a claimed machineSdk request: reason-sdk lives exactly as long as it.
2829
+ const machineSdk = !managed && !piShell && request.input.machineSdk === true;
2830
+ let sdkEndpoint = null;
2831
+ let child = null;
2832
+ let groupCleanup = null;
2833
+ let timedOut = false;
2834
+ let cancelled = false;
2835
+ let done = false;
2836
+ let timer = null;
2837
+ const preparationAbort = new AbortController();
2838
+ let preparation = null;
2839
+ let childEnvironment = null;
2840
+ const spawnGeneration = targetGeneration;
2841
+ const spawnFence = targetFenceId;
2842
+ // Pi's shell has no default timeout (stock Pi's has none): it runs until the
2843
+ // model's own timeout, a cancel or the Hands lease.
2844
+ const initialDeadline = piShell ? config.expiresAtMs
2845
+ : Math.min(config.expiresAtMs, managed ? managed.leaseExpiresAtMs : Date.now() + config.bashTimeoutMs);
2846
+ let acknowledgeSpawn;
2847
+ const started = new Promise((resolve) => { acknowledgeSpawn = resolve; });
2848
+ const canInput = (data) => {
2849
+ if (done || cancelled || timedOut || !child || !child.stdin || child.stdin.destroyed || child.stdin.writableEnded) return "job_not_running";
2850
+ if (child.stdin.writableLength + Buffer.byteLength(data) > 65536) return "job_input_backpressure";
2851
+ return null;
2852
+ };
2853
+ const startGroupCleanup = () => {
2854
+ if (!child) return Promise.resolve();
2855
+ if (!groupCleanup) {
2856
+ groupCleanup = killGroup(child);
2857
+ // Keep the rejection observed even if the process has not closed yet;
2858
+ // callers still await the original promise and fail closed on it.
2859
+ void groupCleanup.catch(() => {});
2860
+ }
2861
+ return groupCleanup;
2862
+ };
2863
+ let progressTimer = null;
2864
+ let progressSequence = 0;
2865
+ let keepaliveTimer = null;
2866
+ const promise = new Promise((resolve) => {
2867
+ const stdoutDecoder = new StringDecoder("utf8");
2868
+ const stderrDecoder = new StringDecoder("utf8");
2869
+ let stdout = "";
2870
+ let stderr = "";
2871
+ const finish = (result) => {
2872
+ if (done) return;
2873
+ done = true;
2874
+ if (sdkEndpoint) sdkEndpoint.close();
2875
+ if (result.error === "process_group_cleanup_unconfirmed") cleanupUnconfirmed = true;
2876
+ acknowledgeSpawn(false);
2877
+ clearTimeout(timer);
2878
+ clearTimeout(progressTimer);
2879
+ clearInterval(keepaliveTimer);
2880
+ if (preparation) {
2881
+ if (result.error === "process_group_cleanup_unconfirmed") preparation.abandon();
2882
+ else preparation.release();
2883
+ }
2884
+ resolve(result);
2885
+ };
2886
+ const publishProgress = () => {
2887
+ progressTimer = null;
2888
+ if (done || (stdout.length === 0 && stderr.length === 0)) return;
2889
+ progressSequence += 1;
2890
+ if (onProgress) onProgress(stdout, stderr, progressSequence);
2891
+ };
2892
+ const scheduleProgress = () => {
2893
+ if (done || progressTimer) return;
2894
+ progressTimer = setTimeout(publishProgress, 50);
2895
+ };
2896
+ timer = setTimeout(() => {
2897
+ timedOut = true;
2898
+ preparationAbort.abort();
2899
+ if (child) void startGroupCleanup().catch(() => {});
2900
+ else finish(failure(request.id, 124, "timeout", "", ""));
2901
+ }, Math.max(1, initialDeadline - Date.now()));
2902
+ const launch = async () => {
2903
+ if (!validPrivateJob && config.environmentSnapshotPath === undefined && !refreshJobEnvSnapshot()) {
2904
+ finish(failure(request.id, 1, "spawn_failed", "", "job_env_refresh_failed"));
2905
+ return;
2906
+ }
2907
+ try {
2908
+ if (machineSdk) {
2909
+ try {
2910
+ sdkEndpoint = machineSdkEndpoint(request.id);
2911
+ await sdkEndpoint.ready;
2912
+ } catch {
2913
+ // Nothing ran: reason-sdk could not be set up on this machine.
2914
+ finish(failure(request.id, 1, "spawn_failed", "", "machine_sdk_unavailable: reason-sdk could not be set up on this machine; nothing ran."));
2915
+ return;
2916
+ }
2917
+ if (done) { sdkEndpoint.close(); return; }
2918
+ }
2919
+ let command = [
2920
+ ...(config.bashCommandPrefix || []),
2921
+ FIXED_BASH,
2922
+ "--noprofile",
2923
+ "--norc",
2924
+ "-c",
2925
+ 'set -euo pipefail; '
2926
+ + (config.environmentSnapshotPath === undefined ? 'set -a; . "$1"; set +a; ' : '')
2927
+ // The wrapper starts with no startup file; Pi's shell then has none set, as stock Pi's.
2928
+ + (piShell ? 'unset BASH_ENV ENV; ' : 'export BASH_ENV=/dev/null ENV=/dev/null; ')
2929
+ + 'unset NODE_OPTIONS NODE_PATH LD_PRELOAD LD_LIBRARY_PATH; '
2930
+ + machineMemoryPrelude()
2931
+ + (sdkEndpoint ? sdkEndpoint.prelude : '')
2932
+ + (managed
2933
+ ? 'IFS= read -r -d "" ara_job_script; eval "$ara_job_script"'
2934
+ // Pi runs bash -c with no stdin script and no errexit.
2935
+ : piShell ? 'exec /bin/bash --noprofile --norc -c "$2"'
2936
+ : 'exec /bin/bash --noprofile --norc -se'),
2937
+ "h1-bash",
2938
+ config.environmentSnapshotPath === undefined ? config.jobEnvPath : "/dev/null",
2939
+ ...(piShell ? [request.input.script] : []),
2940
+ ];
2941
+ if (validPrivateJob) command = [...(config.bashCommandPrefix || []), config.directShell.executable,
2942
+ config.directShell.mode === "host" ? "-lic" : "-c",
2943
+ // Interactive login configuration is retained, but foreground commands
2944
+ // must stay in the owned process group instead of shell job-control groups.
2945
+ config.directShell.mode === "host" ? "set +m; " + request.input.script : request.input.script];
2946
+ // Resident Hands a run took over report ready at claim_ack, while their
2947
+ // environment for this generation is still being installed: wait for it,
2948
+ // bounded, instead of refusing; the wait is reported (environmentWaitMs).
2949
+ // Before the spawn preparation and the currency check below, which then
2950
+ // re-prove cancellation, generation, fence and deadline after the wait.
2951
+ if (config.environmentSnapshotPath !== undefined && environmentWaitNegotiated) {
2952
+ const waitedMs = await waitForSpawnEnvironment(Boolean(piShell), Boolean(piShell && piShell.confined));
2953
+ if (waitedMs > 0) environmentWaits.set(request.id, waitedMs);
2954
+ }
2955
+ if (config.spawnPolicy) {
2956
+ preparation = await prepareBashSpawn(command, 0, initialDeadline, preparationAbort.signal, () => {
2957
+ cancelled = true;
2958
+ void startGroupCleanup().catch(() => {});
2959
+ });
2960
+ command = preparation.argv;
2961
+ }
2962
+ if (done || cancelled || timedOut || stopped || spawnGeneration !== targetGeneration || spawnFence !== targetFenceId
2963
+ || Date.now() >= initialDeadline || (preparation && !preparation.current())) {
2964
+ if (preparation) preparation.release();
2965
+ finish(failure(request.id, 130, "cancelled", "", ""));
2966
+ return;
2967
+ }
2968
+ // This is deliberately after policy RPC awaits: a missing/revoked/stale
2969
+ // snapshot cannot authorize a later asynchronous child spawn.
2970
+ if (config.environmentSnapshotPath !== undefined) childEnvironment = environmentForSpawn(Boolean(piShell), Boolean(piShell && piShell.confined));
2971
+ if (childEnvironment?.confinementPrefix) command = [...childEnvironment.confinementPrefix, ...command];
2972
+ if (managed && managed.asJobsUser) command = [...jobsUserPrefix(true), ...command];
2973
+ if (preparation && childEnvironment && preparation.environmentSnapshotSha256 !== childEnvironment.fileSha256) throw new Error("environment_preparation_changed");
2974
+ if (Date.now() >= initialDeadline || (preparation && !preparation.current())) throw new Error("spawn_lease_expired");
2975
+ child = spawn(
2976
+ command[0], command.slice(1),
2977
+ {
2978
+ cwd: config.repoPath,
2979
+ detached: true,
2980
+ env: validPrivateJob ? process.env : childEnvironment ? childEnvironment.env : {
2981
+ ...process.env,
2982
+ // A physical Device keeps the CLI-resolved host PATH (including
2983
+ // Homebrew). Cloud Hands accepts only immutable image toolchains.
2984
+ PATH: config.jobEnvSourcePath === undefined
2985
+ || (process.platform === "darwin" && config.workspacePoolPath === path.dirname(config.workRoot)
2986
+ && config.repoPath === path.join(config.workRoot, "repo"))
2987
+ ? process.env.PATH
2988
+ : selectCloudChildPath(process.platform, FIXED_CLOUD_CHILD_PATH, FIXED_MACOS_CLOUD_CHILD_PATH),
2989
+ BASH_ENV: "/dev/null",
2990
+ ENV: "/dev/null",
2991
+ NODE_OPTIONS: "",
2992
+ NODE_PATH: "",
2993
+ LD_PRELOAD: "",
2994
+ LD_LIBRARY_PATH: "",
2995
+ },
2996
+ // Pi's shell reads /dev/null, as stock Pi's, so rg PATTERN searches the cwd.
2997
+ // A confined Pi file write passes its content on stdin.
2998
+ stdio: [piShell && piShell.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
2999
+ },
3000
+ );
3001
+ // A new leader with this PID proves the retained group of that ID ended.
3002
+ if (Number.isSafeInteger(child.pid)) backgroundGroups.delete(child.pid);
3003
+ } catch (error) {
3004
+ // The protocol error stays spawn_failed; a coded refusal such as
3005
+ // local_device_gh_confinement_unavailable or environment_unavailable is
3006
+ // its stderr, so the server sees why nothing ran. Uncoded error text
3007
+ // (paths, OS messages) is never sent.
3008
+ const code = !timedOut && !cancelled && error instanceof Error && /^[a-z][a-z0-9_]{2,79}$/.test(error.message) ? error.message : "";
3009
+ finish(failure(request.id, timedOut ? 124 : cancelled ? 130 : 1, timedOut ? "timeout" : cancelled ? "cancelled" : "spawn_failed", "", code));
3010
+ return;
3011
+ }
3012
+ // Pi's shell settles as stock Pi's: once its stdio ends after the shell
3013
+ // exits, or its output has been idle for 100 ms after the exit. Processes
3014
+ // it started in the background keep running, retained below.
3015
+ let settled = false;
3016
+ let exited = null;
3017
+ let idleTimer = null;
3018
+ const armIdle = () => {
3019
+ if (!exited || settled) return;
3020
+ clearTimeout(idleTimer);
3021
+ idleTimer = setTimeout(() => {
3022
+ child.stdout.destroy();
3023
+ child.stderr.destroy();
3024
+ void settle(exited.code, exited.signal);
3025
+ }, PI_EXIT_STDIO_GRACE_MS);
3026
+ };
3027
+ child.stdout.on("data", (chunk) => {
3028
+ if (piShell) { piShell.onData(chunk); armIdle(); }
3029
+ const text = stdoutDecoder.write(chunk);
3030
+ if (managed) managed.onOutput("stdout", text);
3031
+ stdout = clip(stdout + text, MAX_OUTPUT_CHARS);
3032
+ scheduleProgress();
3033
+ });
3034
+ child.stderr.on("data", (chunk) => {
3035
+ if (piShell) { (piShell.onStderr || piShell.onData)(chunk); armIdle(); }
3036
+ const text = stderrDecoder.write(chunk);
3037
+ if (managed) managed.onOutput("stderr", text);
3038
+ stderr = clip(stderr + text, MAX_OUTPUT_CHARS);
3039
+ scheduleProgress();
3040
+ });
3041
+ child.once("error", () => finish(failure(request.id, 1, "spawn_failed", stdout, stderr)));
3042
+ child.once("spawn", () => {
3043
+ if (childEnvironment) {
3044
+ consumedEnvironment = { revision: childEnvironment.snapshot.revision, hash: childEnvironment.hash };
3045
+ try { if (managed) managed.onEnvironment(childEnvironment.snapshot); }
3046
+ catch { cancelled = true; void startGroupCleanup().catch(() => {}); }
3047
+ }
3048
+ acknowledgeSpawn(true);
3049
+ if (onProgress) {
3050
+ keepaliveTimer = setInterval(() => {
3051
+ if (done) { clearInterval(keepaliveTimer); return; }
3052
+ publishProgress();
3053
+ }, 15_000);
3054
+ }
3055
+ });
3056
+ // A leader exiting must not leave descendants holding pipes open until the
3057
+ // deadline. This uses only the live owned group, never recovered PID data.
3058
+ // A retained command's descendants are deliberately left running; any that
3059
+ // still hold its stdio keep the result open until they exit or time out.
3060
+ child.once("exit", (code, signal) => {
3061
+ if (piShell) { exited = { code, signal }; armIdle(); }
3062
+ else if (!retainBackground) void startGroupCleanup().catch(() => {});
3063
+ });
3064
+ const settle = async (code, signal) => {
3065
+ if (settled) return;
3066
+ settled = true;
3067
+ clearTimeout(idleTimer);
3068
+ try {
3069
+ // A command that deliberately closes inherited stdio can leave a
3070
+ // background descendant after its group leader exits. No Bash result
3071
+ // is terminal until the entire detached process group is absent, or,
3072
+ // for a retained command or Pi's shell, tracked until it is.
3073
+ if ((retainBackground || piShell) && !groupCleanup && !timedOut && !cancelled) retainBackgroundGroup(child);
3074
+ else await startGroupCleanup();
3075
+ } catch {
3076
+ finish(failure(request.id, 1, "process_group_cleanup_unconfirmed", stdout, stderr));
3077
+ return;
3078
+ }
3079
+ const stdoutTail = stdoutDecoder.end();
3080
+ const stderrTail = stderrDecoder.end();
3081
+ if (managed) {
3082
+ managed.onOutput("stdout", stdoutTail);
3083
+ managed.onOutput("stderr", stderrTail);
3084
+ }
3085
+ stdout = clip(stdout + stdoutTail, MAX_OUTPUT_CHARS);
3086
+ stderr = clip(stderr + stderrTail, MAX_OUTPUT_CHARS);
3087
+ if (timedOut) finish(failure(request.id, 124, "timeout", stdout, stderr));
3088
+ else if (cancelled) finish(failure(request.id, 130, "cancelled", stdout, stderr));
3089
+ else if (code === 0) finish(success(request.id, stdout, stderr));
3090
+ else finish(failure(request.id, Number.isSafeInteger(code) ? code
3091
+ : piShell && signal ? 128 + (require("node:os").constants.signals[signal] || 0) : 1, "command_failed", stdout, stderr));
3092
+ };
3093
+ child.once("close", (code, signal) => { void settle(code, signal); });
3094
+ if (piShell) {
3095
+ if (piShell.stdin !== undefined) { child.stdin.on("error", () => {}); child.stdin.end(piShell.stdin); }
3096
+ return;
3097
+ }
3098
+ child.stdin.on("error", () => {});
3099
+ if (managed && !validPrivateJob) {
3100
+ // One ordered pipe carries a complete script frame followed by job input.
3101
+ // EOF before its delimiter fails closed instead of evaluating a prefix.
3102
+ child.stdin.write(request.input.script + "\0");
3103
+ } else child.stdin.end(validPrivateJob ? undefined : request.input.script);
3104
+ };
3105
+ void launch().catch(() => finish(failure(request.id, 1, "spawn_failed", "", "")));
3106
+ });
3107
+ return {
3108
+ promise,
3109
+ started,
3110
+ supportsCancellation: true,
3111
+ canInput,
3112
+ processIdentity() {
3113
+ if (done || !child || !Number.isSafeInteger(child.pid) || child.exitCode !== null || child.signalCode !== null) return null;
3114
+ return { pid: child.pid, processGroupId: child.pid, executorLaunchId: config.launchId };
3115
+ },
3116
+ input(data, eof) {
3117
+ const error = canInput(data);
3118
+ if (error) return error;
3119
+ if (eof) child.stdin.end(data);
3120
+ else child.stdin.write(data);
3121
+ return null;
3122
+ },
3123
+ renew(expiresAtMs) {
3124
+ if (done || cancelled || timedOut) return false;
3125
+ clearTimeout(timer);
3126
+ timer = setTimeout(() => { timedOut = true; void startGroupCleanup().catch(() => {}); }, Math.max(1, expiresAtMs - Date.now()));
3127
+ return true;
3128
+ },
3129
+ cancel() {
3130
+ cancelled = true;
3131
+ preparationAbort.abort();
3132
+ return startGroupCleanup();
3133
+ },
3134
+ };
3135
+ }
3136
+ function replOperation(request) {
3137
+ let client = null;
3138
+ let timedOut = false;
3139
+ let hardCancelled = false;
3140
+ let done = false;
3141
+ let timer = null;
3142
+ const promise = new Promise((resolve) => {
3143
+ let raw = "";
3144
+ const finish = (result) => {
3145
+ if (done) return;
3146
+ done = true;
3147
+ clearTimeout(timer);
3148
+ resolve(result);
3149
+ };
3150
+ client = net.createConnection(config.replSocketPath);
3151
+ client.setEncoding("utf8");
3152
+ client.once("connect", () => {
3153
+ client.write(JSON.stringify({ action: "evaluate", code: request.input.code, includeProgressCapture: true,
3154
+ ...(config.browserRunId ? { runId: config.browserRunId } : {}) }) + "\n");
3155
+ });
3156
+ client.on("data", (chunk) => {
3157
+ raw += chunk;
3158
+ if (raw.length > MAX_FRAME_BYTES) {
3159
+ client.destroy();
3160
+ finish(failure(request.id, 1, "repl_response_too_large", "", ""));
3161
+ }
3162
+ });
3163
+ client.once("error", () => {
3164
+ finish(failure(
3165
+ request.id,
3166
+ timedOut ? 124 : hardCancelled ? 130 : 1,
3167
+ timedOut ? "timeout" : hardCancelled ? "cancelled" : "repl_transport_failed",
3168
+ "",
3169
+ "",
3170
+ ));
3171
+ });
3172
+ client.once("end", () => {
3173
+ if (hardCancelled) {
3174
+ finish(failure(request.id, 130, "cancelled", "", ""));
3175
+ return;
3176
+ }
3177
+ let response;
3178
+ try { response = JSON.parse(raw); } catch {
3179
+ finish(failure(request.id, 1, "invalid_repl_response", "", "Invalid REPL response"));
3180
+ return;
3181
+ }
3182
+ if (response && response.ok === true && typeof response.output === "string") {
3183
+ finish({ ...success(request.id, response.output, ""), browserActions: response.browserActions,
3184
+ ...(response.automaticProgressImage === true ? { automaticProgressImage: true } : {}) });
3185
+ return;
3186
+ }
3187
+ const stderr = response && typeof response.stack === "string"
3188
+ ? response.stack
3189
+ : response && typeof response.error === "string" ? response.error : "REPL request failed";
3190
+ finish({ ...failure(request.id, 1, "repl_failed", "", stderr), browserActions: response && response.browserActions });
3191
+ });
3192
+ client.once("close", () => {
3193
+ if (!done) finish(failure(
3194
+ request.id,
3195
+ hardCancelled ? 130 : 1,
3196
+ hardCancelled ? "cancelled" : "repl_transport_failed",
3197
+ "",
3198
+ "",
3199
+ ));
3200
+ });
3201
+ timer = setTimeout(() => {
3202
+ timedOut = true;
3203
+ client.destroy();
3204
+ finish(failure(request.id, 124, "timeout", "", ""));
3205
+ }, config.replTimeoutMs);
3206
+ });
3207
+ return {
3208
+ promise,
3209
+ supportsCancellation: true,
3210
+ cancellationRequiresExternalProof: true,
3211
+ cancel(hard) {
3212
+ if (!hard) return;
3213
+ hardCancelled = true;
3214
+ if (client) client.destroy();
3215
+ },
3216
+ };
3217
+ }
3218
+ // Pi's own read/bash/edit/write (pi_tool), bundled unchanged. The 3 MB source
3219
+ // is not part of this worker: a host that serves pi_tool passes it as
3220
+ // araHandsPiToolsSource, and it is checked against the pinned digest and
3221
+ // evaluated on first use, so worker starts never parse it. read/edit/write use
3222
+ // Pi's local file operations in this process, which deliberately reach paths
3223
+ // outside the workspace; under local GitHub confinement they use
3224
+ // confinedPiFileOperations, the typed file tools' boundary, instead. pi_tool is
3225
+ // refused where a spawn policy or command prefix confines file access to a
3226
+ // child process this worker does not compose. Bash keeps Pi's
3227
+ // output handling and full-output file; its spawn operation is the ordinary
3228
+ // Hands Bash spawn above, in the fenced environment, with stock Pi's process
3229
+ // semantics: the host's filtered variables, BASH_ENV and ENV unset, a
3230
+ // private global Git config, stdin from /dev/null, no deadline but the model's
3231
+ // timeout and the lease, and background processes retained until the worker
3232
+ // stops. One departure from stock: a command idle on a watcher's banner is
3233
+ // stopped (PI_WATCH_BANNER). PI_* variables are not set here.
3234
+ const PI_TOOLS_SHA256 = "${p}";
3235
+ const piToolsSource = typeof araHandsPiToolsSource === "function" ? araHandsPiToolsSource : null;
3236
+ let piTools = null;
3237
+ function loadPiTools() {
3238
+ if (piTools) return piTools;
3239
+ const source = piToolsSource();
3240
+ if (digest(source) !== PI_TOOLS_SHA256) throw new Error("pi_tools_digest_mismatch");
3241
+ const module = { exports: {} };
3242
+ new Function("module", "exports", "require", '"use strict";\n' + source)(module, module.exports, require);
3243
+ piTools = module.exports;
3244
+ return piTools;
3245
+ }
3246
+ function piToolsAvailable() {
3247
+ return piToolsSource !== null && !validPrivateJob && !config.spawnPolicy && !config.bashCommandPrefix
3248
+ && ["bash", "read", "edit", "write"].every((tool) => allowedTools.includes(tool));
3249
+ }
3250
+ function validPiToolRequest(value) {
3251
+ const input = value.input;
3252
+ return piToolsAvailable() && value.semanticDigest === undefined && hasOnlyKeys(input, ["tool", "args"])
3253
+ && ["bash", "read", "edit", "write"].includes(input.tool)
3254
+ && Boolean(input.args) && typeof input.args === "object" && !Array.isArray(input.args)
3255
+ && (input.tool !== "bash" || (typeof input.args.command === "string" && input.args.command.length <= MAX_INPUT_CHARS));
3256
+ }
3257
+ const PI_MAX_TIMEOUT_SECONDS = 2147483647 / 1000;
3258
+ const PI_EXIT_STDIO_GRACE_MS = 100;
3259
+ // Run confinement (shared/pi-tool-confinement.ts): the Brain's template holds
3260
+ // the token once under quoteDepth layers of single quoting, so the confined
3261
+ // script is byte-identical to what the Brain's confineBashPayload gives the
3262
+ // bash tool: srt/bwrap, the payload uid, the network allowlist and allowWrite.
3263
+ const PI_TOOL_CONFINEMENT_TOKEN = "${Q}";
3264
+ function validPiToolConfinement(value) {
3265
+ return hasOnlyKeys(value, ["template", "quoteDepth"]) && typeof value.template === "string"
3266
+ && value.template.length <= ${J} && !value.template.includes("\0")
3267
+ && value.template.split(PI_TOOL_CONFINEMENT_TOKEN).length === 2 && [1, 2, 3].includes(value.quoteDepth);
3268
+ }
3269
+ // The script T when this command is exactly this policy's confineBashPayload(T)
3270
+ // (a Brain that wraps Pi's command itself): the middle is a well-formed escape
3271
+ // of T. Null for any other command.
3272
+ function piConfinedInnerScript(confinement, script) {
3273
+ const [before, after] = confinement.template.split(PI_TOOL_CONFINEMENT_TOKEN);
3274
+ if (script.length < before.length + after.length || !script.startsWith(before) || !script.endsWith(after)) return null;
3275
+ const middle = script.slice(before.length, script.length - after.length);
3276
+ let inner = middle;
3277
+ for (let layer = 0; layer < confinement.quoteDepth; layer += 1) inner = inner.split("'\\''").join("'");
3278
+ let again = inner;
3279
+ for (let layer = 0; layer < confinement.quoteDepth; layer += 1) again = again.replace(/'/g, "'\\''");
3280
+ return again === middle ? inner : null;
3281
+ }
3282
+ function piConfinedScript(confinement, script) {
3283
+ // Already confined by this exact policy: wrapping it again would nest srt.
3284
+ if (piConfinedInnerScript(confinement, script) !== null) return script;
3285
+ const [before, after] = confinement.template.split(PI_TOOL_CONFINEMENT_TOKEN);
3286
+ let escaped = script;
3287
+ for (let layer = 0; layer < confinement.quoteDepth; layer += 1) escaped = escaped.replace(/'/g, "'\\''");
3288
+ return before + escaped + after;
3289
+ }
3290
+ // What identifies a Brain-confined Pi Bash call in the request ledger: the
3291
+ // model's command and timeout, without the wrapper. The wrapper is rendered by
3292
+ // the dispatching Brain's build from the run's policy, so the Brain that takes
3293
+ // a run over at a deploy sends the same call id around a different wrapper. The
3294
+ // ledger then refused the replay as another request (duplicate_request_id) and
3295
+ // the model read a failure for a command that was still running (prod
3296
+ // 2026-10-10, Session 9d60d6ca: a sleep 3500 handed off across the deploy of a
3297
+ // confinement change). Null when the request is not such a call.
3298
+ function piConfinedBashCallInput(request, confinement) {
3299
+ if (request.tool !== "pi_tool" || request.input.tool !== "bash" || !confinement) return null;
3300
+ const command = piConfinedInnerScript(confinement, request.input.args.command);
3301
+ return command === null ? null : { tool: "bash", confinedArgs: { ...request.input.args, command } };
3302
+ }
3303
+ // Pi's read/edit/write under run confinement: every file access is a small
3304
+ // shell step run through the confined script as the payload, so srt's read
3305
+ // denials and allowWrite decide what Pi can see and change, as for model Bash.
3306
+ // The step's exit code names the failure; file bytes come back on stdout and
3307
+ // written content goes in on stdin, so neither passes through argv.
3308
+ const PI_CONFINED_FILE_ERRORS = { 71: ["ENOENT", "no such file or directory"], 72: ["EACCES", "permission denied"],
3309
+ 73: ["EISDIR", "illegal operation on a directory"], 75: ["EEXIST", "file already exists"] };
3310
+ function piConfinedFileError(result, syscall, file) {
3311
+ const known = result.error === "command_failed" ? PI_CONFINED_FILE_ERRORS[result.exitCode] : undefined;
3312
+ if (known) return Object.assign(new Error(known[0] + ": " + known[1] + ", " + syscall + " '" + file + "'"), { code: known[0], syscall, path: file });
3313
+ const detail = String(result.stderr || "").trim().slice(0, 2000);
3314
+ return Object.assign(new Error("confined " + syscall + " failed (" + (result.error || "exit " + result.exitCode) + ")" + (detail ? ": " + detail : "")), { code: "EIO" });
3315
+ }
3316
+ function piConfinedFileScript(step, file) {
3317
+ const quoted = shellQuote(file);
3318
+ if (step === "read") return "p=" + quoted + '; if [ ! -e "$p" ]; then exit 71; fi; if [ -d "$p" ]; then exit 73; fi; if [ ! -r "$p" ]; then exit 72; fi; exec cat -- "$p"';
3319
+ if (step === "access_rw") return "p=" + quoted + '; if [ ! -e "$p" ]; then exit 71; fi; if [ ! -r "$p" ] || [ ! -w "$p" ]; then exit 72; fi';
3320
+ if (step === "mkdir") return "p=" + quoted + '; if [ -e "$p" ] && [ ! -d "$p" ]; then exit 75; fi; mkdir -p -- "$p" || exit 72';
3321
+ return "p=" + quoted + '; if [ -d "$p" ]; then exit 73; fi; if [ ! -d "$(dirname -- "$p")" ]; then exit 71; fi; cat > "$p" || exit 72';
3322
+ }
3323
+ function piConfinedFileOperations(id, state, confinement, signal, writable) {
3324
+ const run = (step, file, stdin) => {
3325
+ const chunks = [];
3326
+ const errors = [];
3327
+ const operation = bashOperation({ id, input: { script: piConfinedScript(confinement, piConfinedFileScript(step, file)) } }, null, null,
3328
+ { onData: (chunk) => chunks.push(chunk), onStderr: (chunk) => errors.push(chunk), confined: true, ...(stdin === undefined ? {} : { stdin }) });
3329
+ const stop = () => { void operation.cancel().catch(() => {}); };
3330
+ if (signal.aborted) stop();
3331
+ else signal.addEventListener("abort", stop, { once: true });
3332
+ return operation.promise.then((result) => {
3333
+ signal.removeEventListener("abort", stop);
3334
+ if (result.error === "process_group_cleanup_unconfirmed") state.cleanupUnconfirmed = true;
3335
+ if (!result.ok) throw piConfinedFileError({ ...result, stderr: Buffer.concat(errors).toString("utf8") },
3336
+ step === "access_rw" ? "access" : step === "read" ? "open" : step === "mkdir" ? "mkdir" : "open", file);
3337
+ return Buffer.concat(chunks);
3338
+ });
3339
+ };
3340
+ const reads = new Map();
3341
+ const read = (file) => {
3342
+ if (!reads.has(file)) reads.set(file, run("read", file));
3343
+ return reads.get(file);
3344
+ };
3345
+ return {
3346
+ // Pi's read checks R_OK (the read itself answers it) and its edit R_OK|W_OK.
3347
+ access: (file) => (writable ? run("access_rw", file) : read(file)).then(() => undefined),
3348
+ readFile: read,
3349
+ detectImageMimeType: (file) => read(file).then((bytes) => loadPiTools().detectSupportedImageMimeType(bytes.subarray(0, 4100))),
3350
+ writeFile: (file, content) => run("write", file, Buffer.from(content, "utf8")).then(() => { reads.delete(file); }),
3351
+ mkdir: (dir) => run("mkdir", dir).then(() => undefined),
3352
+ };
3353
+ }
3354
+ // A watcher's banner ends the output of a command that will never exit: the
3355
+ // tool says it is waiting for file changes. stdin is /dev/null and the output
3356
+ // is a pipe, which stops prompts and pagers but not watchers: vitest before 2
3357
+ // watches unless CI is set, whatever its stdin (prod 2026-10-11: all 10 hung
3358
+ // test calls of a field study ended on vitest's banner and ran to the model's
3359
+ // 120 s timeout, the tests done in 0.5 to 73 s). CI is deliberately not set
3360
+ // for every command: under it pnpm install refuses a changed package.json,
3361
+ // jest and vitest refuse to write a new snapshot, and repository scripts
3362
+ // switch to their CI branches. Only text that itself states the wait counts:
3363
+ // silence or an idle CPU also describes sleep, a download and a remote build.
3364
+ // A dev server's ready line is not a banner (a command may start one, wait and
3365
+ // then use it). Output through a pipeline (| tail) never shows the banner here.
3366
+ const PI_WATCH_IDLE_MS = 10000;
3367
+ const PI_WATCH_BANNER = /(?:waiting|watching) for (?:file )?changes[^\n]*(?:\n[^\n]*press h to show help[^\n]*)?\s*$/i;
3368
+ const PI_VITEST_BANNER = /Waiting for file changes\.\.\.\s+press h to show help/;
3369
+ function piWatchBanner(tail) {
3370
+ const text = tail.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").replace(/\r/g, "\n");
3371
+ if (!PI_WATCH_BANNER.test(text)) return null;
3372
+ return PI_VITEST_BANNER.test(text) ? "vitest" : "watcher";
3373
+ }
3374
+ function piWatchNote(banner) {
3375
+ return "\n\n[Stopped by Reason after " + (PI_WATCH_IDLE_MS / 1000) + " seconds without output: the command finished its run and was waiting for file changes (watch mode), so it would never exit. Its results are above; exit code 124 is this stop, not the command's own. To run it once: "
3376
+ + (banner === "vitest"
3377
+ ? "prefix the same command with CI=1, or pass --run to vitest before any \"--\" (vitest run ...)."
3378
+ : "use its one-shot form (drop --watch or -w; jest: --watchAll=false; vitest: --run), or prefix the command with CI=1 where the tool honours it.")
3379
+ + " To keep a watcher running, start it in the background with its output in a file.]\n";
3380
+ }
3381
+ function piShellOperations(id, onProgress, state, confinement) {
3382
+ return { exec(command, _cwd, { onData: piOnData, signal, timeout }) {
3383
+ // Pi's local operation contract: reject invalid timeouts, report "aborted"
3384
+ // and "timeout:<seconds>", otherwise resolve the exit code.
3385
+ if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
3386
+ return Promise.reject(new Error("Invalid timeout: must be a finite number of seconds"));
3387
+ }
3388
+ if (timeout > PI_MAX_TIMEOUT_SECONDS) {
3389
+ return Promise.reject(new Error("Invalid timeout: maximum is " + PI_MAX_TIMEOUT_SECONDS + " seconds"));
3390
+ }
3391
+ if (signal.aborted) return Promise.reject(new Error("aborted"));
3392
+ const startedAt = Date.now();
3393
+ let timedOut = false;
3394
+ let watchTail = "";
3395
+ let watchTimer = null;
3396
+ let watchStopped = false;
3397
+ let stop = () => {};
3398
+ // Every chunk restarts the wait: the banner must be the last thing printed.
3399
+ const onData = (chunk) => {
3400
+ piOnData(chunk);
3401
+ clearTimeout(watchTimer);
3402
+ watchTail = (watchTail + chunk.toString("latin1")).slice(-2048);
3403
+ const banner = piWatchBanner(watchTail);
3404
+ if (!banner || timedOut || watchStopped || signal.aborted) return;
3405
+ watchTimer = setTimeout(() => {
3406
+ if (timedOut || signal.aborted) return;
3407
+ watchStopped = true;
3408
+ piOnData(Buffer.from(piWatchNote(banner), "utf8"));
3409
+ stop();
3410
+ }, PI_WATCH_IDLE_MS);
3411
+ };
3412
+ const shell = confinement
3413
+ ? bashOperation({ id, input: { script: piConfinedScript(confinement, command) } }, onProgress, null, { onData, confined: true })
3414
+ : bashOperation({ id, input: { script: command } }, onProgress, null, { onData });
3415
+ stop = () => { void shell.cancel().catch(() => {}); };
3416
+ const timer = timeout === undefined ? null : setTimeout(() => { if (!watchStopped) { timedOut = true; stop(); } }, timeout * 1000);
3417
+ signal.addEventListener("abort", stop, { once: true });
3418
+ return shell.promise.then((result) => {
3419
+ clearTimeout(timer);
3420
+ clearTimeout(watchTimer);
3421
+ signal.removeEventListener("abort", stop);
3422
+ if (result.error === "process_group_cleanup_unconfirmed") state.cleanupUnconfirmed = true;
3423
+ if (signal.aborted || state.cleanupUnconfirmed) throw new Error("aborted");
3424
+ // The whole group is gone, the watcher with it; the note is the output's last lines.
3425
+ if (watchStopped) return { exitCode: 124 };
3426
+ if (timedOut) throw new Error("timeout:" + timeout);
3427
+ // The Hands lease ended first.
3428
+ if (result.error === "timeout") throw new Error("timeout:" + Math.round((Date.now() - startedAt) / 1000));
3429
+ if (result.ok || result.error === "command_failed") return { exitCode: result.exitCode };
3430
+ throw new Error(result.error || "spawn_failed");
3431
+ });
3432
+ } };
3433
+ }
3434
+ // Results are journaled and cached; Pi's largest are inline images (4.5 MB
3435
+ // base64) and Bash structured output (1 MiB, escaped).
3436
+ const MAX_PI_TOOL_OUTCOME_BYTES = 12 * 1024 * 1024;
3437
+ function piToolOperation(request, onProgress, confinement) {
3438
+ const controller = new AbortController();
3439
+ const state = { cleanupUnconfirmed: false };
3440
+ let cancelled = false;
3441
+ const promise = (async () => {
3442
+ const { tool, args } = request.input;
3443
+ let outcome;
3444
+ try {
3445
+ const tools = loadPiTools();
3446
+ const cwd = config.repoPath;
3447
+ let instance;
3448
+ if (tool === "bash") {
3449
+ instance = tools.createBashTool(cwd, { operations: piShellOperations(request.id, onProgress, state, confinement), exposeSessionEnvironment: false });
3450
+ } else if (confinement) {
3451
+ const operations = piConfinedFileOperations(request.id, state, confinement, controller.signal, tool === "edit");
3452
+ instance = tool === "read" ? tools.createReadTool(cwd, { operations })
3453
+ : tool === "edit" ? tools.createEditTool(cwd, { operations }) : tools.createWriteTool(cwd, { operations });
3454
+ } else if (localGithubSandbox) {
3455
+ // Relative paths resolve against the canonical root workspaceTarget checks.
3456
+ const root = fs.realpathSync(cwd);
3457
+ const operations = confinedPiFileOperations(request.id, state, tool === "edit", controller.signal);
3458
+ instance = tool === "read" ? tools.createReadTool(root, { operations })
3459
+ : tool === "edit" ? tools.createEditTool(root, { operations }) : tools.createWriteTool(root, { operations });
3460
+ } else {
3461
+ instance = tool === "read" ? tools.createReadTool(cwd) : tool === "edit" ? tools.createEditTool(cwd) : tools.createWriteTool(cwd);
3462
+ }
3463
+ const result = await instance.execute(request.id, args, controller.signal);
3464
+ outcome = {
3465
+ content: result.content,
3466
+ ...(result.details === undefined ? {} : { details: result.details }),
3467
+ ...(result.structuredContent === undefined ? {} : { structuredContent: result.structuredContent }),
3468
+ ...(result.isError === true ? { isError: true } : {}),
3469
+ };
3470
+ } catch (error) {
3471
+ outcome = { thrown: error instanceof Error ? error.message : String(error) };
3472
+ }
3473
+ if (state.cleanupUnconfirmed) return failure(request.id, 1, "process_group_cleanup_unconfirmed", "", "");
3474
+ if (cancelled) return failure(request.id, 130, "cancelled", "", "");
3475
+ const bytes = Buffer.byteLength(JSON.stringify(outcome), "utf8");
3476
+ if (bytes > MAX_PI_TOOL_OUTCOME_BYTES) return failure(request.id, 1, "result_too_large", "", "");
3477
+ const result = { ...success(request.id, "", ""), piToolOutcome: outcome };
3478
+ piToolOutcomeBytes.set(result, bytes);
3479
+ return result;
3480
+ })();
3481
+ return {
3482
+ promise,
3483
+ supportsCancellation: true,
3484
+ cancel() {
3485
+ cancelled = true;
3486
+ controller.abort();
3487
+ return promise.then(() => undefined);
3488
+ },
3489
+ };
3490
+ }
3491
+ // The same exclusive scope lock used by managed jobs owns request receipts.
3492
+ // Store no command/input text. Intent is fsynced BEFORE execution; completion
3493
+ // before acknowledgement. A restarted worker can recover completed output, but
3494
+ // an interrupted intent is uncertain and must never launch that effect again.
3495
+ const MAX_REQUEST_RECORD_BYTES = 16 * 1024 * 1024;
3496
+ let requestJournalFailed = false;
3497
+ const uncertainRequestIds = new Set();
3498
+ function prepareRequestJournal() {
3499
+ if (!journalDirectory) return;
3500
+ const directory = path.join(journalDirectory, "requests");
3501
+ try { fs.mkdirSync(directory, { mode: 0o700 }); }
3502
+ catch (error) { if (error.code !== "EEXIST") throw error; }
3503
+ const stat = fs.lstatSync(directory);
3504
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid()
3505
+ || (stat.mode & 0o077) !== 0) throw new Error("invalid_request_journal");
3506
+ // Stream the private directory: receipt count is not a lifetime call cap.
3507
+ // A crash may leave the fixed temp sibling; only the renamed final record
3508
+ // is authoritative, so it cannot authorize replay or a completed result.
3509
+ const directoryHandle = fs.opendirSync(directory);
3510
+ try {
3511
+ let item;
3512
+ while ((item = directoryHandle.readSync()) !== null) {
3513
+ const file = item.name;
3514
+ if (/^[0-9a-f]{64}\.json\.tmp$/.test(file)) continue;
3515
+ if (!/^[0-9a-f]{64}\.json$/.test(file)) throw new Error("invalid_request_journal");
3516
+ const record = privateJournalRead("requests/" + file, MAX_REQUEST_RECORD_BYTES).value;
3517
+ if (safeId(record.id) === "invalid" || requestFile(record.id) !== "requests/" + file) throw new Error("invalid_request_journal");
3518
+ const entry = readRequestReceipt(record.id);
3519
+ if (entry && entry.uncertain) uncertainRequestIds.add(record.id);
3520
+ }
3521
+ } finally { directoryHandle.closeSync(); }
3522
+ }
3523
+ function requestFile(id) { return "requests/" + digest(id) + ".json"; }
3524
+ function readRequestReceipt(id) {
3525
+ if (!journalDirectory) return null;
3526
+ if (requestJournalFailed) throw new Error("request_journal_unavailable");
3527
+ let record;
3528
+ try { record = privateJournalRead(requestFile(id), MAX_REQUEST_RECORD_BYTES).value; }
3529
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
3530
+ if (!hasOnlyKeys(record, ["schema", "id", "hash", "semanticDigest", "result"])
3531
+ || record.schema !== 1 || record.id !== id || !SHA256_RE.test(record.hash)
3532
+ || (record.semanticDigest !== null && !SHA256_RE.test(record.semanticDigest))
3533
+ || (record.result !== null && (record.result.id !== id
3534
+ || record.result.version !== VERSION || typeof record.result.ok !== "boolean"
3535
+ || typeof record.result.stdout !== "string" || typeof record.result.stderr !== "string"))) {
3536
+ throw new Error("invalid_request_journal");
3537
+ }
3538
+ return { hash: record.hash, semanticDigest: record.semanticDigest, result: record.result,
3539
+ promise: null, uncertain: record.result === null };
3540
+ }
3541
+ function persistRequestReceipt(id, entry, result) {
3542
+ if (!journalDirectory) return;
3543
+ if (requestJournalFailed) throw new Error("request_journal_unavailable");
3544
+ try {
3545
+ privateJournalWrite(requestFile(id), JSON.stringify({ schema: 1, id, hash: entry.hash,
3546
+ semanticDigest: entry.semanticDigest, result }), MAX_REQUEST_RECORD_BYTES);
3547
+ } catch (error) { requestJournalFailed = true; throw error; }
3548
+ }
3549
+ function rememberResult(id, entry, result) {
3550
+ try { persistRequestReceipt(id, entry, result); }
3551
+ catch {
3552
+ entry.promise = null; entry.result = null; entry.uncertain = true;
3553
+ uncertainRequestIds.add(id);
3554
+ return false;
3555
+ }
3556
+ entry.promise = null;
3557
+ entry.result = result;
3558
+ uncertainRequestIds.delete(id);
3559
+ cachedResultIds.push(id);
3560
+ cachedPiToolOutcomeBytes += piToolOutcomeBytes.get(result) || 0;
3561
+ while (cachedResultIds.length > MAX_CACHED_RESULTS || cachedPiToolOutcomeBytes > MAX_CACHED_PI_TOOL_OUTCOME_BYTES) {
3562
+ const evictedId = cachedResultIds.shift();
3563
+ const evicted = ledger.get(evictedId);
3564
+ if (evicted && evicted.result) cachedPiToolOutcomeBytes -= piToolOutcomeBytes.get(evicted.result) || 0;
3565
+ if (journalDirectory) ledger.delete(evictedId);
3566
+ else if (evicted) evicted.result = null;
3567
+ }
3568
+ return true;
3569
+ }
3570
+ // Count retained Pi outcomes by size as well; other results are bounded by count.
3571
+ const MAX_CACHED_PI_TOOL_OUTCOME_BYTES = 64 * 1024 * 1024;
3572
+ const piToolOutcomeBytes = new WeakMap();
3573
+ let cachedPiToolOutcomeBytes = 0;
3574
+ async function handleRequest(target, connectionGeneration, claim, raw) {
3575
+ const receivedAtMs = performance.now();
3576
+ if (
3577
+ claim.generation !== targetGeneration
3578
+ || claim.fenceId !== targetFenceId
3579
+ ) {
3580
+ closeSocket(target, "stale_generation");
3581
+ return;
3582
+ }
3583
+ let request;
3584
+ try {
3585
+ request = parseRequest(raw, claim.generation);
3586
+ } catch (error) {
3587
+ if (error.code === "frame_too_large") closeSocket(target, "frame_too_large");
3588
+ else if (error.code === "invalid_json") closeSocket(target, "invalid_json");
3589
+ else if (error.code === "stale_generation") closeSocket(target, "stale_generation");
3590
+ else sendResult(target, failure(
3591
+ error.id || "invalid",
3592
+ 2,
3593
+ "invalid_request",
3594
+ "",
3595
+ "",
3596
+ claim.generation,
3597
+ ));
3598
+ return;
3599
+ }
3600
+ if ((request.tool === "pi_tool" && claim.piToolsVersion !== 1)
3601
+ || (request.tool === "bash" && request.input.retainBackground === true && claim.backgroundBashVersion !== 1)
3602
+ || (request.tool === "bash" && request.input.machineSdk === true && claim.machineSdkVersion !== 1)) {
3603
+ sendResult(target, failure(request.id, 2, "invalid_request", "", "", request.generation));
3604
+ return;
3605
+ }
3606
+ const heldMaintenance = currentMaintenance();
3607
+ if ((heldMaintenance && (request.tool !== "bash" || request.lifecycleLeaseId !== heldMaintenance.id || heldMaintenance.expiresAtMs <= Date.now()))
3608
+ || (request.lifecycleLeaseId !== undefined && (!heldMaintenance || request.lifecycleLeaseId !== heldMaintenance.id))) {
3609
+ sendResult(target, failure(request.id, 2, "maintenance_held", "", "", request.generation)); return;
3610
+ }
3611
+ let existing;
3612
+ try { existing = ledger.get(request.id) || readRequestReceipt(request.id); }
3613
+ catch {
3614
+ sendResult(target, failure(request.id, 2, "request_journal_unavailable", "", "", request.generation));
3615
+ return;
3616
+ }
3617
+ if (request.tool === "resume") {
3618
+ if (!existing) {
3619
+ sendResult(target, failure(request.id, 2, "request_not_found", "", "", request.generation));
3620
+ return;
3621
+ }
3622
+ if (existing.semanticDigest !== request.input.semanticDigest) {
3623
+ sendResult(target, failure(request.id, 2, "resume_digest_mismatch", "", "", request.generation));
3624
+ return;
3625
+ }
3626
+ if (existing.result) sendResult(
3627
+ target,
3628
+ bindResumeResult(existing.result, request.generation),
3629
+ );
3630
+ else if (existing.promise) {
3631
+ const result = await existing.promise;
3632
+ if (
3633
+ target === socket
3634
+ && generation === connectionGeneration
3635
+ && targetGeneration === request.generation
3636
+ && targetFenceId === claim.fenceId
3637
+ ) sendResult(target, bindResumeResult(result, request.generation));
3638
+ } else sendResult(target, failure(
3639
+ request.id,
3640
+ 2,
3641
+ existing.uncertain ? "request_outcome_uncertain" : "duplicate_result_evicted",
3642
+ "",
3643
+ "",
3644
+ request.generation,
3645
+ ));
3646
+ return;
3647
+ }
3648
+ // A receipt an earlier worker build journaled holds the hash of the frame as sent.
3649
+ const frameHash = requestHash(request);
3650
+ const confinedBashCall = piConfinedBashCallInput(request, claim.piToolsConfinement);
3651
+ const hash = confinedBashCall ? requestHash(request, confinedBashCall) : frameHash;
3652
+ if (existing) {
3653
+ if (existing.hash !== hash && existing.hash !== frameHash) {
3654
+ sendResult(target, failure(
3655
+ request.id,
3656
+ 2,
3657
+ "duplicate_request_id",
3658
+ "",
3659
+ "",
3660
+ request.generation,
3661
+ ));
3662
+ return;
3663
+ }
3664
+ if (existing.result) sendResult(
3665
+ target,
3666
+ bindResultGeneration(existing.result, request.generation),
3667
+ );
3668
+ else if (existing.promise) {
3669
+ const result = await existing.promise;
3670
+ if (
3671
+ target === socket
3672
+ && generation === connectionGeneration
3673
+ && targetGeneration === request.generation
3674
+ && targetFenceId === claim.fenceId
3675
+ ) sendResult(target, bindResultGeneration(result, request.generation));
3676
+ } else sendResult(target, failure(
3677
+ request.id,
3678
+ 2,
3679
+ existing.uncertain ? "request_outcome_uncertain" : "duplicate_result_evicted",
3680
+ "",
3681
+ "",
3682
+ request.generation,
3683
+ ));
3684
+ return;
3685
+ }
3686
+ if (ledger.size >= MAX_LEDGER_IDS) {
3687
+ sendResult(target, failure(
3688
+ request.id,
3689
+ 1,
3690
+ "request_ledger_full",
3691
+ "",
3692
+ "",
3693
+ request.generation,
3694
+ ));
3695
+ return;
3696
+ }
3697
+ const entry = { hash, semanticDigest: request.semanticDigest || null, promise: null, result: null };
3698
+ try { persistRequestReceipt(request.id, entry, null); }
3699
+ catch {
3700
+ sendResult(target, failure(request.id, 2, "request_journal_unavailable", "", "", request.generation));
3701
+ return;
3702
+ }
3703
+ const progress = (stdout, stderr, sequence) => {
3704
+ if (
3705
+ target === socket
3706
+ && generation === connectionGeneration
3707
+ && targetGeneration === request.generation
3708
+ && targetFenceId === claim.fenceId
3709
+ ) sendProgress(
3710
+ target,
3711
+ request.id,
3712
+ sequence,
3713
+ stdout,
3714
+ stderr,
3715
+ request.generation,
3716
+ );
3717
+ };
3718
+ const operation = request.tool === "bash"
3719
+ ? bashOperation(request, progress)
3720
+ : request.tool === "pi_tool"
3721
+ ? piToolOperation(request, request.input.tool === "bash" ? progress : null, claim.piToolsConfinement)
3722
+ : request.tool === "repl"
3723
+ ? localGithubSandbox ? { promise: Promise.resolve(failure(request.id, 1, "tool_unavailable", "", "local_device_gh_repl_unavailable")), cancel() {} } : replOperation(request)
3724
+ : localGithubSandbox || (config.spawnPolicy || config.bashCommandPrefix) && request.tool !== "read"
3725
+ ? payloadFileOperation(request)
3726
+ : fileOperation(request);
3727
+ // Every waiter observes the durable completion, never the raw child result.
3728
+ entry.promise = operation.promise.then((result) => {
3729
+ active.delete(request.id);
3730
+ return rememberResult(request.id, entry, result) ? result
3731
+ : failure(request.id, 2, "request_outcome_uncertain", "", "", request.generation);
3732
+ });
3733
+ ledger.set(request.id, entry);
3734
+ active.set(request.id, operation);
3735
+ const result = await entry.promise.finally(() => {
3736
+ // Read below at most once; never kept past this request (a changed socket or generation sends nothing).
3737
+ queueMicrotask(() => environmentWaits.delete(request.id));
3738
+ });
3739
+ if (
3740
+ target === socket
3741
+ && generation === connectionGeneration
3742
+ && targetGeneration === request.generation
3743
+ && targetFenceId === claim.fenceId
3744
+ ) {
3745
+ // Only this connection's own execution is timed; a cached or resumed
3746
+ // result is not. The Brain subtracts this from its round trip.
3747
+ const bound = bindResultGeneration(result, request.generation);
3748
+ const environmentWaitMs = claim.environmentWaitVersion === 1 ? environmentWaits.get(request.id) : undefined;
3749
+ sendResult(target, claim.executionTimingVersion === 1
3750
+ ? { ...bound, workerElapsedMs: Math.max(0, Math.round(performance.now() - receivedAtMs)),
3751
+ ...(environmentWaitMs ? { environmentWaitMs } : {}) } : bound);
3752
+ }
3753
+ }
3754
+ function parseClaim(raw) {
3755
+ if (Buffer.byteLength(raw, "utf8") > MAX_FRAME_BYTES) {
3756
+ const error = new Error("frame_too_large"); error.code = "frame_too_large"; throw error;
3757
+ }
3758
+ let value;
3759
+ try { value = JSON.parse(raw); } catch {
3760
+ const error = new Error("invalid_json"); error.code = "invalid_json"; throw error;
3761
+ }
3762
+ if (
3763
+ !hasOnlyKeys(value, ["type", "version", "generation", "fenceId", "executorLaunchId", "requestCancelVersion", "managedJobsVersion", "browserRunEnvelopeVersion", "terminalCleanupVersion", "guestMemoryVersion", "guestMemoryDiagnosticsVersion", "piToolsVersion", "piToolsConfinement", "backgroundBashVersion", "machineSdkVersion", "executionTimingVersion", "environmentWaitVersion"]) ||
3764
+ value.type !== "claim" || value.version !== VERSION ||
3765
+ !Number.isSafeInteger(value.generation) || value.generation < 1 ||
3766
+ typeof value.fenceId !== "string" || !UUID_RE.test(value.fenceId) ||
3767
+ typeof value.executorLaunchId !== "string" || !UUID_RE.test(value.executorLaunchId) ||
3768
+ (value.requestCancelVersion !== undefined && value.requestCancelVersion !== 1) ||
3769
+ (value.managedJobsVersion !== undefined && value.managedJobsVersion !== 1) ||
3770
+ (value.browserRunEnvelopeVersion !== undefined && value.browserRunEnvelopeVersion !== 1) ||
3771
+ (value.guestMemoryVersion !== undefined && value.guestMemoryVersion !== 1) ||
3772
+ (value.guestMemoryDiagnosticsVersion !== undefined && (value.guestMemoryDiagnosticsVersion !== 1 || value.guestMemoryVersion !== 1)) ||
3773
+ (value.terminalCleanupVersion !== undefined && value.terminalCleanupVersion !== 1) ||
3774
+ (value.piToolsVersion !== undefined && value.piToolsVersion !== 1) ||
3775
+ (value.piToolsConfinement !== undefined && (!validPiToolConfinement(value.piToolsConfinement) || value.piToolsVersion !== 1)) ||
3776
+ (value.backgroundBashVersion !== undefined && value.backgroundBashVersion !== 1) ||
3777
+ (value.machineSdkVersion !== undefined && value.machineSdkVersion !== 1) ||
3778
+ (value.executionTimingVersion !== undefined && value.executionTimingVersion !== 1)
3779
+ || (value.environmentWaitVersion !== undefined && (value.environmentWaitVersion !== 1 || value.executionTimingVersion !== 1))
3780
+ ) {
3781
+ const error = new Error("invalid_claim"); error.code = "invalid_claim"; throw error;
3782
+ }
3783
+ return value;
3784
+ }
3785
+ function handleClaim(target, connectionClaim, raw) {
3786
+ if (terminalCleanupStarted) { closeSocket(target, "terminal_cleanup_started"); return; }
3787
+ let claim;
3788
+ try {
3789
+ claim = parseClaim(raw);
3790
+ } catch (error) {
3791
+ diagnostic("claim_rejected", error.code === "frame_too_large" ? "frame_too_large" : error.code === "invalid_json" ? "invalid_json" : "invalid_claim");
3792
+ closeSocket(
3793
+ target,
3794
+ error.code === "frame_too_large" ? "frame_too_large" : error.code === "invalid_json" ? "invalid_json" : "invalid_claim",
3795
+ );
3796
+ return;
3797
+ }
3798
+ if (claim.executorLaunchId !== config.launchId) {
3799
+ diagnostic("claim_rejected", "executor_mismatch", claim.generation);
3800
+ closeSocket(target, "executor_mismatch");
3801
+ return;
3802
+ }
3803
+ if (claim.generation < targetGeneration) {
3804
+ diagnostic("claim_rejected", "stale_generation", claim.generation);
3805
+ closeSocket(target, "stale_generation");
3806
+ return;
3807
+ }
3808
+ if (claim.generation === targetGeneration && claim.fenceId !== targetFenceId) {
3809
+ diagnostic("claim_rejected", "fence_conflict", claim.generation);
3810
+ closeSocket(target, "fence_conflict");
3811
+ return;
3812
+ }
3813
+ if (claim.generation > targetGeneration) {
3814
+ maintenanceLease = null;
3815
+ if (journalDirectory) {
3816
+ try { privateJournalWrite("scope.json", JSON.stringify(journalScope(claim.generation, claim.fenceId))); }
3817
+ catch { closeSocket(target, "job_journal_unavailable"); return; }
3818
+ }
3819
+ targetGeneration = claim.generation;
3820
+ targetFenceId = claim.fenceId;
3821
+ markReady();
3822
+ }
3823
+ diagnostic("claim_accepted", null, claim.generation);
3824
+ connectionClaim.generation = claim.generation;
3825
+ connectionClaim.fenceId = claim.fenceId;
3826
+ connectionClaim.terminalCleanupVersion = claim.terminalCleanupVersion;
3827
+ connectionClaim.guestMemoryVersion = claim.guestMemoryVersion;
3828
+ connectionClaim.guestMemoryDiagnosticsVersion = claim.guestMemoryDiagnosticsVersion;
3829
+ connectionClaim.piToolsVersion = claim.piToolsVersion === 1 && piToolsAvailable() ? 1 : undefined;
3830
+ // The run's confinement for every pi_tool request on this connection. Local
3831
+ // GitHub confinement owns Pi's file and shell boundary there instead.
3832
+ connectionClaim.piToolsConfinement = claim.piToolsConfinement !== undefined && connectionClaim.piToolsVersion === 1
3833
+ && !localGithubSandbox ? claim.piToolsConfinement : undefined;
3834
+ // A spawn policy authorizes each Bash child only until its own deadline.
3835
+ connectionClaim.backgroundBashVersion = claim.backgroundBashVersion === 1 && allowedTools.includes("bash")
3836
+ && !config.spawnPolicy ? 1 : undefined;
3837
+ connectionClaim.machineSdkVersion = claim.machineSdkVersion === 1 && machineSdkAvailable() ? 1 : undefined;
3838
+ connectionClaim.executionTimingVersion = claim.executionTimingVersion === 1 ? 1 : undefined;
3839
+ connectionClaim.environmentWaitVersion = claim.environmentWaitVersion === 1 && claim.executionTimingVersion === 1 ? 1 : undefined;
3840
+ if (connectionClaim.environmentWaitVersion === 1) environmentWaitNegotiated = true;
3841
+ target.send(JSON.stringify({
3842
+ type: "claim_ack",
3843
+ version: VERSION,
3844
+ generation: targetGeneration,
3845
+ fenceId: targetFenceId,
3846
+ executorLaunchId: config.launchId,
3847
+ workerVersion: WORKER_VERSION,
3848
+ workerArtifactSha256: config.workerArtifactSha256,
3849
+ ...(claim.guestMemoryVersion === 1 && process.platform === "linux" && config.terminalCleanupRunId ? { guestMemoryVersion: 1 } : {}),
3850
+ ...(claim.guestMemoryDiagnosticsVersion === 1 && process.platform === "linux" && config.terminalCleanupRunId ? { guestMemoryDiagnosticsVersion: 1 } : {}),
3851
+ ...(claim.terminalCleanupVersion === 1 && process.platform === "linux" && config.terminalCleanupRunId && config.jobEnvSourcePath ? { terminalCleanupVersion: 1 } : {}),
3852
+ ...(claim.browserRunEnvelopeVersion === 1 && config.browserRunId ? { browserRunEnvelopeVersion: 1 } : {}),
3853
+ ...(connectionClaim.piToolsVersion === 1 ? { piToolsVersion: 1 } : {}),
3854
+ ...(connectionClaim.piToolsConfinement !== undefined ? { piToolsConfinementVersion: 1 } : {}),
3855
+ ...(connectionClaim.backgroundBashVersion === 1 ? { backgroundBashVersion: 1 } : {}),
3856
+ ...(connectionClaim.machineSdkVersion === 1 ? { machineSdkVersion: 1 } : {}),
3857
+ ...(connectionClaim.executionTimingVersion === 1 ? { executionTimingVersion: 1 } : {}),
3858
+ ...(connectionClaim.environmentWaitVersion === 1 ? { environmentWaitVersion: 1 } : {}),
3859
+ ...(claim.requestCancelVersion === 1 && claim.managedJobsVersion === 1 ? { lifecycleVersion: 1 } : {}),
3860
+ ...(claim.requestCancelVersion === 1 ? { requestCancelVersion: 1 } : {}),
3861
+ ...(claim.managedJobsVersion === 1 && config.managedJobs && allowedTools.includes("bash") ? { managedJobsVersion: 1 } : {}),
3862
+ }));
3863
+ }
3864
+ function cancelActive(hard) {
3865
+ const terminal = [];
3866
+ for (const operation of active.values()) {
3867
+ const cleanup = operation.cancel(hard);
3868
+ terminal.push(operation.promise);
3869
+ if (cleanup) terminal.push(cleanup);
3870
+ }
3871
+ return terminal;
3872
+ }
3873
+ function markReady() {
3874
+ const temp = config.readyPath + ".tmp";
3875
+ fs.writeFileSync(temp, JSON.stringify({
3876
+ generation,
3877
+ pid: process.pid,
3878
+ launchId: config.launchId,
3879
+ workerVersion: WORKER_VERSION,
3880
+ protocolVersion: VERSION,
3881
+ workerArtifactSha256: config.workerArtifactSha256,
3882
+ workerEntrypoint: WORKER_ENTRYPOINT,
3883
+ targetGeneration,
3884
+ fenceId: targetFenceId,
3885
+ workspaceAliasReady: true,
3886
+ }), { encoding: "utf8", mode: 0o600 });
3887
+ fs.renameSync(temp, config.readyPath);
3888
+ fs.chmodSync(config.readyPath, 0o600);
3889
+ }
3890
+ function reconnect() {
3891
+ if (stopped || Date.now() >= config.expiresAtMs) return;
3892
+ setTimeout(connect, retryMs);
3893
+ retryMs = Math.min(1000, retryMs * 2);
3894
+ }
3895
+ function disconnected(next, closeEvent) {
3896
+ if (stopped || next !== socket) return;
3897
+ socket = null;
3898
+ socketClaim = null;
3899
+ failSdkCalls(next, SDK_DISCONNECTED);
3900
+ // A Brain socket disappearing is not, by itself, a user cancellation. The
3901
+ // executor ledger must keep an in-flight Bash/REPL operation alive so a
3902
+ // replacement Brain can reconnect and receive the exact result. Explicit
3903
+ // lifecycle/abort close reasons still cancel work; an unclean TCP close has
3904
+ // no reason and is deliberately recoverable.
3905
+ const reason = closeEvent && typeof closeEvent.reason === "string"
3906
+ ? closeEvent.reason
3907
+ : "";
3908
+ if ([
3909
+ "request_aborted",
3910
+ "request_timeout",
3911
+ "client_closed",
3912
+ "driver_stopped",
3913
+ "driver_handoff",
3914
+ "run_cancelled",
3915
+ "run_finished",
3916
+ ].includes(reason)) {
3917
+ cancelActive(false);
3918
+ }
3919
+ reconnect();
3920
+ }
3921
+ function connect() {
3922
+ if (stopped || Date.now() >= config.expiresAtMs) return;
3923
+ connectionAttempt += 1;
3924
+ diagnostic("connecting");
3925
+ const next = config.proxyUrl
3926
+ ? new WebSocket(config.url, { proxy: config.proxyUrl })
3927
+ : new WebSocket(config.url);
3928
+ const connectionClaim = { generation: 0, fenceId: "" };
3929
+ socket = next;
3930
+ socketClaim = connectionClaim;
3931
+ const pendingTimer = setTimeout(() => {
3932
+ if (!stopped && next === socket && next.readyState === WebSocket.CONNECTING) diagnostic("dial_pending", "open_not_received");
3933
+ }, 10000);
3934
+ pendingTimer.unref();
3935
+ next.addEventListener("open", () => {
3936
+ if (stopped || next !== socket) return;
3937
+ clearTimeout(pendingTimer);
3938
+ generation += 1;
3939
+ diagnostic("open");
3940
+ retryMs = 25;
3941
+ markReady();
3942
+ });
3943
+ next.addEventListener("message", (event) => {
3944
+ if (next !== socket || generation < 1) return;
3945
+ if (typeof event.data !== "string") {
3946
+ closeSocket(next, "text_only");
3947
+ return;
3948
+ }
3949
+ if (Buffer.byteLength(event.data, "utf8") > MAX_FRAME_BYTES) {
3950
+ closeSocket(next, "frame_too_large"); return;
3951
+ }
3952
+ let frameType = "";
3953
+ try {
3954
+ const value = JSON.parse(event.data);
3955
+ frameType = value && typeof value === "object" ? value.type : "";
3956
+ } catch { frameType = ""; }
3957
+ if (frameType === "claim") {
3958
+ handleClaim(next, connectionClaim, event.data);
3959
+ return;
3960
+ }
3961
+ if (connectionClaim.generation < 1) {
3962
+ closeSocket(next, "claim_required");
3963
+ return;
3964
+ }
3965
+ if (connectionClaim.generation !== targetGeneration || connectionClaim.fenceId !== targetFenceId) {
3966
+ closeSocket(next, "stale_generation"); return;
3967
+ }
3968
+ if (frameType === "guest_memory") { handleGuestMemory(next, { ...connectionClaim }, event.data); return; }
3969
+ if (frameType === "terminal_cleanup") { void handleTerminalCleanup(next, { ...connectionClaim }, event.data); return; }
3970
+ if (terminalCleanupStarted) { closeSocket(next, "terminal_cleanup_started"); return; }
3971
+ if (frameType === "lifecycle") { handleLifecycle(next, { ...connectionClaim }, event.data); return; }
3972
+ if (frameType === "job") {
3973
+ void handleJob({ ...connectionClaim }, event.data, (result) => sendResult(next, result), () => next === socket);
3974
+ return;
3975
+ }
3976
+ if (frameType === "cancel_request") { void handleRequestCancel(next, { ...connectionClaim }, event.data); return; }
3977
+ if (frameType === "sdk_response") { handleSdkResponse(next, event.data); return; }
3978
+ void handleRequest(next, generation, connectionClaim, event.data);
3979
+ });
3980
+ next.addEventListener("error", () => {
3981
+ clearTimeout(pendingTimer);
3982
+ diagnostic("socket_error", next === socket ? "current_connection" : "obsolete_connection");
3983
+ disconnected(next);
3984
+ });
3985
+ next.addEventListener("close", (event) => {
3986
+ clearTimeout(pendingTimer);
3987
+ const knownReasons = ["request_aborted", "request_timeout", "client_closed", "driver_stopped", "driver_handoff", "run_cancelled", "run_finished"];
3988
+ diagnostic("close", knownReasons.includes(event.reason) ? event.reason : "other",
3989
+ null, Number.isSafeInteger(event.code) ? event.code : null);
3990
+ disconnected(next, event);
3991
+ });
3992
+ }
3993
+ function stop(reason) {
3994
+ if (stoppingPromise) return stoppingPromise;
3995
+ stopped = true;
3996
+ for (const client of jobControlClients) client.destroy();
3997
+ if (jobControlServer) jobControlServer.close();
3998
+ diagnostic("stopping", reason);
3999
+ // Persistent terminal shells outlive the worker; their viewer clients are active PTY jobs.
4000
+ const terminal = [...cancelActive(true), ...stopBackgroundGroups()];
4001
+ removeMachineSdk();
4002
+ if (socket && socket.readyState < WebSocket.CLOSING) socket.close(1000, "stopped");
4003
+ try { fs.unlinkSync(config.readyPath); } catch { stopped = true; }
4004
+ // A clean worker exit is the driver's exact stop proof. Do not exit until
4005
+ // every tracked Bash process group or REPL request has reached its terminal
4006
+ // close path after cancellation. If one cannot settle, the driver eventually
4007
+ // force-kills us and deliberately withholds hands.stopped.
4008
+ stoppingPromise = Promise.all([...terminal, ...(terminalCleanupPromise ? [terminalCleanupPromise] : [])]).then(
4009
+ (results) => {
4010
+ if (journalFailed || cleanupUnconfirmed || results.some((result) => result && result.error === "process_group_cleanup_unconfirmed")
4011
+ || [...jobs.values()].some((job) => job.record.state === "uncertain")) {
4012
+ diagnostic("stopped", "cleanup_failed"); process.exit(1);
4013
+ }
4014
+ try { releaseJournalOwnership(); } catch { diagnostic("stopped", "cleanup_failed"); process.exit(1); }
4015
+ if (config.directTerminalReceiptPath) {
4016
+ const job = jobs.get(config.directShell.jobId)?.record;
4017
+ if (job && ["exited", "cancelled", "expired"].includes(job.state) && Number.isSafeInteger(job.exitCode)) {
4018
+ try {
4019
+ const fd = fs.openSync(config.directTerminalReceiptPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
4020
+ try {
4021
+ fs.writeFileSync(fd, JSON.stringify({ launch_id: config.launchId, fence_id: config.fenceId,
4022
+ scope_id: config.managedJobs.scopeId, observation: { version: 1, process_id: job.jobId,
4023
+ state: job.state, exit_code: job.exitCode, observed_at: new Date().toISOString() } }));
4024
+ fs.fsyncSync(fd);
4025
+ } finally { fs.closeSync(fd); }
4026
+ } catch { diagnostic("stopped", "cleanup_receipt_failed"); process.exit(1); }
4027
+ }
4028
+ }
4029
+ diagnostic("stopped", "cleanup_complete");
4030
+ process.exit(0);
4031
+ },
4032
+ () => { diagnostic("stopped", "cleanup_failed"); process.exit(1); },
4033
+ );
4034
+ return stoppingPromise;
4035
+ }
4036
+ process.once("SIGTERM", () => { void stop("SIGTERM"); });
4037
+ process.once("SIGINT", () => { void stop("SIGINT"); });
4038
+ process.once("SIGHUP", () => { void stop("SIGHUP"); });
4039
+ setTimeout(() => { void stop("expired"); }, Math.max(1, config.expiresAtMs - Date.now())).unref();
4040
+ void startJobControl().then(() => {
4041
+ // Socket ownership is established before recovery mutates any live records.
4042
+ try { loadJobs(); prepareRequestJournal(); } catch { process.exit(44); }
4043
+ if (validPrivateJob) markReady(); else connect();
4044
+ }, () => process.exit(45));
4045
+ `.trimStart(),Rt=ot("sha256").update(ht,"utf8").digest("hex");
4046
+ export{n as tb,h as ub,qt as vb,o as wb,kt as xb,b as yb,m as zb,ut as Ab,ct as Bb,ft as Cb,dt as Db,Nt as Eb,lt as Fb,gt as Gb,ht as Hb,Rt as Ib};