@cjhyy/code-shell-server 0.9.47 → 0.9.49

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 (57) hide show
  1. package/README.md +4 -0
  2. package/dist/desktop-web/http-api.d.ts +9 -0
  3. package/dist/desktop-web/http-api.js +17 -1
  4. package/dist/http-byte-range.d.ts +5 -0
  5. package/dist/http-byte-range.js +19 -0
  6. package/dist/hub/auth-store.d.ts +2 -0
  7. package/dist/hub/auth-store.js +26 -7
  8. package/dist/hub/files.js +71 -9
  9. package/dist/index.panels.d.ts +1 -0
  10. package/dist/index.panels.js +1 -0
  11. package/dist/mobile-remote/access-passcode.d.ts +5 -0
  12. package/dist/mobile-remote/access-passcode.js +48 -9
  13. package/dist/mobile-remote/codex-room-agent.js +3 -1
  14. package/dist/mobile-remote/mobile-chat-turn.d.ts +6 -0
  15. package/dist/mobile-remote/mobile-chat-turn.js +15 -2
  16. package/dist/mobile-remote/mobile-client-event-validator.js +30 -1
  17. package/dist/mobile-remote/mobile-run-dispatch.d.ts +1 -0
  18. package/dist/mobile-remote/mobile-run-dispatch.js +13 -5
  19. package/dist/mobile-remote/mobile-static.d.ts +5 -6
  20. package/dist/mobile-remote/mobile-static.js +13 -14
  21. package/dist/mobile-remote/remote-host-manager.d.ts +7 -1
  22. package/dist/mobile-remote/remote-host-manager.js +132 -56
  23. package/dist/mobile-remote/resident-agent.js +3 -1
  24. package/dist/mobile-remote/room-manager.d.ts +4 -0
  25. package/dist/mobile-remote/room-manager.js +5 -0
  26. package/dist/mobile-remote/room-process-stop.d.ts +8 -0
  27. package/dist/mobile-remote/room-process-stop.js +18 -0
  28. package/dist/mobile-remote/tunnel-manager.d.ts +1 -0
  29. package/dist/mobile-remote/tunnel-manager.js +45 -20
  30. package/dist/panels/background-service-process.d.ts +39 -0
  31. package/dist/panels/background-service-process.js +457 -0
  32. package/dist/panels/background-service-store.d.ts +67 -0
  33. package/dist/panels/background-service-store.js +539 -0
  34. package/dist/panels/background-services.d.ts +193 -0
  35. package/dist/panels/background-services.js +1159 -0
  36. package/dist/panels/bridge-contract.d.ts +1 -1
  37. package/dist/panels/execution-gate.d.ts +10 -2
  38. package/dist/panels/execution-gate.js +36 -2
  39. package/dist/panels/http.d.ts +1 -0
  40. package/dist/panels/management-http.d.ts +1 -0
  41. package/dist/panels/management.d.ts +3 -0
  42. package/dist/panels/management.js +105 -21
  43. package/dist/panels/package-mutation.d.ts +1 -1
  44. package/dist/panels/package-mutation.js +3 -10
  45. package/dist/panels/process-files.js +28 -3
  46. package/dist/panels/resources/library.d.ts +1 -4
  47. package/dist/panels/resources/library.js +2 -18
  48. package/dist/panels/resources/references.js +21 -8
  49. package/dist/panels/runtime.js +1 -1
  50. package/dist/panels/tool-jobs.d.ts +2 -0
  51. package/dist/panels/tool-jobs.js +1 -1
  52. package/dist/serve/headless-server.js +113 -79
  53. package/dist/sessions-service.d.ts +4 -0
  54. package/dist/sessions-service.js +17 -0
  55. package/dist/static-files.d.ts +7 -0
  56. package/dist/static-files.js +114 -0
  57. package/package.json +6 -6
@@ -0,0 +1,67 @@
1
+ /** Immutable OS boot/start identity; lack of inspection is never proof of death. */
2
+ export interface ServiceProcessIdentity {
3
+ pid: number;
4
+ start: string;
5
+ host: string;
6
+ boot?: string;
7
+ /** Stable local OS machine identity; hostname alone is not shared-root ownership. */
8
+ machine?: string;
9
+ }
10
+ export interface ServiceIdentityInspector {
11
+ current(pid: number): ServiceProcessIdentity | null;
12
+ status(identity: ServiceProcessIdentity): "alive" | "dead" | "unknown";
13
+ }
14
+ /** A live Host cannot cross OS boots. Only this constant is cached; PID/start
15
+ * identity and process/group liveness must always be inspected afresh. */
16
+ export declare function createBootSessionReader(read: () => string, now?: () => number): () => string | null;
17
+ /** Machine identity is constant for this Host lifetime. Failed OS inspection may
18
+ * be slow, so retry once per minute rather than on each reconciliation tick. */
19
+ export declare function createMachineIdentityReader(read: () => string | undefined, platform?: NodeJS.Platform, now?: () => number): () => string | undefined;
20
+ export declare function serviceSystemIdentity(): Pick<ServiceProcessIdentity, "host" | "boot" | "machine">;
21
+ export declare function parseLinuxProcessStat(stat: string): {
22
+ pid: number;
23
+ group: number;
24
+ state: string;
25
+ start: string;
26
+ } | null;
27
+ export declare function processIdentity(pid: number): ServiceProcessIdentity | null;
28
+ /** Kernel unique process ID and exec version, never a wall-clock timestamp. */
29
+ export declare function darwinProcessStart(pid: number): string | null;
30
+ export declare function identityStatus(identity: ServiceProcessIdentity, inspect?: (pid: number) => ServiceProcessIdentity | null, system?: Pick<ServiceProcessIdentity, "host" | "boot" | "machine"> | null): "alive" | "dead" | "unknown";
31
+ /** Online migration is safe only inside the same verified OS boot. A legacy
32
+ * record first encountered after reboot has no proof it came from this machine. */
33
+ export declare function upgradeLocalServiceIdentity(identity: ServiceProcessIdentity, system?: Pick<ServiceProcessIdentity, "host" | "boot" | "machine">): ServiceProcessIdentity;
34
+ export declare function identityGroupStatus(identity: ServiceProcessIdentity, system?: Pick<ServiceProcessIdentity, "host" | "boot" | "machine">, inspectGroup?: typeof groupStatus): "alive" | "dead" | "unknown";
35
+ export declare const serviceIdentityInspector: ServiceIdentityInspector;
36
+ export declare function groupStatus(pid: number): "alive" | "dead" | "unknown";
37
+ /** kill(0) includes unreaped zombies on Linux. Prove that every visible group
38
+ * member is terminal; incomplete /proc visibility remains unknown. */
39
+ export declare function linuxGroupStatus(pid: number, read?: (path: string) => string, list?: () => string[]): "alive" | "dead" | "unknown";
40
+ export declare const backgroundServiceReadFormats: Set<string>;
41
+ export declare const backgroundServiceWriteFormats: Set<string>;
42
+ export type CommitBoundary = "write" | "file-fsync" | "rename" | "directory-fsync" | "lock-write" | "lock-file-fsync" | "lock-stage-fsync" | "lock-rename" | "lock-root-fsync";
43
+ export declare class BackgroundServiceStore<T extends {
44
+ format: string;
45
+ }> {
46
+ readonly root: string;
47
+ private readonly fault?;
48
+ private readonly identities;
49
+ private owner?;
50
+ private poisoned;
51
+ private lockPending;
52
+ constructor(root: string, fault?: ((boundary: CommitBoundary) => void) | undefined, identities?: ServiceIdentityInspector);
53
+ acquire(): void;
54
+ available(): boolean;
55
+ /** Only the retained exclusive owner may reload after an uncertain write. */
56
+ recover(validate?: (record: T) => void): T[];
57
+ quarantine(): void;
58
+ release(): void;
59
+ key(scope: {
60
+ appId: string;
61
+ projectPath: string;
62
+ name: string;
63
+ }): string;
64
+ read(key: string): T | undefined;
65
+ all(): T[];
66
+ commit(key: string, record: T): void;
67
+ }
@@ -0,0 +1,539 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { closeSync, fsyncSync, mkdirSync, lstatSync, openSync, readFileSync, readdirSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { hostname } from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ /** A live Host cannot cross OS boots. Only this constant is cached; PID/start
7
+ * identity and process/group liveness must always be inspected afresh. */
8
+ export function createBootSessionReader(read, now = () => performance.now()) {
9
+ let boot = null;
10
+ let retryAt = -Infinity;
11
+ return () => {
12
+ if (boot)
13
+ return boot;
14
+ if (now() < retryAt)
15
+ return null;
16
+ try {
17
+ const value = read().trim();
18
+ if (/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value)) {
19
+ boot = value;
20
+ return boot;
21
+ }
22
+ }
23
+ catch {
24
+ // Failed inspection proves neither ownership nor death.
25
+ }
26
+ retryAt = now() + 1000;
27
+ return null;
28
+ };
29
+ }
30
+ const darwinBootSession = createBootSessionReader(() => execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
31
+ encoding: "utf8",
32
+ timeout: 2000,
33
+ killSignal: "SIGKILL",
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ }));
36
+ const linuxBootSession = createBootSessionReader(() => readFileSync("/proc/sys/kernel/random/boot_id", "utf8"));
37
+ /** Machine identity is constant for this Host lifetime. Failed OS inspection may
38
+ * be slow, so retry once per minute rather than on each reconciliation tick. */
39
+ export function createMachineIdentityReader(read, platform = process.platform, now = () => performance.now()) {
40
+ let machineIdentity;
41
+ let retryAt = -Infinity;
42
+ return () => {
43
+ if (machineIdentity)
44
+ return machineIdentity;
45
+ if (now() < retryAt)
46
+ return undefined;
47
+ try {
48
+ const value = read()?.trim();
49
+ if (value && /^(?:[a-f0-9]{32}|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/i.test(value)) {
50
+ machineIdentity = createHash("sha256")
51
+ .update(`${platform}:${value.toLowerCase()}`)
52
+ .digest("hex");
53
+ return machineIdentity;
54
+ }
55
+ }
56
+ catch {
57
+ /* unavailable machine evidence never proves a different boot is local */
58
+ }
59
+ retryAt = now() + 60_000;
60
+ return undefined;
61
+ };
62
+ }
63
+ const localMachineIdentity = createMachineIdentityReader(() => process.platform === "linux"
64
+ ? readFileSync("/etc/machine-id", "utf8")
65
+ : process.platform === "darwin"
66
+ ? execFileSync("/usr/sbin/ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
67
+ encoding: "utf8",
68
+ timeout: 2000,
69
+ killSignal: "SIGKILL",
70
+ maxBuffer: 65536,
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ }).match(/"IOPlatformUUID"\s*=\s*"([a-f0-9-]+)"/i)?.[1]
73
+ : undefined);
74
+ export function serviceSystemIdentity() {
75
+ return {
76
+ host: hostname(),
77
+ boot: (process.platform === "linux" ? linuxBootSession() : darwinBootSession()) ?? undefined,
78
+ machine: localMachineIdentity(),
79
+ };
80
+ }
81
+ export function parseLinuxProcessStat(stat) {
82
+ const fields = stat
83
+ .slice(stat.lastIndexOf(")") + 2)
84
+ .trim()
85
+ .split(/\s+/);
86
+ const pid = Number(stat.slice(0, stat.indexOf(" "))), group = Number(fields[2]);
87
+ if (!Number.isSafeInteger(pid) ||
88
+ pid <= 0 ||
89
+ !Number.isSafeInteger(group) ||
90
+ group < 0 ||
91
+ !/^\d+$/.test(fields[19] ?? "") ||
92
+ !/^[A-Zt]$/.test(fields[0] ?? ""))
93
+ return null;
94
+ return { pid, group, state: fields[0], start: `linux-ticks:${fields[19]}` };
95
+ }
96
+ export function processIdentity(pid) {
97
+ if (!Number.isSafeInteger(pid) || pid <= 0)
98
+ return null;
99
+ try {
100
+ if (process.platform === "linux") {
101
+ const system = serviceSystemIdentity();
102
+ const stat = parseLinuxProcessStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
103
+ // Zombies have closed their descriptors and can never execute again. Their
104
+ // retained /proc entry is not a live worker identity waiting for a sleep.
105
+ return system.boot && stat && !["Z", "X"].includes(stat.state)
106
+ ? { pid, ...system, start: stat.start }
107
+ : null;
108
+ }
109
+ if (process.platform !== "darwin")
110
+ return null;
111
+ // bootsessionuuid is stable under wall-clock/NTP adjustment; boottime is not.
112
+ const system = serviceSystemIdentity();
113
+ if (!system.boot)
114
+ return null;
115
+ const start = darwinProcessStart(pid);
116
+ return start ? { pid, start, ...system } : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ /** Kernel unique process ID and exec version, never a wall-clock timestamp. */
123
+ export function darwinProcessStart(pid) {
124
+ if (!Number.isSafeInteger(pid) || pid <= 0)
125
+ return null;
126
+ try {
127
+ // System JXA calls libproc without requiring Python, a compiler, an addon,
128
+ // or Apple Events to another application. Missing OS inspection fails closed.
129
+ const info = JSON.parse(execFileSync("/usr/bin/osascript", [
130
+ "-l",
131
+ "JavaScript",
132
+ "-e",
133
+ `
134
+ ObjC.import("Foundation");
135
+ ObjC.bindFunction("proc_pidinfo", ["int", ["int", "int", "uint64", "void *", "int"]]);
136
+ var data = $.NSMutableData.dataWithLength(56);
137
+ var count = $.proc_pidinfo(${pid}, 17, 0, data.mutableBytes, 56);
138
+ JSON.stringify({count: count, data: ObjC.unwrap(data.base64EncodedStringWithOptions(0))});
139
+ `,
140
+ ], {
141
+ encoding: "utf8",
142
+ timeout: 2000,
143
+ killSignal: "SIGKILL",
144
+ maxBuffer: 4096,
145
+ stdio: ["ignore", "pipe", "ignore"],
146
+ }));
147
+ const bytes = Buffer.from(info.data, "base64");
148
+ if (info.count !== 56 || bytes.length !== 56)
149
+ return null;
150
+ const unique = bytes.readBigUInt64LE(16);
151
+ const version = bytes.readUInt32LE(32);
152
+ return unique ? `darwin-unique:${unique}:${version}` : null;
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ export function identityStatus(identity, inspect = processIdentity, system = inspect === processIdentity ? serviceSystemIdentity() : inspect(process.pid)) {
159
+ const locality = identityLocality(identity, system);
160
+ if (locality === "old-boot")
161
+ return "dead";
162
+ if (locality !== "same-boot")
163
+ return "unknown";
164
+ try {
165
+ process.kill(identity.pid, 0);
166
+ }
167
+ catch (error) {
168
+ return error.code === "ESRCH" ? "dead" : "unknown";
169
+ }
170
+ const current = inspect(identity.pid);
171
+ if (!current && process.platform === "linux") {
172
+ try {
173
+ const stat = parseLinuxProcessStat(readFileSync(`/proc/${identity.pid}/stat`, "utf8"));
174
+ if (stat && ["Z", "X"].includes(stat.state))
175
+ return "dead";
176
+ }
177
+ catch {
178
+ /* an unreadable /proc entry does not prove death */
179
+ }
180
+ }
181
+ // Legacy nonce-only records cannot prove PID reuse. Never kill or steal them.
182
+ if (!current ||
183
+ !identity.boot ||
184
+ !/^(?:darwin-unique:\d+:\d+|linux-ticks:\d+)$/.test(identity.start))
185
+ return "unknown";
186
+ return current.boot === identity.boot && current.start === identity.start ? "alive" : "dead";
187
+ }
188
+ function identityLocality(identity, system) {
189
+ if (!identity.boot || !system?.boot)
190
+ return "unknown";
191
+ if (identity.machine && system.machine && identity.machine !== system.machine)
192
+ return "unknown";
193
+ if (identity.boot === system.boot)
194
+ return "same-boot";
195
+ return identity.machine && identity.machine === system.machine ? "old-boot" : "unknown";
196
+ }
197
+ /** Online migration is safe only inside the same verified OS boot. A legacy
198
+ * record first encountered after reboot has no proof it came from this machine. */
199
+ export function upgradeLocalServiceIdentity(identity, system = serviceSystemIdentity()) {
200
+ return !identity.machine && system.machine && identityLocality(identity, system) === "same-boot"
201
+ ? { ...identity, machine: system.machine }
202
+ : identity;
203
+ }
204
+ export function identityGroupStatus(identity, system = serviceSystemIdentity(), inspectGroup = groupStatus) {
205
+ const locality = identityLocality(identity, system);
206
+ return locality === "old-boot"
207
+ ? "dead"
208
+ : locality === "same-boot"
209
+ ? inspectGroup(identity.pid)
210
+ : "unknown";
211
+ }
212
+ export const serviceIdentityInspector = {
213
+ current: processIdentity,
214
+ status: identityStatus,
215
+ };
216
+ export function groupStatus(pid) {
217
+ if (!Number.isSafeInteger(pid) || pid <= 0)
218
+ return "unknown";
219
+ try {
220
+ process.kill(-pid, 0);
221
+ if (process.platform !== "linux")
222
+ return "alive";
223
+ return linuxGroupStatus(pid);
224
+ }
225
+ catch (error) {
226
+ return error.code === "ESRCH" ? "dead" : "unknown";
227
+ }
228
+ }
229
+ /** kill(0) includes unreaped zombies on Linux. Prove that every visible group
230
+ * member is terminal; incomplete /proc visibility remains unknown. */
231
+ export function linuxGroupStatus(pid, read = (path) => readFileSync(path, "utf8"), list = () => readdirSync("/proc")) {
232
+ // A task can disappear after enumeration or after opening its stat file.
233
+ // ENOENT and ESRCH both describe that absence; permissions/visibility errors
234
+ // do not. A short/invalid read gets one fresh observation, never a guessed
235
+ // identity: only a valid stat or confirmed absence can complete the scan.
236
+ const memberAt = (memberPid) => {
237
+ for (let attempt = 0; attempt < 2; attempt++) {
238
+ try {
239
+ const member = parseLinuxProcessStat(read(`/proc/${memberPid}/stat`));
240
+ if (member?.pid === memberPid)
241
+ return member;
242
+ }
243
+ catch (error) {
244
+ if (["ENOENT", "ESRCH"].includes(error.code ?? ""))
245
+ return null;
246
+ throw error;
247
+ }
248
+ }
249
+ throw new Error("SERVICE_PROC_STAT_UNREADABLE");
250
+ };
251
+ try {
252
+ const leader = memberAt(pid);
253
+ if (leader?.group === pid && !["Z", "X"].includes(leader.state))
254
+ return "alive";
255
+ const mounts = read("/proc/mounts")
256
+ .split("\n")
257
+ .map((line) => line.split(" "));
258
+ const mount = mounts.find((fields) => fields[1] === "/proc" && fields[2] === "proc");
259
+ if (!mount ||
260
+ mount[3].split(",").some((option) => option.startsWith("hidepid=") && option !== "hidepid=0"))
261
+ return "unknown";
262
+ if (parseLinuxProcessStat(read("/proc/self/stat"))?.pid !== process.pid)
263
+ return "unknown";
264
+ const scan = () => {
265
+ const terminal = new Map();
266
+ for (const name of list()) {
267
+ if (!/^\d+$/.test(name))
268
+ continue;
269
+ const member = memberAt(Number(name));
270
+ if (!member || member.group !== pid)
271
+ continue;
272
+ if (!["Z", "X"].includes(member.state))
273
+ return "alive";
274
+ terminal.set(member.pid, member.start);
275
+ }
276
+ return terminal;
277
+ };
278
+ const before = scan();
279
+ if (typeof before === "string")
280
+ return before;
281
+ if (!before.size)
282
+ return "unknown";
283
+ // /proc enumeration is not a snapshot: a member can fork after the first
284
+ // listing and become a zombie before stat. All first-pass members are now
285
+ // terminal, so enumerate afresh and require exactly the same start identities.
286
+ // A late descendant, reaped member or PID reuse keeps recovery conservative.
287
+ const after = scan();
288
+ if (typeof after === "string")
289
+ return after;
290
+ return before.size === after.size &&
291
+ [...before].every(([member, start]) => after.get(member) === start)
292
+ ? "dead"
293
+ : "unknown";
294
+ }
295
+ catch {
296
+ return "unknown";
297
+ }
298
+ }
299
+ export const backgroundServiceReadFormats = new Set(["panel.background-service/1"]);
300
+ export const backgroundServiceWriteFormats = new Set(["panel.background-service/1"]);
301
+ function syncDirectory(path) {
302
+ const file = openSync(path, "r");
303
+ try {
304
+ fsyncSync(file);
305
+ }
306
+ finally {
307
+ closeSync(file);
308
+ }
309
+ }
310
+ function readLockOwner(path) {
311
+ try {
312
+ const info = lstatSync(path);
313
+ if (!info.isFile() || info.size === 0 || info.size > 8192)
314
+ throw new Error("owner is not a bounded regular file");
315
+ const value = JSON.parse(readFileSync(path, "utf8"));
316
+ if (!value ||
317
+ !Number.isSafeInteger(value.pid) ||
318
+ value.pid <= 0 ||
319
+ typeof value.host !== "string" ||
320
+ !value.host ||
321
+ value.host.length > 255 ||
322
+ typeof value.start !== "string" ||
323
+ !value.start ||
324
+ value.start.length > 256 ||
325
+ (value.boot !== undefined &&
326
+ (typeof value.boot !== "string" || !value.boot || value.boot.length > 128)) ||
327
+ (value.machine !== undefined &&
328
+ (typeof value.machine !== "string" || !value.machine || value.machine.length > 128)))
329
+ throw new Error("invalid owner identity");
330
+ return value;
331
+ }
332
+ catch (cause) {
333
+ throw new Error("SERVICE_COORDINATOR_LOCK_CORRUPT: owner token is empty, damaged or unreadable; retained for diagnosis", { cause });
334
+ }
335
+ }
336
+ export class BackgroundServiceStore {
337
+ root;
338
+ fault;
339
+ identities;
340
+ owner;
341
+ poisoned = false;
342
+ lockPending = false;
343
+ constructor(root, fault, identities = serviceIdentityInspector) {
344
+ this.root = root;
345
+ this.fault = fault;
346
+ this.identities = identities;
347
+ }
348
+ acquire() {
349
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
350
+ const lock = join(this.root, "coordinator.lock");
351
+ if (this.owner) {
352
+ if (!this.lockPending)
353
+ return;
354
+ const owners = readdirSync(lock);
355
+ if (owners.length !== 1 || owners[0] !== this.owner)
356
+ throw new Error("SERVICE_COORDINATOR_LOCK_CORRUPT: retained ownership changed");
357
+ readLockOwner(join(lock, this.owner));
358
+ // A previous rename succeeded but its directory fsync was uncertain. The
359
+ // same retained owner may finish durability; another owner cannot steal it.
360
+ syncDirectory(lock);
361
+ syncDirectory(this.root);
362
+ this.poisoned = false;
363
+ this.lockPending = false;
364
+ return;
365
+ }
366
+ const identity = this.identities.current(process.pid);
367
+ if (!identity)
368
+ throw new Error("SERVICE_IDENTITY_UNAVAILABLE");
369
+ const token = `owner-${randomUUID()}.json`;
370
+ const stage = join(this.root, `.lock-${randomUUID()}`);
371
+ const install = () => {
372
+ renameSync(stage, lock);
373
+ this.owner = token;
374
+ this.poisoned = true;
375
+ this.lockPending = true;
376
+ this.fault?.("lock-rename");
377
+ syncDirectory(this.root);
378
+ this.fault?.("lock-root-fsync");
379
+ this.poisoned = false;
380
+ this.lockPending = false;
381
+ // Abandoned payloads were never authoritative. Only the elected writer
382
+ // cleans exact temporary-record names; stop tombstones are never removed.
383
+ for (const name of readdirSync(this.root))
384
+ if (/^\.[a-f0-9]{64}-[a-f0-9-]+\.tmp$/.test(name))
385
+ unlinkSync(join(this.root, name));
386
+ };
387
+ try {
388
+ mkdirSync(stage, { mode: 0o700 });
389
+ const file = openSync(join(stage, token), "wx", 0o600);
390
+ try {
391
+ writeFileSync(file, JSON.stringify(identity));
392
+ this.fault?.("lock-write");
393
+ fsyncSync(file);
394
+ this.fault?.("lock-file-fsync");
395
+ }
396
+ finally {
397
+ closeSync(file);
398
+ }
399
+ syncDirectory(stage);
400
+ this.fault?.("lock-stage-fsync");
401
+ try {
402
+ install();
403
+ return;
404
+ }
405
+ catch (error) {
406
+ if (!["EEXIST", "ENOTEMPTY"].includes(error.code ?? ""))
407
+ throw error;
408
+ }
409
+ const owners = readdirSync(lock);
410
+ if (owners.length !== 1 || !/^owner-[a-f0-9-]+\.json$/.test(owners[0]))
411
+ throw new Error("SERVICE_COORDINATOR_LOCK_CORRUPT: expected exactly one immutable owner token");
412
+ const previous = readLockOwner(join(lock, owners[0]));
413
+ if (this.identities.status(previous) !== "dead") {
414
+ if (!previous.machine && previous.boot && identity.boot && previous.boot !== identity.boot)
415
+ throw new Error("SERVICE_COORDINATOR_LEGACY_LOCALITY_UNCONFIRMED: old-boot token has no machine proof; retained, do not remove until its original Host and group are verified stopped");
416
+ throw new Error("SERVICE_COORDINATOR_BUSY");
417
+ }
418
+ // Exact immutable token only. Never rename an existing shared lock directory.
419
+ unlinkSync(join(lock, owners[0]));
420
+ syncDirectory(lock);
421
+ rmdirSync(lock);
422
+ syncDirectory(this.root);
423
+ install();
424
+ }
425
+ finally {
426
+ if (!this.owner) {
427
+ try {
428
+ unlinkSync(join(stage, token));
429
+ rmdirSync(stage);
430
+ }
431
+ catch {
432
+ /* another owner wins */
433
+ }
434
+ }
435
+ }
436
+ }
437
+ available() {
438
+ return !!this.owner && !this.poisoned;
439
+ }
440
+ /** Only the retained exclusive owner may reload after an uncertain write. */
441
+ recover(validate) {
442
+ if (!this.owner || this.lockPending)
443
+ throw new Error("SERVICE_COORDINATOR_UNAVAILABLE");
444
+ const records = this.all();
445
+ for (const record of records)
446
+ validate?.(record);
447
+ this.poisoned = false;
448
+ return records;
449
+ }
450
+ quarantine() {
451
+ this.poisoned = true;
452
+ }
453
+ release() {
454
+ if (!this.owner)
455
+ return;
456
+ const token = this.owner;
457
+ this.owner = undefined;
458
+ try {
459
+ unlinkSync(join(this.root, "coordinator.lock", token));
460
+ syncDirectory(join(this.root, "coordinator.lock"));
461
+ rmdirSync(join(this.root, "coordinator.lock"));
462
+ syncDirectory(this.root);
463
+ }
464
+ catch {
465
+ /* only our exact token */
466
+ }
467
+ }
468
+ key(scope) {
469
+ return createHash("sha256")
470
+ .update([scope.appId, scope.projectPath, scope.name].join("\0"))
471
+ .digest("hex");
472
+ }
473
+ read(key) {
474
+ try {
475
+ const bytes = readFileSync(join(this.root, `${key}.json`));
476
+ if (bytes.length > 5 * 1024 * 1024)
477
+ throw new Error("SERVICE_RECORD_TOO_LARGE");
478
+ const record = JSON.parse(bytes.toString());
479
+ if (!backgroundServiceReadFormats.has(record.format))
480
+ throw new Error("SERVICE_FORMAT_UNSUPPORTED");
481
+ return record;
482
+ }
483
+ catch (error) {
484
+ if (error.code === "ENOENT")
485
+ return undefined;
486
+ throw error;
487
+ }
488
+ }
489
+ all() {
490
+ return readdirSync(this.root)
491
+ .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
492
+ .map((name) => this.read(name.slice(0, -5)));
493
+ }
494
+ commit(key, record) {
495
+ if (!this.owner || this.poisoned)
496
+ throw new Error("SERVICE_COORDINATOR_UNAVAILABLE");
497
+ if (!backgroundServiceWriteFormats.has(record.format))
498
+ throw new Error("SERVICE_FORMAT_UNSUPPORTED");
499
+ const data = JSON.stringify(record);
500
+ if (Buffer.byteLength(data) > 5 * 1024 * 1024)
501
+ throw new Error("SERVICE_RECORD_TOO_LARGE");
502
+ const temp = join(this.root, `.${key}-${randomUUID()}.tmp`);
503
+ let file;
504
+ try {
505
+ file = openSync(temp, "wx", 0o600);
506
+ writeFileSync(file, data);
507
+ this.fault?.("write");
508
+ fsyncSync(file);
509
+ this.fault?.("file-fsync");
510
+ closeSync(file);
511
+ file = undefined;
512
+ renameSync(temp, join(this.root, `${key}.json`));
513
+ this.fault?.("rename");
514
+ const directory = openSync(this.root, "r");
515
+ try {
516
+ fsyncSync(directory);
517
+ }
518
+ finally {
519
+ closeSync(directory);
520
+ }
521
+ this.fault?.("directory-fsync");
522
+ }
523
+ catch (error) {
524
+ // A rename may already be durable. Never keep writing a stale in-memory generation.
525
+ this.poisoned = true;
526
+ throw error;
527
+ }
528
+ finally {
529
+ if (file !== undefined)
530
+ closeSync(file);
531
+ try {
532
+ unlinkSync(temp);
533
+ }
534
+ catch {
535
+ /* rename consumed the temporary file */
536
+ }
537
+ }
538
+ }
539
+ }