stitchkit 0.103.8 → 0.103.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/agent-runtime/sandbox-process.d.ts +2 -2
- package/dist/agent-runtime/sandbox-process.d.ts.map +1 -1
- package/dist/agent-runtime/store-conformance-lifecycle.d.ts +10 -10
- package/dist/durability/contract.d.ts +3 -2
- package/dist/durability/contract.d.ts.map +1 -1
- package/dist/durability/effect.d.ts.map +1 -1
- package/dist/durability/engine.d.ts.map +1 -1
- package/dist/durability/json.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/harness.js +2 -2
- package/dist/entrypoints/agent-runtime/sandbox.js +29 -81
- package/dist/entrypoints/agent-runtime.js +2 -2
- package/dist/entrypoints/application/diagnostic-journal.js +2 -1
- package/dist/entrypoints/cli.js +1 -1
- package/dist/entrypoints/files.d.ts +2 -0
- package/dist/entrypoints/files.d.ts.map +1 -1
- package/dist/entrypoints/files.js +14 -6
- package/dist/entrypoints/primitives.js +77 -57
- package/dist/entrypoints/process.d.ts +5 -0
- package/dist/entrypoints/process.d.ts.map +1 -0
- package/dist/entrypoints/process.js +20 -0
- package/dist/entrypoints/tools.js +12 -3
- package/dist/files/boundary.d.ts +19 -1
- package/dist/files/boundary.d.ts.map +1 -1
- package/dist/files/file-io.d.ts +5 -1
- package/dist/files/file-io.d.ts.map +1 -1
- package/dist/files/read.d.ts +13 -0
- package/dist/files/read.d.ts.map +1 -0
- package/dist/index-12m2v2a9.js +98 -0
- package/dist/index-6m6a9ty4.js +285 -0
- package/dist/index-8a9ksm91.js +95 -0
- package/dist/{index-90ppdx3q.js → index-9jb61j5a.js} +1 -1
- package/dist/{index-5vyd0fkb.js → index-ary5s7k0.js} +40 -61
- package/dist/index-c1jmtxa0.js +74 -0
- package/dist/index-ga597cvy.js +184 -0
- package/dist/index-mbnnw3f2.js +25 -0
- package/dist/index-nybn2err.js +213 -0
- package/dist/{index-v4a2fjtk.js → index-sybedc8b.js} +124 -55
- package/dist/internal/abort-race.d.ts +3 -0
- package/dist/internal/abort-race.d.ts.map +1 -0
- package/dist/internal/atomic-file.d.ts +20 -2
- package/dist/internal/atomic-file.d.ts.map +1 -1
- package/dist/internal/atomic-publication.d.ts +19 -0
- package/dist/internal/atomic-publication.d.ts.map +1 -0
- package/dist/internal/exclusive-lock.d.ts +2 -1
- package/dist/internal/exclusive-lock.d.ts.map +1 -1
- package/dist/internal/json-data.d.ts +8 -0
- package/dist/internal/json-data.d.ts.map +1 -0
- package/dist/internal/process-instance.d.ts +46 -9
- package/dist/internal/process-instance.d.ts.map +1 -1
- package/dist/internal/with-exclusive-lock.d.ts +4 -3
- package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
- package/dist/primitives/canonical-json.d.ts +11 -0
- package/dist/primitives/canonical-json.d.ts.map +1 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/process/command-owner.d.ts +13 -0
- package/dist/process/command-owner.d.ts.map +1 -0
- package/dist/process/command.d.ts +3 -0
- package/dist/process/command.d.ts.map +1 -0
- package/dist/process/contract.d.ts +45 -0
- package/dist/process/contract.d.ts.map +1 -0
- package/dist/process/group.d.ts +6 -0
- package/dist/process/group.d.ts.map +1 -0
- package/dist/process/leader-settlement.d.ts +4 -0
- package/dist/process/leader-settlement.d.ts.map +1 -0
- package/dist/tools/transfer/managed-file-error.d.ts.map +1 -1
- package/llms/files.txt +9 -1
- package/llms/primitives.txt +31 -1
- package/llms/process.txt +242 -0
- package/llms/stitchkit.txt +1 -0
- package/llms/tools.txt +18 -0
- package/llms-full.txt +292 -2
- package/llms.txt +6 -4
- package/native/darwin-arm64.node +0 -0
- package/native/darwin-x64.node +0 -0
- package/package.json +5 -1
- package/dist/index-7kxw1awn.js +0 -328
- package/dist/index-aan6hqb9.js +0 -65
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isJsonData
|
|
3
|
+
} from "./index-c1jmtxa0.js";
|
|
4
|
+
import {
|
|
5
|
+
raceAbort
|
|
6
|
+
} from "./index-mbnnw3f2.js";
|
|
7
|
+
|
|
1
8
|
// src/durability/contract.ts
|
|
2
9
|
import { z } from "zod";
|
|
3
10
|
var DURABILITY_STEP_EVENT_KIND2 = "durability/step";
|
|
@@ -93,44 +100,12 @@ class ParkAbortedError2 extends Error {
|
|
|
93
100
|
|
|
94
101
|
// src/durability/json.ts
|
|
95
102
|
import { z as z2 } from "zod";
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return Number.isFinite(value) && !Object.is(value, -0);
|
|
103
|
-
if (typeof value !== "object" || ancestors.has(value))
|
|
104
|
-
return false;
|
|
105
|
-
const array = Array.isArray(value);
|
|
106
|
-
const prototype = Object.getPrototypeOf(value);
|
|
107
|
-
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null)
|
|
108
|
-
return false;
|
|
109
|
-
ancestors.add(value);
|
|
110
|
-
try {
|
|
111
|
-
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
112
|
-
const keys = Reflect.ownKeys(descriptors);
|
|
113
|
-
if (array && keys.length !== value.length + 1)
|
|
114
|
-
return false;
|
|
115
|
-
for (const key of keys) {
|
|
116
|
-
if (array && key === "length")
|
|
117
|
-
continue;
|
|
118
|
-
if (typeof key !== "string")
|
|
119
|
-
return false;
|
|
120
|
-
if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= value.length))
|
|
121
|
-
return false;
|
|
122
|
-
const descriptor = descriptors[key];
|
|
123
|
-
if (!descriptor?.enumerable || !("value" in descriptor))
|
|
124
|
-
return false;
|
|
125
|
-
if (!losslessJson(descriptor.value, ancestors, depth + 1))
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
return true;
|
|
129
|
-
} finally {
|
|
130
|
-
ancestors.delete(value);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
var DurableJsonSchema = z2.custom((value) => losslessJson(value), "Expected lossless JSON data (finite numbers, plain objects and dense arrays; no accessors)");
|
|
103
|
+
var DurableJsonSchema = z2.custom((value) => isJsonData(value, {
|
|
104
|
+
maxDepth: 100,
|
|
105
|
+
maxNodes: Number.MAX_SAFE_INTEGER,
|
|
106
|
+
maxBytes: Number.MAX_SAFE_INTEGER,
|
|
107
|
+
omitObjectUndefined: false
|
|
108
|
+
}), "Expected lossless JSON data (finite numbers, plain objects and dense arrays; no accessors)");
|
|
134
109
|
|
|
135
110
|
// src/durability/ledger.ts
|
|
136
111
|
function stepKey(conversationId, runId, stepName) {
|
|
@@ -292,30 +267,25 @@ async function append(context, payload, name) {
|
|
|
292
267
|
payload: { runId: context.runId, effectName: name, ...payload }
|
|
293
268
|
});
|
|
294
269
|
}
|
|
295
|
-
async function reconcile(context, name, handlers, timeoutMs) {
|
|
270
|
+
async function reconcile(context, name, handlers, timeoutMs, outerSignal) {
|
|
271
|
+
const caller = [context.signal, outerSignal].filter((signal) => signal !== undefined);
|
|
296
272
|
const controller = new AbortController;
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
timer = setTimeout(() => {
|
|
300
|
-
controller.abort(new Error(`reconcile did not finish within ${timeoutMs} ms`));
|
|
301
|
-
resolve("timeout");
|
|
302
|
-
}, timeoutMs);
|
|
303
|
-
});
|
|
273
|
+
const timeout = setTimeout(() => controller.abort(new Error(`reconcile did not finish within ${timeoutMs} ms`)), timeoutMs);
|
|
274
|
+
const signal = AbortSignal.any([...caller, controller.signal]);
|
|
304
275
|
let found;
|
|
305
276
|
try {
|
|
306
|
-
found = await Promise.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
277
|
+
found = await raceAbort(Promise.resolve().then(() => {
|
|
278
|
+
signal.throwIfAborted();
|
|
279
|
+
return handlers.reconcile(signal);
|
|
280
|
+
}), signal);
|
|
281
|
+
signal.throwIfAborted();
|
|
310
282
|
} catch (cause) {
|
|
311
|
-
|
|
283
|
+
if (caller.some((signal) => signal.aborted))
|
|
284
|
+
throw new StepAbortedError2(name);
|
|
285
|
+
throw new EffectUnresolvedError2(name, controller.signal.aborted ? "reconcile-timeout" : "reconcile-failed", { cause });
|
|
312
286
|
} finally {
|
|
313
|
-
clearTimeout(
|
|
314
|
-
|
|
315
|
-
if (found === "timeout") {
|
|
316
|
-
throw new EffectUnresolvedError2(name, "reconcile-timeout", {
|
|
317
|
-
cause: controller.signal.reason
|
|
318
|
-
});
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
controller.abort(new Error("Reconciliation completed"));
|
|
319
289
|
}
|
|
320
290
|
if (found === null) {
|
|
321
291
|
await append(context, { phase: "uncertain" }, name);
|
|
@@ -327,8 +297,8 @@ async function reconcile(context, name, handlers, timeoutMs) {
|
|
|
327
297
|
}
|
|
328
298
|
async function executeEffect(context, name, handlers, options) {
|
|
329
299
|
const timeoutMs = options?.reconcileTimeoutMs ?? DEFAULT_RECONCILE_TIMEOUT_MS;
|
|
330
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
331
|
-
throw new TypeError(`reconcileTimeoutMs must be a positive finite number, got ${timeoutMs}`);
|
|
300
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
301
|
+
throw new TypeError(`reconcileTimeoutMs must be a positive finite number at most 2147483647, got ${timeoutMs}`);
|
|
332
302
|
}
|
|
333
303
|
const key = effectKey(context.conversationId, context.runId, name);
|
|
334
304
|
const recorded = (await context.readLedger()).effects.get(key);
|
|
@@ -340,7 +310,7 @@ async function executeEffect(context, name, handlers, options) {
|
|
|
340
310
|
if (options?.signal?.aborted || context.signal?.aborted)
|
|
341
311
|
throw new StepAbortedError2(name);
|
|
342
312
|
if (recorded)
|
|
343
|
-
return reconcile(context, name, handlers, timeoutMs);
|
|
313
|
+
return reconcile(context, name, handlers, timeoutMs, options?.signal);
|
|
344
314
|
await append(context, { phase: "intent" }, name);
|
|
345
315
|
let proof;
|
|
346
316
|
try {
|
|
@@ -581,7 +551,16 @@ function createLocalStepDurability2(options) {
|
|
|
581
551
|
const key = effectKey(conversationId, runId, name);
|
|
582
552
|
const existing = inFlightEffects.get(key);
|
|
583
553
|
if (existing) {
|
|
584
|
-
|
|
554
|
+
const signals = [options.signal, runOptions?.signal].filter((signal) => signal !== undefined);
|
|
555
|
+
const result = detachedResult(existing);
|
|
556
|
+
if (signals.length === 0)
|
|
557
|
+
return result;
|
|
558
|
+
const signal = AbortSignal.any(signals);
|
|
559
|
+
return raceAbort(result, signal).catch((error) => {
|
|
560
|
+
if (signal.aborted)
|
|
561
|
+
throw new StepAbortedError2(name);
|
|
562
|
+
throw error;
|
|
563
|
+
});
|
|
585
564
|
}
|
|
586
565
|
const pending = executeEffect({ store, conversationId, runId, signal: options.signal, readLedger }, name, handlers, runOptions);
|
|
587
566
|
inFlightEffects.set(key, pending);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/internal/json-data.ts
|
|
2
|
+
function isJsonData(value, limits) {
|
|
3
|
+
const ancestors = new Set;
|
|
4
|
+
let nodes = 0;
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
const charge = (amount) => {
|
|
7
|
+
bytes += amount;
|
|
8
|
+
return bytes <= limits.maxBytes;
|
|
9
|
+
};
|
|
10
|
+
const textSize = (text) => {
|
|
11
|
+
if (text.length > limits.maxBytes)
|
|
12
|
+
return limits.maxBytes + 1;
|
|
13
|
+
return new TextEncoder().encode(JSON.stringify(text)).byteLength;
|
|
14
|
+
};
|
|
15
|
+
const visit = (item, depth) => {
|
|
16
|
+
if (++nodes > limits.maxNodes || depth > limits.maxDepth)
|
|
17
|
+
return false;
|
|
18
|
+
if (item === null)
|
|
19
|
+
return charge(4);
|
|
20
|
+
if (typeof item === "string")
|
|
21
|
+
return charge(textSize(item));
|
|
22
|
+
if (typeof item === "boolean")
|
|
23
|
+
return charge(item ? 4 : 5);
|
|
24
|
+
if (typeof item === "number")
|
|
25
|
+
return Number.isFinite(item) && !Object.is(item, -0) && charge(String(item).length);
|
|
26
|
+
if (typeof item !== "object" || ancestors.has(item))
|
|
27
|
+
return false;
|
|
28
|
+
const array = Array.isArray(item);
|
|
29
|
+
const prototype = Object.getPrototypeOf(item);
|
|
30
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null)
|
|
31
|
+
return false;
|
|
32
|
+
if (array && item.length > limits.maxNodes - nodes)
|
|
33
|
+
return false;
|
|
34
|
+
const keys = Reflect.ownKeys(item);
|
|
35
|
+
if (keys.length > limits.maxNodes - nodes + (array ? 1 : 0))
|
|
36
|
+
return false;
|
|
37
|
+
if (array && keys.length !== item.length + 1)
|
|
38
|
+
return false;
|
|
39
|
+
ancestors.add(item);
|
|
40
|
+
try {
|
|
41
|
+
if (!charge(2))
|
|
42
|
+
return false;
|
|
43
|
+
let members = 0;
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
if (array && key === "length")
|
|
46
|
+
continue;
|
|
47
|
+
if (typeof key !== "string")
|
|
48
|
+
return false;
|
|
49
|
+
if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length))
|
|
50
|
+
return false;
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
52
|
+
if (!descriptor?.enumerable || !("value" in descriptor))
|
|
53
|
+
return false;
|
|
54
|
+
if (!array && limits.omitObjectUndefined && descriptor.value === undefined) {
|
|
55
|
+
if (++nodes > limits.maxNodes)
|
|
56
|
+
return false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (members++ > 0 && !charge(1))
|
|
60
|
+
return false;
|
|
61
|
+
if (!array && !charge(textSize(key) + 1))
|
|
62
|
+
return false;
|
|
63
|
+
if (!visit(descriptor.value, depth + 1))
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
} finally {
|
|
68
|
+
ancestors.delete(item);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return visit(value, 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { isJsonData };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
machineIdentity,
|
|
3
|
+
ProcessInstanceSchema2,
|
|
4
|
+
readProcessInstance,
|
|
5
|
+
probeProcessOwner2
|
|
6
|
+
} from "./index-nybn2err.js";
|
|
7
|
+
import {
|
|
8
|
+
isRecord
|
|
9
|
+
} from "./index-9jfdm5tg.js";
|
|
10
|
+
|
|
11
|
+
// src/internal/exclusive-lock.ts
|
|
12
|
+
import { constants } from "node:fs";
|
|
13
|
+
import { lstat, open, unlink } from "node:fs/promises";
|
|
14
|
+
import { hostname } from "node:os";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var ExclusiveLockOwnerSchema = z.object({
|
|
17
|
+
pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
18
|
+
host: z.string().min(1),
|
|
19
|
+
acquiredAt: z.string(),
|
|
20
|
+
machine: z.string().min(1).optional().catch(undefined),
|
|
21
|
+
process: ProcessInstanceSchema2.nullable().optional().catch(null)
|
|
22
|
+
});
|
|
23
|
+
function isCode(error, code) {
|
|
24
|
+
return isRecord(error) && error.code === code;
|
|
25
|
+
}
|
|
26
|
+
function readOwner(text) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(text);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const owner = ExclusiveLockOwnerSchema.safeParse(parsed);
|
|
34
|
+
return owner.success ? owner.data : undefined;
|
|
35
|
+
}
|
|
36
|
+
function attribute(owner, identity) {
|
|
37
|
+
if (identity !== null && owner.machine !== undefined) {
|
|
38
|
+
return owner.machine === identity ? "this-machine" : "another-machine";
|
|
39
|
+
}
|
|
40
|
+
return owner.host === hostname() ? "this-machine" : "unattributable";
|
|
41
|
+
}
|
|
42
|
+
async function diagnose(owner, declaredIdentity) {
|
|
43
|
+
if (!owner)
|
|
44
|
+
return { attribution: "unattributable", liveness: "not-probed", owner: null };
|
|
45
|
+
const attribution = attribute(owner, await machineIdentity(declaredIdentity));
|
|
46
|
+
if (attribution !== "this-machine")
|
|
47
|
+
return { attribution, liveness: "not-probed", owner };
|
|
48
|
+
return { attribution, ...await probeProcessOwner2(owner.pid, owner.process), owner };
|
|
49
|
+
}
|
|
50
|
+
async function readLockFile(path, onFailure) {
|
|
51
|
+
let handle;
|
|
52
|
+
try {
|
|
53
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
54
|
+
const info = await handle.stat();
|
|
55
|
+
return {
|
|
56
|
+
owner: readOwner(await handle.readFile("utf8")),
|
|
57
|
+
ino: info.ino,
|
|
58
|
+
mtimeMs: info.mtimeMs
|
|
59
|
+
};
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
if (!isCode(cause, "ENOENT"))
|
|
62
|
+
onFailure?.(cause);
|
|
63
|
+
return;
|
|
64
|
+
} finally {
|
|
65
|
+
await handle?.close().catch(() => {
|
|
66
|
+
return;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function unlinkIfSame(path, ino) {
|
|
71
|
+
try {
|
|
72
|
+
if ((await lstat(path)).ino !== ino)
|
|
73
|
+
return;
|
|
74
|
+
await unlink(path);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isCode(error, "ENOENT"))
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function createOwned(path, options, reclaimed) {
|
|
81
|
+
const identity = await machineIdentity(options.machineIdentity);
|
|
82
|
+
const instance = await readProcessInstance(process.pid);
|
|
83
|
+
const handle = await open(path, "wx", options.mode);
|
|
84
|
+
const owner = {
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
process: instance,
|
|
87
|
+
host: hostname(),
|
|
88
|
+
acquiredAt: new Date().toISOString(),
|
|
89
|
+
...identity !== null && { machine: identity }
|
|
90
|
+
};
|
|
91
|
+
let ino;
|
|
92
|
+
try {
|
|
93
|
+
ino = (await handle.stat()).ino;
|
|
94
|
+
await handle.chmod(options.mode);
|
|
95
|
+
await handle.writeFile(`${JSON.stringify(owner)}
|
|
96
|
+
`, "utf8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
await handle.close().catch(() => {
|
|
99
|
+
return;
|
|
100
|
+
});
|
|
101
|
+
if (ino !== undefined)
|
|
102
|
+
await unlinkIfSame(path, ino).catch(() => {
|
|
103
|
+
return;
|
|
104
|
+
});
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const ownedInode = ino;
|
|
108
|
+
let released = false;
|
|
109
|
+
return {
|
|
110
|
+
path,
|
|
111
|
+
owner,
|
|
112
|
+
reclaimed,
|
|
113
|
+
async release() {
|
|
114
|
+
if (released)
|
|
115
|
+
return;
|
|
116
|
+
released = true;
|
|
117
|
+
try {
|
|
118
|
+
await handle.close();
|
|
119
|
+
} finally {
|
|
120
|
+
await unlinkIfSame(path, ownedInode);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function reclaimable(state, diagnosis, graceMs) {
|
|
126
|
+
if (state.owner)
|
|
127
|
+
return diagnosis.liveness === "gone";
|
|
128
|
+
return typeof graceMs === "number" && Date.now() - state.mtimeMs >= graceMs;
|
|
129
|
+
}
|
|
130
|
+
async function reclaimUnderGuard(path, options) {
|
|
131
|
+
const guardPath = `${path}.reclaim`;
|
|
132
|
+
const acquired = await attemptExclusiveLock(guardPath, {
|
|
133
|
+
...options,
|
|
134
|
+
ownerlessGraceMs: options.ownerlessGraceMs === undefined ? 5000 : options.ownerlessGraceMs
|
|
135
|
+
});
|
|
136
|
+
if (!("held" in acquired))
|
|
137
|
+
return;
|
|
138
|
+
const guard = acquired.held;
|
|
139
|
+
try {
|
|
140
|
+
const state = await readLockFile(path);
|
|
141
|
+
if (state) {
|
|
142
|
+
const diagnosis = await diagnose(state.owner, options.machineIdentity);
|
|
143
|
+
if (!reclaimable(state, diagnosis, options.ownerlessGraceMs))
|
|
144
|
+
return "refused";
|
|
145
|
+
await unlinkIfSame(path, state.ino);
|
|
146
|
+
}
|
|
147
|
+
return await createOwned(path, options, state !== undefined).catch((error) => {
|
|
148
|
+
if (isCode(error, "EEXIST"))
|
|
149
|
+
return "refused";
|
|
150
|
+
throw error;
|
|
151
|
+
});
|
|
152
|
+
} finally {
|
|
153
|
+
await guard.release().catch(() => {
|
|
154
|
+
return;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function attemptExclusiveLock(path, options) {
|
|
159
|
+
try {
|
|
160
|
+
return { held: await createOwned(path, options, false) };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (!isCode(error, "EEXIST"))
|
|
163
|
+
throw error;
|
|
164
|
+
if (!options.reclaim)
|
|
165
|
+
return { error };
|
|
166
|
+
let readFailure;
|
|
167
|
+
const state = await readLockFile(path, (cause) => {
|
|
168
|
+
readFailure = cause;
|
|
169
|
+
});
|
|
170
|
+
const diagnosis = {
|
|
171
|
+
...await diagnose(state?.owner, options.machineIdentity),
|
|
172
|
+
...readFailure !== undefined && { cause: readFailure }
|
|
173
|
+
};
|
|
174
|
+
if (!state || !reclaimable(state, diagnosis, options.ownerlessGraceMs)) {
|
|
175
|
+
return { error, diagnosis };
|
|
176
|
+
}
|
|
177
|
+
const reclaimed = await reclaimUnderGuard(path, options);
|
|
178
|
+
if (reclaimed !== undefined && reclaimed !== "refused")
|
|
179
|
+
return { held: reclaimed };
|
|
180
|
+
return { error, diagnosis };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { attemptExclusiveLock };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/internal/abort-race.ts
|
|
2
|
+
function raceAbort(operation, signal) {
|
|
3
|
+
if (signal.aborted) {
|
|
4
|
+
operation.catch(() => {
|
|
5
|
+
return;
|
|
6
|
+
});
|
|
7
|
+
return Promise.reject(signal.reason);
|
|
8
|
+
}
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const abort = () => {
|
|
11
|
+
signal.removeEventListener("abort", abort);
|
|
12
|
+
reject(signal.reason);
|
|
13
|
+
};
|
|
14
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
15
|
+
operation.then((value) => {
|
|
16
|
+
signal.removeEventListener("abort", abort);
|
|
17
|
+
resolve(value);
|
|
18
|
+
}, (error) => {
|
|
19
|
+
signal.removeEventListener("abort", abort);
|
|
20
|
+
reject(error);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { raceAbort };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadDarwinBinding
|
|
3
|
+
} from "./index-d4pa2bgs.js";
|
|
4
|
+
import {
|
|
5
|
+
isRecord
|
|
6
|
+
} from "./index-9jfdm5tg.js";
|
|
7
|
+
|
|
8
|
+
// src/internal/process-instance.ts
|
|
9
|
+
import { readFile as readFile2, readlink } from "node:fs/promises";
|
|
10
|
+
import { platform as platform2 } from "node:os";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
// src/internal/process-identity.ts
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { platform } from "node:os";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
var detectedIdentity;
|
|
19
|
+
async function firstReadableLine(paths) {
|
|
20
|
+
for (const path of paths) {
|
|
21
|
+
try {
|
|
22
|
+
const text = (await readFile(path, "utf8")).trim();
|
|
23
|
+
if (text.length > 0)
|
|
24
|
+
return text;
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
var run = promisify(execFile);
|
|
30
|
+
function parsePlatformUuid(dump) {
|
|
31
|
+
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
|
|
32
|
+
}
|
|
33
|
+
async function readDarwinPlatformUuid(command = "ioreg") {
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
36
|
+
timeout: 2000,
|
|
37
|
+
maxBuffer: 1 << 20,
|
|
38
|
+
encoding: "utf8"
|
|
39
|
+
});
|
|
40
|
+
return parsePlatformUuid(stdout);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function machineIdentity(declared) {
|
|
46
|
+
if (declared !== undefined)
|
|
47
|
+
return declared;
|
|
48
|
+
if (detectedIdentity !== undefined)
|
|
49
|
+
return detectedIdentity;
|
|
50
|
+
detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
|
|
51
|
+
return detectedIdentity;
|
|
52
|
+
}
|
|
53
|
+
async function probeLiveness(pid, reach = (target) => process.kill(target, 0)) {
|
|
54
|
+
try {
|
|
55
|
+
reach(pid);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
|
|
58
|
+
}
|
|
59
|
+
return await isZombieProcess(pid) ? "gone" : "alive";
|
|
60
|
+
}
|
|
61
|
+
async function isZombieProcess(pid, seam = {}) {
|
|
62
|
+
const { procRoot = "/proc", psCommand = "ps" } = seam;
|
|
63
|
+
try {
|
|
64
|
+
const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
|
|
65
|
+
return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
|
|
66
|
+
} catch {}
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
|
|
69
|
+
timeout: 2000,
|
|
70
|
+
maxBuffer: 1 << 16,
|
|
71
|
+
encoding: "utf8"
|
|
72
|
+
});
|
|
73
|
+
return stdout.trim().startsWith("Z");
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/internal/process-instance.ts
|
|
80
|
+
var ProcessInstanceSchema2 = z.object({
|
|
81
|
+
platform: z.enum(["linux", "darwin"]),
|
|
82
|
+
bootId: z.string().min(1),
|
|
83
|
+
namespace: z.string().min(1),
|
|
84
|
+
startId: z.string().regex(/^\d+$/)
|
|
85
|
+
}).strict();
|
|
86
|
+
var ProcessInstanceObservationSchema = z.discriminatedUnion("state", [
|
|
87
|
+
z.object({ state: z.literal("observed"), instance: ProcessInstanceSchema2 }),
|
|
88
|
+
z.object({ state: z.literal("unavailable"), cause: z.unknown() })
|
|
89
|
+
]);
|
|
90
|
+
async function observeProcessInstance2(pid) {
|
|
91
|
+
z.number().int().positive().max(2147483647).parse(pid);
|
|
92
|
+
return observeProcessInstanceAt(pid, "/proc");
|
|
93
|
+
}
|
|
94
|
+
function linuxProcessStart(stat, pid) {
|
|
95
|
+
if (!stat.startsWith(`${pid} (`))
|
|
96
|
+
return null;
|
|
97
|
+
const end = stat.lastIndexOf(")");
|
|
98
|
+
if (end < 0)
|
|
99
|
+
return null;
|
|
100
|
+
const start = stat.slice(end + 1).trim().split(/\s+/)[19];
|
|
101
|
+
return start !== undefined && /^\d+$/.test(start) ? start : null;
|
|
102
|
+
}
|
|
103
|
+
async function readProcessInstance(pid, procRoot = "/proc") {
|
|
104
|
+
const result = await observeProcessInstanceAt(pid, procRoot);
|
|
105
|
+
return result.state === "observed" ? result.instance : null;
|
|
106
|
+
}
|
|
107
|
+
async function observeProcessInstanceAt(pid, procRoot) {
|
|
108
|
+
try {
|
|
109
|
+
if (platform2() === "darwin") {
|
|
110
|
+
return {
|
|
111
|
+
state: "observed",
|
|
112
|
+
instance: ProcessInstanceSchema2.parse(loadDarwinBinding().processIdentity(pid))
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (platform2() !== "linux")
|
|
116
|
+
throw new Error("Process lifetime observation requires Linux or Darwin");
|
|
117
|
+
const [bootId, namespace, ownStatus, timeNamespace, ownStat, stat] = await Promise.all([
|
|
118
|
+
readFile2(`${procRoot}/sys/kernel/random/boot_id`, "utf8"),
|
|
119
|
+
readlink(`${procRoot}/self/ns/pid`),
|
|
120
|
+
readFile2(`${procRoot}/self/status`, "utf8"),
|
|
121
|
+
readlink(`${procRoot}/self/ns/time`).catch((error) => {
|
|
122
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
123
|
+
return "";
|
|
124
|
+
throw error;
|
|
125
|
+
}),
|
|
126
|
+
readFile2(`${procRoot}/self/stat`, "utf8"),
|
|
127
|
+
readFile2(`${procRoot}/${pid}/stat`, "utf8")
|
|
128
|
+
]);
|
|
129
|
+
const namespacePids = ownStatus.match(/^NStgid:[ \t]*(.*)$/m)?.[1]?.trim().split(/\s+/);
|
|
130
|
+
if (namespacePids?.length !== 1 || namespacePids[0] !== String(process.pid) || linuxProcessStart(ownStat, process.pid) === null)
|
|
131
|
+
throw new Error("Process namespace evidence is partial or belongs to a foreign proc mount");
|
|
132
|
+
const startId = linuxProcessStart(stat, pid);
|
|
133
|
+
if (startId === null || !bootId.trim())
|
|
134
|
+
throw new Error("Process boot/start evidence is incomplete");
|
|
135
|
+
return {
|
|
136
|
+
state: "observed",
|
|
137
|
+
instance: {
|
|
138
|
+
platform: "linux",
|
|
139
|
+
bootId: bootId.trim(),
|
|
140
|
+
namespace: `${namespace};${timeNamespace}`,
|
|
141
|
+
startId
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
return { state: "unavailable", cause };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var ProcessOwnerEvidenceSchema = z.object({
|
|
149
|
+
liveness: z.enum(["alive", "gone", "not-probed"]),
|
|
150
|
+
identity: z.enum([
|
|
151
|
+
"matched",
|
|
152
|
+
"different-boot",
|
|
153
|
+
"reused-pid",
|
|
154
|
+
"pid-gone",
|
|
155
|
+
"legacy",
|
|
156
|
+
"unavailable"
|
|
157
|
+
]),
|
|
158
|
+
cause: z.unknown().optional()
|
|
159
|
+
});
|
|
160
|
+
async function probeProcessOwner2(pid, recorded) {
|
|
161
|
+
z.number().int().positive().max(2147483647).parse(pid);
|
|
162
|
+
if (recorded != null)
|
|
163
|
+
ProcessInstanceSchema2.parse(recorded);
|
|
164
|
+
return probeProcessOwnerWith(pid, recorded, {
|
|
165
|
+
read: readProcessInstance,
|
|
166
|
+
observe: observeProcessInstance2,
|
|
167
|
+
liveness: probeLiveness
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function probeProcessOwnerWith(pid, recorded, probes = { read: readProcessInstance, liveness: probeLiveness }) {
|
|
171
|
+
if (recorded === undefined)
|
|
172
|
+
return { liveness: await probes.liveness(pid), identity: "legacy" };
|
|
173
|
+
const unavailable = (cause) => ({
|
|
174
|
+
liveness: "not-probed",
|
|
175
|
+
identity: "unavailable",
|
|
176
|
+
cause
|
|
177
|
+
});
|
|
178
|
+
if (recorded === null)
|
|
179
|
+
return unavailable(new Error("Recorded process lifetime is unavailable"));
|
|
180
|
+
const observe = async (target) => {
|
|
181
|
+
if (probes.observe)
|
|
182
|
+
return probes.observe(target);
|
|
183
|
+
const instance = await probes.read(target);
|
|
184
|
+
return instance ? { state: "observed", instance } : {
|
|
185
|
+
state: "unavailable",
|
|
186
|
+
cause: new Error("Process lifetime reader returned no evidence")
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
const own = await observe(process.pid);
|
|
190
|
+
if (own.state === "unavailable")
|
|
191
|
+
return unavailable(own.cause);
|
|
192
|
+
const current = own.instance;
|
|
193
|
+
if (recorded.platform !== current.platform)
|
|
194
|
+
return unavailable(new Error("Process platforms differ"));
|
|
195
|
+
if (recorded.bootId !== current.bootId)
|
|
196
|
+
return { liveness: "gone", identity: "different-boot" };
|
|
197
|
+
if (recorded.namespace !== current.namespace)
|
|
198
|
+
return unavailable(new Error("Process namespaces differ"));
|
|
199
|
+
const liveness = await probes.liveness(pid);
|
|
200
|
+
if (liveness === "gone")
|
|
201
|
+
return { liveness, identity: "pid-gone" };
|
|
202
|
+
const observation = await observe(pid);
|
|
203
|
+
if (observation.state === "unavailable")
|
|
204
|
+
return unavailable(observation.cause);
|
|
205
|
+
const observed = observation.instance;
|
|
206
|
+
if (!observed || observed.bootId !== current.bootId || observed.namespace !== current.namespace)
|
|
207
|
+
return unavailable(new Error("Observed boot or namespace changed during probe"));
|
|
208
|
+
if (recorded.startId !== observed.startId)
|
|
209
|
+
return { liveness: "gone", identity: "reused-pid" };
|
|
210
|
+
return { liveness: "alive", identity: "matched" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { machineIdentity, ProcessInstanceSchema2, observeProcessInstance2, readProcessInstance, probeProcessOwner2 };
|