@forgeax/engine-tool-runtime 0.1.2
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/LICENSE +202 -0
- package/README.md +96 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/artifacts.d.ts +76 -0
- package/dist/capability.d.ts +85 -0
- package/dist/carrier.d.ts +68 -0
- package/dist/errors.d.ts +16 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.mjs +1275 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lease.d.ts +16 -0
- package/dist/migration.d.ts +44 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/snapshot.d.ts +3 -0
- package/dist/timing.d.ts +16 -0
- package/dist/transport.d.ts +69 -0
- package/dist/types.d.ts +314 -0
- package/package.json +53 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
function invalidArgsError(message, value) {
|
|
3
|
+
return {
|
|
4
|
+
code: "tool-invalid-args",
|
|
5
|
+
expected: "arguments accepted by the contribution argsSchema",
|
|
6
|
+
hint: "Read the descriptor schema and retry with typed arguments.",
|
|
7
|
+
detail: { message, value }
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function capabilityUnavailableError(capability, realm) {
|
|
11
|
+
return {
|
|
12
|
+
code: "tool-capability-unavailable",
|
|
13
|
+
expected: `capability ${capability} in realm ${realm}`,
|
|
14
|
+
hint: "Inspect the capability matrix and select an available realm or path.",
|
|
15
|
+
detail: { capability, realm }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function snapshotStaleError(expectedDigest, actualDigest) {
|
|
19
|
+
return {
|
|
20
|
+
code: "tool-snapshot-stale",
|
|
21
|
+
expected: "the supplied snapshot to match the current authority",
|
|
22
|
+
hint: "Refresh the authority snapshot and retry the write.",
|
|
23
|
+
detail: { expectedDigest, actualDigest }
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function domainFailureError(code, expected = "the producer operation to succeed", hint = "Inspect detail and repair the owning producer before retrying.", payload) {
|
|
27
|
+
return {
|
|
28
|
+
code: "tool-domain-failed",
|
|
29
|
+
expected,
|
|
30
|
+
hint,
|
|
31
|
+
detail: { code, ...payload === void 0 ? {} : { payload } }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function artifactIncompleteError(missing, runId) {
|
|
35
|
+
return {
|
|
36
|
+
code: "tool-artifact-incomplete",
|
|
37
|
+
expected: "the requested evidence artifacts to be produced by their owners",
|
|
38
|
+
hint: "Request only supported evidence and inspect the artifact manifest before retrying.",
|
|
39
|
+
detail: { missing: [...missing], runId }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function cancellationError(reason) {
|
|
43
|
+
return {
|
|
44
|
+
code: "tool-run-cancelled",
|
|
45
|
+
expected: "the tool run not to be cancelled",
|
|
46
|
+
hint: "Retry the command after resolving the cancellation source.",
|
|
47
|
+
detail: { reason }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function timeoutError(deadlineMs) {
|
|
51
|
+
return {
|
|
52
|
+
code: "tool-run-timeout",
|
|
53
|
+
expected: `the tool run to complete within ${deadlineMs}ms`,
|
|
54
|
+
hint: "Increase the deadline only when the operation is expected to be bounded.",
|
|
55
|
+
detail: { deadlineMs }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function disconnectedError(transport) {
|
|
59
|
+
return {
|
|
60
|
+
code: "tool-run-disconnected",
|
|
61
|
+
expected: "the tool transport to remain connected",
|
|
62
|
+
hint: "Reconnect the transport and retry from the last serialized snapshot.",
|
|
63
|
+
detail: { transport }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function terminalError(runId, outcome) {
|
|
67
|
+
return {
|
|
68
|
+
code: "tool-run-terminal",
|
|
69
|
+
expected: "a non-terminal ToolRun",
|
|
70
|
+
hint: "Use the existing terminal and do not attach another executor.",
|
|
71
|
+
detail: { runId, outcome }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function cleanupError(runId, message) {
|
|
75
|
+
return {
|
|
76
|
+
code: "tool-cleanup-failed",
|
|
77
|
+
expected: "all ToolRun cleanup callbacks to complete",
|
|
78
|
+
hint: "Inspect the cleanup detail and release the owning resource.",
|
|
79
|
+
detail: { runId, message }
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function artifactManifestError(expected, hint, detail) {
|
|
83
|
+
return { code: "tool-artifact-manifest-invalid", expected, hint, detail };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/artifacts.ts
|
|
87
|
+
function createArtifactManifest(manifest) {
|
|
88
|
+
const result = validateArtifactManifest(manifest, []);
|
|
89
|
+
if (!result.ok) throw new TypeError("artifact manifest is invalid");
|
|
90
|
+
return {
|
|
91
|
+
schemaVersion: "1.0.0",
|
|
92
|
+
identity: { ...manifest.identity },
|
|
93
|
+
artifacts: manifest.artifacts.map((artifact) => ({ ...artifact }))
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function validateArtifactManifest(manifest, required) {
|
|
97
|
+
const fail = (reason, expected) => ({
|
|
98
|
+
ok: false,
|
|
99
|
+
error: artifactManifestError(
|
|
100
|
+
expected,
|
|
101
|
+
"regenerate the complete manifest from the owning producers",
|
|
102
|
+
{
|
|
103
|
+
reason,
|
|
104
|
+
...typeof manifest?.identity?.runId === "string" ? { runId: manifest.identity.runId } : {}
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
});
|
|
108
|
+
if (manifest?.schemaVersion !== "1.0.0")
|
|
109
|
+
return fail("unsupported schemaVersion", 'schemaVersion === "1.0.0"');
|
|
110
|
+
const identity = manifest.identity;
|
|
111
|
+
if (identity === void 0 || identity.runId.length === 0 || identity.snapshotDigest.length === 0 || identity.stepId.length === 0 || identity.captureId.length === 0 || !Number.isSafeInteger(identity.frameId) || identity.frameId < 0)
|
|
112
|
+
return fail(
|
|
113
|
+
"identity is incomplete",
|
|
114
|
+
"runId/snapshotDigest/stepId/captureId and non-negative frameId"
|
|
115
|
+
);
|
|
116
|
+
const seen = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const artifact of manifest.artifacts) {
|
|
118
|
+
if (artifact.owner.length === 0 || artifact.uri.length === 0 || artifact.digest.length === 0 || !Number.isSafeInteger(artifact.byteLength) || artifact.byteLength < 0)
|
|
119
|
+
return fail(
|
|
120
|
+
`invalid ${artifact.kind} artifact entry`,
|
|
121
|
+
"owner/uri/digest and byteLength are complete"
|
|
122
|
+
);
|
|
123
|
+
if (seen.has(artifact.kind))
|
|
124
|
+
return fail(
|
|
125
|
+
`duplicate artifact kind '${artifact.kind}'`,
|
|
126
|
+
"one owner entry per evidence kind"
|
|
127
|
+
);
|
|
128
|
+
seen.add(artifact.kind);
|
|
129
|
+
}
|
|
130
|
+
for (const kind of required)
|
|
131
|
+
if (!seen.has(kind))
|
|
132
|
+
return fail(
|
|
133
|
+
`missing artifact kind '${kind}'`,
|
|
134
|
+
`manifest includes requested '${kind}' evidence`
|
|
135
|
+
);
|
|
136
|
+
return { ok: true, value: manifest };
|
|
137
|
+
}
|
|
138
|
+
var PREVIEW_ROLE_KINDS = {
|
|
139
|
+
report: "report",
|
|
140
|
+
"rhi-tape": "rhi-tape",
|
|
141
|
+
capture: "png",
|
|
142
|
+
"fresh-replay": "png",
|
|
143
|
+
"profile-capture": "profile-capture",
|
|
144
|
+
"contact-sheet": "contact-sheet"
|
|
145
|
+
};
|
|
146
|
+
function createPreviewArtifactManifest(manifest) {
|
|
147
|
+
const result = validatePreviewArtifactManifest(manifest, []);
|
|
148
|
+
if (!result.ok) throw new TypeError("preview artifact manifest is invalid");
|
|
149
|
+
return {
|
|
150
|
+
schemaVersion: "2.0.0",
|
|
151
|
+
identity: { ...manifest.identity },
|
|
152
|
+
artifacts: manifest.artifacts.map((artifact) => ({
|
|
153
|
+
...artifact,
|
|
154
|
+
derivedFrom: [...artifact.derivedFrom]
|
|
155
|
+
}))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function validatePreviewArtifactManifest(manifest, requiredRoles) {
|
|
159
|
+
const fail = (reason) => ({
|
|
160
|
+
ok: false,
|
|
161
|
+
error: artifactManifestError(
|
|
162
|
+
"schemaVersion 2.0.0 with one complete identity and role per artifact",
|
|
163
|
+
"regenerate the staged report and evidence from one lexical ToolRun",
|
|
164
|
+
{
|
|
165
|
+
reason,
|
|
166
|
+
...typeof manifest?.identity?.runId === "string" ? { runId: manifest.identity.runId } : {}
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
});
|
|
170
|
+
if (manifest?.schemaVersion !== "2.0.0") return fail("v1 manifest or unsupported schemaVersion");
|
|
171
|
+
const identity = manifest.identity;
|
|
172
|
+
if (identity === void 0 || [
|
|
173
|
+
identity.runId,
|
|
174
|
+
identity.snapshotDigest,
|
|
175
|
+
identity.subjectDigest,
|
|
176
|
+
identity.presentationDigest,
|
|
177
|
+
identity.captureId
|
|
178
|
+
].some((value) => typeof value !== "string" || value.length === 0) || !Number.isSafeInteger(identity.frameId) || identity.frameId < 0) {
|
|
179
|
+
return fail("identity is incomplete or stale");
|
|
180
|
+
}
|
|
181
|
+
const seenRoles = /* @__PURE__ */ new Set();
|
|
182
|
+
const digests = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const artifact of manifest.artifacts) {
|
|
184
|
+
if (seenRoles.has(artifact.role)) return fail(`duplicate role '${artifact.role}'`);
|
|
185
|
+
if (PREVIEW_ROLE_KINDS[artifact.role] !== artifact.kind) {
|
|
186
|
+
return fail(`role '${artifact.role}' does not match kind '${artifact.kind}'`);
|
|
187
|
+
}
|
|
188
|
+
if (artifact.owner.length === 0 || artifact.uri.length === 0 || artifact.digest.length === 0 || artifact.mediaType.length === 0 || !Number.isSafeInteger(artifact.byteLength) || artifact.byteLength < 0 || artifact.derivedFrom.some((digest) => digest.length === 0)) {
|
|
189
|
+
return fail(`incomplete '${artifact.role}' artifact`);
|
|
190
|
+
}
|
|
191
|
+
seenRoles.add(artifact.role);
|
|
192
|
+
digests.add(artifact.digest);
|
|
193
|
+
}
|
|
194
|
+
for (const artifact of manifest.artifacts) {
|
|
195
|
+
if (artifact.derivedFrom.some((digest) => !digests.has(digest))) {
|
|
196
|
+
return fail(`'${artifact.role}' derivedFrom references an unpublished artifact`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const role of requiredRoles) {
|
|
200
|
+
if (!seenRoles.has(role)) return fail(`missing required role '${role}'`);
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, value: manifest };
|
|
203
|
+
}
|
|
204
|
+
function isSerializableValue(value, seen = /* @__PURE__ */ new Set()) {
|
|
205
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
206
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
207
|
+
if (typeof value !== "object") return false;
|
|
208
|
+
if (seen.has(value)) return false;
|
|
209
|
+
seen.add(value);
|
|
210
|
+
if (Array.isArray(value)) return value.every((entry) => isSerializableValue(entry, seen));
|
|
211
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return false;
|
|
212
|
+
return Object.entries(value).every(
|
|
213
|
+
([key, entry]) => typeof key === "string" && isSerializableValue(entry, seen)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
function createArtifactRef(input) {
|
|
217
|
+
if (input.digest.length === 0) throw new TypeError("ArtifactRef digest must not be empty");
|
|
218
|
+
if (input.sizeBytes !== void 0 && (!Number.isSafeInteger(input.sizeBytes) || input.sizeBytes < 0)) {
|
|
219
|
+
throw new TypeError("ArtifactRef sizeBytes must be a non-negative safe integer");
|
|
220
|
+
}
|
|
221
|
+
return { ...input };
|
|
222
|
+
}
|
|
223
|
+
function createSnapshotRef(input) {
|
|
224
|
+
if (!Number.isSafeInteger(input.revision) || input.revision < 0) {
|
|
225
|
+
throw new TypeError("SnapshotRef revision must be a non-negative safe integer");
|
|
226
|
+
}
|
|
227
|
+
if (input.digest.length === 0) throw new TypeError("SnapshotRef digest must not be empty");
|
|
228
|
+
return { revision: input.revision, digest: input.digest };
|
|
229
|
+
}
|
|
230
|
+
function validateArtifactRefs(value) {
|
|
231
|
+
return Array.isArray(value) && value.every(
|
|
232
|
+
(entry) => typeof entry === "object" && entry !== null && typeof Reflect.get(entry, "kind") === "string" && typeof Reflect.get(entry, "digest") === "string" && (Reflect.get(entry, "uri") === void 0 || typeof Reflect.get(entry, "uri") === "string")
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/capability.ts
|
|
237
|
+
var capabilityIdPattern = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
|
|
238
|
+
function defineToolCapability(id) {
|
|
239
|
+
if (!capabilityIdPattern.test(id)) {
|
|
240
|
+
throw new TypeError(`Tool capability id must use a stable lower-case path: ${id}`);
|
|
241
|
+
}
|
|
242
|
+
return Object.freeze({ id });
|
|
243
|
+
}
|
|
244
|
+
function createCapabilityResolver(resolve) {
|
|
245
|
+
return (capability) => {
|
|
246
|
+
const value = resolve(capability);
|
|
247
|
+
return value === void 0 ? void 0 : { ok: true, value };
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function admissionFailure(admission, expected) {
|
|
251
|
+
if (admission === void 0) return "no workload-scoped admission report was supplied";
|
|
252
|
+
if (admission.schema !== "forgeax.tool-service-admission-ref.v1" || !/^sha256:[0-9a-f]{64}$/.test(admission.reportDigest)) {
|
|
253
|
+
return "admission report identity is invalid";
|
|
254
|
+
}
|
|
255
|
+
for (const key of [
|
|
256
|
+
"toolId",
|
|
257
|
+
"descriptorDigest",
|
|
258
|
+
"recipeDigest",
|
|
259
|
+
"workloadClass",
|
|
260
|
+
"codeDigest",
|
|
261
|
+
"browserVersion",
|
|
262
|
+
"backend"
|
|
263
|
+
]) {
|
|
264
|
+
if (admission[key] !== expected[key]) return `admission ${key} does not match this run`;
|
|
265
|
+
}
|
|
266
|
+
if (admission.frameCount < 300) return "admission workload ran fewer than 300 frames";
|
|
267
|
+
if (Object.values(admission.samples).some((count) => count < 30)) {
|
|
268
|
+
return "admission sample set is incomplete";
|
|
269
|
+
}
|
|
270
|
+
if (!admission.correctness.terminalEquivalent || !admission.correctness.artifactIntegrity || !admission.correctness.freshReplay || !admission.correctness.hiddenParity || admission.correctness.drawCalls <= 0 || admission.correctness.nonBlackPixels <= 0) {
|
|
271
|
+
return "admission correctness gate failed";
|
|
272
|
+
}
|
|
273
|
+
const performance2 = admission.performance;
|
|
274
|
+
if (Object.values(performance2).some((value) => !Number.isFinite(value) || value <= 0) || performance2.serviceMedianMs > performance2.privateMedianMs * 0.8 || performance2.serviceP95Ms > performance2.privateP95Ms * 0.9 || performance2.serviceMaxMs > performance2.privateMaxMs * 1.1 || performance2.serviceRssBytes > performance2.privateRssBytes * 1.25) {
|
|
275
|
+
return "admission performance threshold failed";
|
|
276
|
+
}
|
|
277
|
+
if (!admission.cleanupPassed) return "admission cleanup gate failed";
|
|
278
|
+
if (!admission.evictionPassed) return "admission eviction gate failed";
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
function createServiceCapability(admission, expected) {
|
|
282
|
+
const reason = admissionFailure(admission, expected);
|
|
283
|
+
if (reason === void 0 && admission !== void 0) {
|
|
284
|
+
return { available: true, reportDigest: admission.reportDigest };
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
available: false,
|
|
288
|
+
code: "tool-service-capability-absent",
|
|
289
|
+
expected: "an admitted acceleration service",
|
|
290
|
+
hint: "Use the private executor and rerun benchmark admission before enabling service.",
|
|
291
|
+
detail: { reason: reason ?? "benchmark admission did not pass" }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function createRealmCapabilityMatrix(input) {
|
|
295
|
+
const realms = ["build", "host", "engine"].reduce(
|
|
296
|
+
(result, realm) => {
|
|
297
|
+
const supported = input.supported[realm];
|
|
298
|
+
result[realm] = supported ? { realm, supported: true } : { realm, supported: false, reason: "realm-capability-unavailable" };
|
|
299
|
+
return result;
|
|
300
|
+
},
|
|
301
|
+
{}
|
|
302
|
+
);
|
|
303
|
+
return { catalogDigest: input.catalogDigest, realms };
|
|
304
|
+
}
|
|
305
|
+
function bootstrapNotCloneSafeError(detail) {
|
|
306
|
+
return {
|
|
307
|
+
code: "tool-bootstrap-not-clone-safe",
|
|
308
|
+
expected: "bootstrap input to contain structured-clone-safe data",
|
|
309
|
+
hint: "Remove live handles, functions, ports, and realm-owned objects from bootstrap input.",
|
|
310
|
+
detail
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function validateRealmBootstrapPayload(value) {
|
|
314
|
+
try {
|
|
315
|
+
structuredClone(value);
|
|
316
|
+
return { ok: true };
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
error: bootstrapNotCloneSafeError({
|
|
321
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
322
|
+
})
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/carrier.ts
|
|
328
|
+
function token() {
|
|
329
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
330
|
+
}
|
|
331
|
+
function carrierError(code, expected, hint, detail) {
|
|
332
|
+
return { code, expected, hint, detail };
|
|
333
|
+
}
|
|
334
|
+
function containsLiveKey(value, seen = /* @__PURE__ */ new Set()) {
|
|
335
|
+
if (typeof value !== "object" || value === null) return false;
|
|
336
|
+
if (seen.has(value)) return false;
|
|
337
|
+
seen.add(value);
|
|
338
|
+
if (Array.isArray(value)) return value.some((entry) => containsLiveKey(entry, seen));
|
|
339
|
+
return Object.entries(value).some(([key, nested]) => {
|
|
340
|
+
if (["world", "renderer", "canvas", "ui", "profile", "liveHandle", "context", "fiber"].includes(
|
|
341
|
+
key
|
|
342
|
+
))
|
|
343
|
+
return true;
|
|
344
|
+
return containsLiveKey(nested, seen);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function validateOptions(options) {
|
|
348
|
+
if (options.projectId.length === 0 || options.consumerId.length === 0) {
|
|
349
|
+
throw new TypeError("carrier projectId and consumerId must not be empty");
|
|
350
|
+
}
|
|
351
|
+
const endpoint = new URL(options.endpoint);
|
|
352
|
+
if (endpoint.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(endpoint.hostname)) {
|
|
353
|
+
throw new TypeError("carrier endpoint must be loopback HTTP");
|
|
354
|
+
}
|
|
355
|
+
if (!Number.isFinite(options.now) || !Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
356
|
+
throw new TypeError("carrier clock and ttl must be finite and positive");
|
|
357
|
+
}
|
|
358
|
+
if (containsLiveKey(options.payload)) throw new TypeError("carrier-payload-live-state");
|
|
359
|
+
}
|
|
360
|
+
function createCarrierStateMachine(options) {
|
|
361
|
+
validateOptions(options);
|
|
362
|
+
const offer = {
|
|
363
|
+
schemaVersion: "1.0.0",
|
|
364
|
+
projectId: options.projectId,
|
|
365
|
+
consumerId: options.consumerId,
|
|
366
|
+
offerId: `offer:${token()}`,
|
|
367
|
+
endpoint: options.endpoint,
|
|
368
|
+
bearerToken: token(),
|
|
369
|
+
livenessToken: token(),
|
|
370
|
+
expiresAt: options.now + options.ttlMs,
|
|
371
|
+
...options.descriptorDigest === void 0 ? {} : { descriptorDigest: options.descriptorDigest },
|
|
372
|
+
...options.recipeDigest === void 0 ? {} : { recipeDigest: options.recipeDigest },
|
|
373
|
+
state: "offered"
|
|
374
|
+
};
|
|
375
|
+
let state = { state: "offered" };
|
|
376
|
+
let lease;
|
|
377
|
+
const leaseOffer = (requestOrId, maybeRequest) => {
|
|
378
|
+
const request = typeof requestOrId === "string" ? maybeRequest : requestOrId;
|
|
379
|
+
const requestedLeaseId = typeof requestOrId === "string" ? requestOrId : void 0;
|
|
380
|
+
if (request === void 0) {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
error: carrierError(
|
|
384
|
+
"carrier-token-invalid",
|
|
385
|
+
"a complete lease request",
|
|
386
|
+
"Provide consumer identity, bearer token, and current time.",
|
|
387
|
+
{}
|
|
388
|
+
)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (state.state === "started") {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: carrierError(
|
|
395
|
+
"carrier-started",
|
|
396
|
+
"a started carrier not to be retried",
|
|
397
|
+
"Report one terminal failure and clean up the existing started lease.",
|
|
398
|
+
{}
|
|
399
|
+
)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (state.state === "exited") {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: carrierError(
|
|
406
|
+
"carrier-exited",
|
|
407
|
+
"an exited carrier not to be retried",
|
|
408
|
+
"Re-run the operation from a serialized snapshot instead of migrating live state.",
|
|
409
|
+
{}
|
|
410
|
+
)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (request.consumerId !== offer.consumerId) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: carrierError(
|
|
417
|
+
"carrier-consumer-mismatch",
|
|
418
|
+
"the offer consumer identity to match",
|
|
419
|
+
"Use the consumer identity that was authenticated for this project offer.",
|
|
420
|
+
{ expected: offer.consumerId, actual: request.consumerId }
|
|
421
|
+
)
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (request.bearerToken !== offer.bearerToken) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: carrierError(
|
|
428
|
+
"carrier-token-invalid",
|
|
429
|
+
"the bearer token to match the ephemeral offer",
|
|
430
|
+
"Request a fresh visible offer; never persist or guess bearer tokens.",
|
|
431
|
+
{}
|
|
432
|
+
)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (offer.descriptorDigest !== void 0 && request.descriptorDigest !== offer.descriptorDigest) {
|
|
436
|
+
return {
|
|
437
|
+
ok: false,
|
|
438
|
+
error: carrierError(
|
|
439
|
+
"carrier-descriptor-mismatch",
|
|
440
|
+
"the descriptor digest to match the authenticated offer",
|
|
441
|
+
"Refresh the descriptor and request a new visible offer before retrying.",
|
|
442
|
+
{ expected: offer.descriptorDigest, actual: request.descriptorDigest }
|
|
443
|
+
)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (offer.recipeDigest !== void 0 && request.recipeDigest !== offer.recipeDigest) {
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
error: carrierError(
|
|
450
|
+
"carrier-recipe-mismatch",
|
|
451
|
+
"the recipe digest to match the authenticated offer",
|
|
452
|
+
"Serialize the current snapshot and request a fresh visible offer before retrying.",
|
|
453
|
+
{ expected: offer.recipeDigest, actual: request.recipeDigest }
|
|
454
|
+
)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
if (request.now >= offer.expiresAt) {
|
|
458
|
+
state = { state: "expired" };
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
error: carrierError(
|
|
462
|
+
"carrier-offer-expired",
|
|
463
|
+
"the offer to be within its expiry window",
|
|
464
|
+
"Fall back to the ordinary visible carrier before retrying.",
|
|
465
|
+
{ expiresAt: offer.expiresAt, now: request.now }
|
|
466
|
+
)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (requestedLeaseId !== void 0 && requestedLeaseId !== lease?.leaseId) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
error: carrierError(
|
|
473
|
+
"carrier-token-invalid",
|
|
474
|
+
"the lease id to match the authenticated offer",
|
|
475
|
+
"Use the lease id returned by the first successful lease.",
|
|
476
|
+
{}
|
|
477
|
+
)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
lease = {
|
|
481
|
+
leaseId: `lease:${token()}`,
|
|
482
|
+
offerId: offer.offerId,
|
|
483
|
+
consumerId: request.consumerId,
|
|
484
|
+
state: "leased"
|
|
485
|
+
};
|
|
486
|
+
state = { state: "leased", leaseId: lease.leaseId };
|
|
487
|
+
return { ok: true, value: lease, state: "leased" };
|
|
488
|
+
};
|
|
489
|
+
const started = (leaseId) => {
|
|
490
|
+
if (lease?.leaseId !== leaseId) {
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
error: carrierError(
|
|
494
|
+
"carrier-lease-required",
|
|
495
|
+
"a valid lease before started",
|
|
496
|
+
"Lease the authenticated offer before reporting provider started.",
|
|
497
|
+
{}
|
|
498
|
+
)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
state = { state: "started", leaseId };
|
|
502
|
+
return { ok: true, value: state, state: "started" };
|
|
503
|
+
};
|
|
504
|
+
const exit = (leaseId) => {
|
|
505
|
+
if (lease?.leaseId !== leaseId || state.state !== "started") {
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
error: carrierError(
|
|
509
|
+
"carrier-lease-required",
|
|
510
|
+
"a started lease before provider exit",
|
|
511
|
+
"Provider exit is terminal only after started has been acknowledged.",
|
|
512
|
+
{}
|
|
513
|
+
)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
state = { state: "exited", leaseId };
|
|
517
|
+
return { ok: true, value: state, state: "exited" };
|
|
518
|
+
};
|
|
519
|
+
const fallback = () => {
|
|
520
|
+
if (state.state === "started" || state.state === "exited") {
|
|
521
|
+
return {
|
|
522
|
+
ok: false,
|
|
523
|
+
error: carrierError(
|
|
524
|
+
"carrier-started",
|
|
525
|
+
"fallback to happen before provider started",
|
|
526
|
+
"Do not retry or migrate a carrier after started; return its terminal failure.",
|
|
527
|
+
{ state: state.state }
|
|
528
|
+
)
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
state = { state: "fallback" };
|
|
532
|
+
return { ok: true, value: state, state: "fallback" };
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
offer,
|
|
536
|
+
lease: leaseOffer,
|
|
537
|
+
started,
|
|
538
|
+
exit,
|
|
539
|
+
fallback,
|
|
540
|
+
snapshot: () => state
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/lease.ts
|
|
545
|
+
function createLexicalLease(runId) {
|
|
546
|
+
const cleanups = [];
|
|
547
|
+
let state = "active";
|
|
548
|
+
let terminalId;
|
|
549
|
+
let termination;
|
|
550
|
+
const terminate = (reason) => {
|
|
551
|
+
if (termination !== void 0) return termination;
|
|
552
|
+
terminalId = `${runId}:terminal:${crypto.randomUUID()}`;
|
|
553
|
+
state = "terminating";
|
|
554
|
+
termination = (async () => {
|
|
555
|
+
const failures = [];
|
|
556
|
+
for (let index = cleanups.length - 1; index >= 0; index -= 1) {
|
|
557
|
+
const entry = cleanups[index];
|
|
558
|
+
if (entry === void 0) continue;
|
|
559
|
+
try {
|
|
560
|
+
await entry.cleanup();
|
|
561
|
+
} catch (cause) {
|
|
562
|
+
failures.push({
|
|
563
|
+
owner: entry.owner,
|
|
564
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
state = "terminated";
|
|
569
|
+
return { terminalId, reason, failures };
|
|
570
|
+
})();
|
|
571
|
+
return termination;
|
|
572
|
+
};
|
|
573
|
+
return {
|
|
574
|
+
get state() {
|
|
575
|
+
return state;
|
|
576
|
+
},
|
|
577
|
+
register(owner, cleanup) {
|
|
578
|
+
if (state !== "active") return false;
|
|
579
|
+
cleanups.push({ owner, cleanup });
|
|
580
|
+
return true;
|
|
581
|
+
},
|
|
582
|
+
terminate
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/migration.ts
|
|
587
|
+
var liveStateKeys = /* @__PURE__ */ new Set([
|
|
588
|
+
"world",
|
|
589
|
+
"renderer",
|
|
590
|
+
"canvas",
|
|
591
|
+
"context",
|
|
592
|
+
"fiber",
|
|
593
|
+
"page",
|
|
594
|
+
"carrier",
|
|
595
|
+
"ui",
|
|
596
|
+
"selection",
|
|
597
|
+
"draft",
|
|
598
|
+
"undo",
|
|
599
|
+
"session"
|
|
600
|
+
]);
|
|
601
|
+
function liveStateError(path, key) {
|
|
602
|
+
return {
|
|
603
|
+
code: "tool-migration-live-state",
|
|
604
|
+
expected: "migration payload to contain only recipe, snapshot, and artifact references",
|
|
605
|
+
hint: "Recreate the operation from serializable authority facts; do not migrate live state.",
|
|
606
|
+
detail: { path, key }
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function createCapabilityToken(operation, source, version, target) {
|
|
610
|
+
if (source.catalogDigest.length === 0 || target.catalogDigest.length === 0)
|
|
611
|
+
throw new TypeError("capability probe catalogDigest must not be empty");
|
|
612
|
+
return {
|
|
613
|
+
kind: "forgeax-tool-capability",
|
|
614
|
+
version,
|
|
615
|
+
operation,
|
|
616
|
+
source: { ...source, evidence: [...source.evidence] },
|
|
617
|
+
target: { ...target, evidence: [...target.evidence] }
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function probeMigrationTarget(token2, target) {
|
|
621
|
+
const sameEvidence = token2.target.evidence.every((kind) => target.evidence.includes(kind));
|
|
622
|
+
const matches = token2.target.realm === target.realm && token2.target.catalogDigest === target.catalogDigest && token2.target.rhiBackend === target.rhiBackend && sameEvidence;
|
|
623
|
+
if (!matches) {
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
error: capabilityUnavailableError(`migration:${token2.operation}`, target.realm)
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return { ok: true, value: { ...target, evidence: [...target.evidence] } };
|
|
630
|
+
}
|
|
631
|
+
function createMigrationRecipe(input) {
|
|
632
|
+
const recipe = {
|
|
633
|
+
operation: input.operation,
|
|
634
|
+
args: input.args,
|
|
635
|
+
...input.snapshot === void 0 ? {} : { snapshot: { ...input.snapshot } },
|
|
636
|
+
artifacts: (input.artifacts ?? []).map((artifact) => ({ ...artifact }))
|
|
637
|
+
};
|
|
638
|
+
const validation = validateMigrationPayload(recipe);
|
|
639
|
+
if (!validation.ok) {
|
|
640
|
+
const path = validation.error.code === "tool-migration-live-state" ? validation.error.detail.path : "$";
|
|
641
|
+
throw new TypeError(path);
|
|
642
|
+
}
|
|
643
|
+
return recipe;
|
|
644
|
+
}
|
|
645
|
+
function validateMigrationPayload(value) {
|
|
646
|
+
const visit = (candidate, path) => {
|
|
647
|
+
if (!isSerializableValue(candidate)) {
|
|
648
|
+
return { ok: false, error: liveStateError(path, path.split(".").at(-1) ?? "<root>") };
|
|
649
|
+
}
|
|
650
|
+
if (candidate === null || typeof candidate !== "object") return { ok: true };
|
|
651
|
+
if (Array.isArray(candidate)) {
|
|
652
|
+
for (const [index, child] of candidate.entries()) {
|
|
653
|
+
const result = visit(child, `${path}[${index}]`);
|
|
654
|
+
if (!result.ok) return result;
|
|
655
|
+
}
|
|
656
|
+
return { ok: true };
|
|
657
|
+
}
|
|
658
|
+
for (const [key, child] of Object.entries(candidate)) {
|
|
659
|
+
const normalized = key.replaceAll("_", "").replaceAll("-", "").toLowerCase();
|
|
660
|
+
if (liveStateKeys.has(normalized)) return { ok: false, error: liveStateError(path, key) };
|
|
661
|
+
const result = visit(child, `${path}.${key}`);
|
|
662
|
+
if (!result.ok) return result;
|
|
663
|
+
}
|
|
664
|
+
return { ok: true };
|
|
665
|
+
};
|
|
666
|
+
return visit(value, "$");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/timing.ts
|
|
670
|
+
var PHASES = [
|
|
671
|
+
"lookup",
|
|
672
|
+
"lease",
|
|
673
|
+
"transport",
|
|
674
|
+
"execute",
|
|
675
|
+
"capture",
|
|
676
|
+
"finalize",
|
|
677
|
+
"analyze"
|
|
678
|
+
];
|
|
679
|
+
function createExclusiveTiming(now = () => performance.now()) {
|
|
680
|
+
const phases = Object.fromEntries(
|
|
681
|
+
PHASES.map((phase) => [phase, { status: "not-applicable" }])
|
|
682
|
+
);
|
|
683
|
+
const closed = /* @__PURE__ */ new Set();
|
|
684
|
+
let active;
|
|
685
|
+
let startedAtMs;
|
|
686
|
+
let endedAtMs;
|
|
687
|
+
return {
|
|
688
|
+
begin(phase) {
|
|
689
|
+
if (active !== void 0) throw new TypeError(`phase '${active.phase}' is still open`);
|
|
690
|
+
if (closed.has(phase)) throw new TypeError(`phase '${phase}' was already closed`);
|
|
691
|
+
const at = now();
|
|
692
|
+
startedAtMs ??= at;
|
|
693
|
+
active = { phase, startedAtMs: at };
|
|
694
|
+
},
|
|
695
|
+
end(phase) {
|
|
696
|
+
if (active?.phase !== phase) throw new TypeError(`phase '${phase}' is not the active phase`);
|
|
697
|
+
const at = now();
|
|
698
|
+
phases[phase] = { status: "observed", durationMs: Math.max(0, at - active.startedAtMs) };
|
|
699
|
+
closed.add(phase);
|
|
700
|
+
active = void 0;
|
|
701
|
+
endedAtMs = at;
|
|
702
|
+
},
|
|
703
|
+
record(phase, durationMs) {
|
|
704
|
+
if (!Number.isFinite(durationMs) || durationMs < 0)
|
|
705
|
+
throw new TypeError("phase duration must be finite and non-negative");
|
|
706
|
+
if (closed.has(phase)) throw new TypeError(`phase '${phase}' was already closed`);
|
|
707
|
+
const at = now();
|
|
708
|
+
startedAtMs ??= at - durationMs;
|
|
709
|
+
phases[phase] = { status: "observed", durationMs };
|
|
710
|
+
closed.add(phase);
|
|
711
|
+
endedAtMs = at;
|
|
712
|
+
},
|
|
713
|
+
finish() {
|
|
714
|
+
if (active !== void 0) throw new TypeError(`phase '${active.phase}' is still open`);
|
|
715
|
+
const start = startedAtMs ?? now();
|
|
716
|
+
const end = endedAtMs ?? start;
|
|
717
|
+
return {
|
|
718
|
+
startedAtMs: start,
|
|
719
|
+
endedAtMs: end,
|
|
720
|
+
totalMs: Math.max(0, end - start),
|
|
721
|
+
phases: { ...phases }
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function startToolTiming() {
|
|
727
|
+
return performance.now();
|
|
728
|
+
}
|
|
729
|
+
function finishToolTiming(startedAtMs, operationTiming) {
|
|
730
|
+
const endedAtMs = performance.now();
|
|
731
|
+
const result = operationTiming?.finish();
|
|
732
|
+
const durationMs = Math.max(0, endedAtMs - startedAtMs);
|
|
733
|
+
const attributedMs = result === void 0 ? 0 : Object.values(result.phases).reduce(
|
|
734
|
+
(sum, observation) => observation.status === "observed" ? sum + observation.durationMs : sum,
|
|
735
|
+
0
|
|
736
|
+
);
|
|
737
|
+
return {
|
|
738
|
+
startedAtMs,
|
|
739
|
+
endedAtMs,
|
|
740
|
+
durationMs,
|
|
741
|
+
...result === void 0 ? {} : { phases: result.phases, unattributedMs: Math.max(0, durationMs - attributedMs) }
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/runtime.ts
|
|
746
|
+
function defineTool(descriptor, execute) {
|
|
747
|
+
if (!/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(descriptor.id)) {
|
|
748
|
+
throw new TypeError(`Tool id must use a stable lower-case path: ${descriptor.id}`);
|
|
749
|
+
}
|
|
750
|
+
if (descriptor.title.trim().length === 0 || descriptor.summary.trim().length === 0) {
|
|
751
|
+
throw new TypeError("Tool title and summary must not be empty");
|
|
752
|
+
}
|
|
753
|
+
if (!Array.isArray(descriptor.evidence)) throw new TypeError("Tool evidence must be an array");
|
|
754
|
+
if (descriptor.preview !== void 0 && descriptor.preview.realm !== descriptor.realm) {
|
|
755
|
+
throw new TypeError(
|
|
756
|
+
`Tool ${descriptor.id} preview contract declares ${descriptor.preview.realm} but descriptor declares ${descriptor.realm}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
const argsSchema = descriptor.argsSchema;
|
|
760
|
+
const resultSchema = descriptor.resultSchema;
|
|
761
|
+
return {
|
|
762
|
+
descriptor: { ...descriptor, argsSchema, resultSchema, evidence: [...descriptor.evidence] },
|
|
763
|
+
execute
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function eventChannel() {
|
|
767
|
+
const queue = [];
|
|
768
|
+
const waiters = [];
|
|
769
|
+
let closed = false;
|
|
770
|
+
const emit = (event) => {
|
|
771
|
+
const waiter = waiters.shift();
|
|
772
|
+
if (waiter !== void 0) waiter({ done: false, value: event });
|
|
773
|
+
else queue.push(event);
|
|
774
|
+
};
|
|
775
|
+
const close = () => {
|
|
776
|
+
closed = true;
|
|
777
|
+
while (waiters.length > 0) waiters.shift()?.({ done: true, value: void 0 });
|
|
778
|
+
};
|
|
779
|
+
const events = {
|
|
780
|
+
[Symbol.asyncIterator]() {
|
|
781
|
+
return {
|
|
782
|
+
next: async () => {
|
|
783
|
+
const event = queue.shift();
|
|
784
|
+
if (event !== void 0) return { done: false, value: event };
|
|
785
|
+
if (closed) return { done: true, value: void 0 };
|
|
786
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
return { emit, close, events };
|
|
792
|
+
}
|
|
793
|
+
function isTerminal(value) {
|
|
794
|
+
if (typeof value !== "object" || value === null) return false;
|
|
795
|
+
const outcome = Reflect.get(value, "outcome");
|
|
796
|
+
return outcome === "succeeded" || outcome === "failed";
|
|
797
|
+
}
|
|
798
|
+
function serializablePreview(value) {
|
|
799
|
+
return isSerializableValue(value) ? value : null;
|
|
800
|
+
}
|
|
801
|
+
function domainError(error) {
|
|
802
|
+
return domainFailureError(
|
|
803
|
+
error.code,
|
|
804
|
+
error.expected ?? "the producer operation to succeed",
|
|
805
|
+
error.hint ?? "Inspect detail and repair the owning producer before retrying.",
|
|
806
|
+
error.detail
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
function hasOkField(value) {
|
|
810
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "ok") === "boolean";
|
|
811
|
+
}
|
|
812
|
+
function isToolRuntimeError(value) {
|
|
813
|
+
return typeof value === "object" && value !== null && typeof Reflect.get(value, "code") === "string" && Reflect.get(value, "code").startsWith("tool-");
|
|
814
|
+
}
|
|
815
|
+
function createToolRuntime(contributions) {
|
|
816
|
+
const byId = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const candidate of contributions) {
|
|
818
|
+
if (typeof candidate !== "object" || candidate === null) {
|
|
819
|
+
throw new TypeError("Tool contributions must be objects");
|
|
820
|
+
}
|
|
821
|
+
const contribution = candidate;
|
|
822
|
+
if (typeof contribution.execute !== "function") {
|
|
823
|
+
throw new TypeError("Tool contributions must provide an executor");
|
|
824
|
+
}
|
|
825
|
+
const id = contribution.descriptor.id;
|
|
826
|
+
if (byId.has(id)) throw new TypeError(`Duplicate tool contribution id: ${id}`);
|
|
827
|
+
byId.set(id, contribution);
|
|
828
|
+
}
|
|
829
|
+
const list = () => [...byId.values()].map((contribution) => contribution.descriptor);
|
|
830
|
+
const run = (contribution, args, options = {}) => {
|
|
831
|
+
const runId = `${contribution.descriptor.id}:${crypto.randomUUID()}`;
|
|
832
|
+
const channel = eventChannel();
|
|
833
|
+
const controller = new AbortController();
|
|
834
|
+
const startedAtMs = startToolTiming();
|
|
835
|
+
const operationTiming = createExclusiveTiming();
|
|
836
|
+
const lookupStartedAtMs = startToolTiming();
|
|
837
|
+
operationTiming.record("lookup", Math.max(0, startToolTiming() - lookupStartedAtMs));
|
|
838
|
+
const lease = createLexicalLease(runId);
|
|
839
|
+
const leaseStartedAtMs = startToolTiming();
|
|
840
|
+
operationTiming.record("lease", Math.max(0, startToolTiming() - leaseStartedAtMs));
|
|
841
|
+
let terminalStarted = false;
|
|
842
|
+
let cleanupReport = {
|
|
843
|
+
census: { worlds: 0, renderers: 0, canvases: 0, leases: 0 },
|
|
844
|
+
failures: []
|
|
845
|
+
};
|
|
846
|
+
let cancelReason = "cancelled by caller";
|
|
847
|
+
let resolveTerminal;
|
|
848
|
+
const terminal = new Promise((resolve) => {
|
|
849
|
+
resolveTerminal = resolve;
|
|
850
|
+
});
|
|
851
|
+
const settle = async (candidate, reason = "terminal") => {
|
|
852
|
+
if (terminalStarted) return;
|
|
853
|
+
terminalStarted = true;
|
|
854
|
+
controller.abort();
|
|
855
|
+
const finalizeStartedAtMs = startToolTiming();
|
|
856
|
+
const cleanupResult = await lease.terminate(reason);
|
|
857
|
+
const cleanupFailure = cleanupResult.failures[0];
|
|
858
|
+
const liveResources = Object.entries(cleanupReport.census).filter(([, count]) => count !== 0);
|
|
859
|
+
const reportFailure = cleanupReport.failures[0] ?? (liveResources.length === 0 ? void 0 : `live resources remain: ${liveResources.map(([kind, count]) => `${kind}=${count}`).join(", ")}`);
|
|
860
|
+
operationTiming.record("finalize", Math.max(0, startToolTiming() - finalizeStartedAtMs));
|
|
861
|
+
const finalTerminal = cleanupFailure || candidate.outcome === "succeeded" && reportFailure !== void 0 ? {
|
|
862
|
+
outcome: "failed",
|
|
863
|
+
failure: cleanupError(
|
|
864
|
+
runId,
|
|
865
|
+
cleanupFailure === void 0 ? reportFailure : `${cleanupFailure.owner}: ${cleanupFailure.message}`
|
|
866
|
+
),
|
|
867
|
+
artifacts: candidate.artifacts,
|
|
868
|
+
cleanup: cleanupReport,
|
|
869
|
+
...candidate.snapshotAfter === void 0 ? {} : { snapshotAfter: candidate.snapshotAfter },
|
|
870
|
+
timing: finishToolTiming(startedAtMs, operationTiming)
|
|
871
|
+
} : {
|
|
872
|
+
...candidate,
|
|
873
|
+
cleanup: cleanupReport,
|
|
874
|
+
timing: finishToolTiming(startedAtMs, operationTiming)
|
|
875
|
+
};
|
|
876
|
+
resolveTerminal(finalTerminal);
|
|
877
|
+
channel.emit({
|
|
878
|
+
kind: "terminal",
|
|
879
|
+
runId,
|
|
880
|
+
outcome: finalTerminal.outcome,
|
|
881
|
+
atMs: performance.now()
|
|
882
|
+
});
|
|
883
|
+
channel.close();
|
|
884
|
+
};
|
|
885
|
+
const context = {
|
|
886
|
+
runId,
|
|
887
|
+
signal: controller.signal,
|
|
888
|
+
...options.snapshot === void 0 ? {} : { snapshot: options.snapshot },
|
|
889
|
+
emit: (event) => {
|
|
890
|
+
if (!terminalStarted) channel.emit({ ...event, runId });
|
|
891
|
+
},
|
|
892
|
+
addCleanup: (cleanup) => {
|
|
893
|
+
if (!lease.register(`cleanup:${lease.state}`, cleanup)) void cleanup();
|
|
894
|
+
},
|
|
895
|
+
setCleanupReport: (report) => {
|
|
896
|
+
cleanupReport = {
|
|
897
|
+
census: { ...report.census },
|
|
898
|
+
failures: [...report.failures]
|
|
899
|
+
};
|
|
900
|
+
},
|
|
901
|
+
require: (capability) => {
|
|
902
|
+
if (terminalStarted) {
|
|
903
|
+
return { ok: false, error: terminalError(runId, "succeeded") };
|
|
904
|
+
}
|
|
905
|
+
const resolved = options.capabilityResolver?.(capability);
|
|
906
|
+
if (resolved !== void 0) return resolved;
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
error: capabilityUnavailableError(capability.id, contribution.descriptor.realm)
|
|
910
|
+
};
|
|
911
|
+
},
|
|
912
|
+
runChild: async (childContribution, childArgs, childOptions = {}) => {
|
|
913
|
+
if (terminalStarted) {
|
|
914
|
+
return {
|
|
915
|
+
outcome: "failed",
|
|
916
|
+
failure: terminalError(runId, "failed"),
|
|
917
|
+
artifacts: []
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const childRun = runtime.run(childContribution, childArgs, {
|
|
921
|
+
...childOptions,
|
|
922
|
+
signal: controller.signal,
|
|
923
|
+
...childOptions.capabilityResolver === void 0 && options.capabilityResolver !== void 0 ? { capabilityResolver: options.capabilityResolver } : {},
|
|
924
|
+
...options.snapshot === void 0 ? {} : { snapshot: options.snapshot }
|
|
925
|
+
});
|
|
926
|
+
channel.emit({
|
|
927
|
+
kind: "child-started",
|
|
928
|
+
runId,
|
|
929
|
+
childRunId: childRun.id,
|
|
930
|
+
atMs: performance.now()
|
|
931
|
+
});
|
|
932
|
+
lease.register(`child:${childRun.id}`, () => {
|
|
933
|
+
childRun.cancel("parent terminal");
|
|
934
|
+
});
|
|
935
|
+
return childRun.terminal;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const fail = (failure) => {
|
|
939
|
+
void settle({ outcome: "failed", failure, artifacts: [] });
|
|
940
|
+
};
|
|
941
|
+
const cancel = (reason = "cancelled by caller") => {
|
|
942
|
+
cancelReason = reason;
|
|
943
|
+
void settle(
|
|
944
|
+
{ outcome: "failed", failure: cancellationError(reason), artifacts: [] },
|
|
945
|
+
"cancel"
|
|
946
|
+
);
|
|
947
|
+
};
|
|
948
|
+
const disconnect = (transport = "tool transport") => {
|
|
949
|
+
void settle(
|
|
950
|
+
{ outcome: "failed", failure: disconnectedError(transport), artifacts: [] },
|
|
951
|
+
"disconnect"
|
|
952
|
+
);
|
|
953
|
+
};
|
|
954
|
+
const providerExit = (provider = "provider") => {
|
|
955
|
+
void settle(
|
|
956
|
+
{
|
|
957
|
+
outcome: "failed",
|
|
958
|
+
failure: domainFailureError(
|
|
959
|
+
"provider-exit",
|
|
960
|
+
"the provider to remain alive until terminal",
|
|
961
|
+
"Restart the provider and retry from the serialized snapshot.",
|
|
962
|
+
provider
|
|
963
|
+
),
|
|
964
|
+
artifacts: []
|
|
965
|
+
},
|
|
966
|
+
"provider-exit"
|
|
967
|
+
);
|
|
968
|
+
};
|
|
969
|
+
const timeout = options.deadlineMs === void 0 ? void 0 : setTimeout(
|
|
970
|
+
() => void settle(
|
|
971
|
+
{
|
|
972
|
+
outcome: "failed",
|
|
973
|
+
failure: timeoutError(options.deadlineMs),
|
|
974
|
+
artifacts: []
|
|
975
|
+
},
|
|
976
|
+
"timeout"
|
|
977
|
+
),
|
|
978
|
+
options.deadlineMs
|
|
979
|
+
);
|
|
980
|
+
channel.emit({ kind: "started", runId, atMs: performance.now() });
|
|
981
|
+
void (async () => {
|
|
982
|
+
const parsedArgs = contribution.descriptor.argsSchema.parse(args);
|
|
983
|
+
if (!parsedArgs.ok) {
|
|
984
|
+
fail(invalidArgsError(parsedArgs.error, serializablePreview(args)));
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
if (options.signal?.aborted) {
|
|
989
|
+
cancelReason = "aborted by caller";
|
|
990
|
+
fail(cancellationError(cancelReason));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const onAbort = () => cancel("aborted by caller");
|
|
994
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
995
|
+
let produced;
|
|
996
|
+
const executeStartedAtMs = startToolTiming();
|
|
997
|
+
try {
|
|
998
|
+
produced = await contribution.execute(parsedArgs.value, context);
|
|
999
|
+
} finally {
|
|
1000
|
+
operationTiming.record("execute", Math.max(0, startToolTiming() - executeStartedAtMs));
|
|
1001
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1002
|
+
}
|
|
1003
|
+
if (terminalStarted) return;
|
|
1004
|
+
if (controller.signal.aborted) {
|
|
1005
|
+
fail(cancellationError(cancelReason));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
let result = produced;
|
|
1009
|
+
let snapshotAfter = options.snapshot;
|
|
1010
|
+
let artifacts = [];
|
|
1011
|
+
if (isTerminal(produced)) {
|
|
1012
|
+
if (produced.outcome === "failed") {
|
|
1013
|
+
await settle(produced);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
result = produced.result;
|
|
1017
|
+
snapshotAfter = produced.snapshotAfter;
|
|
1018
|
+
artifacts = produced.artifacts;
|
|
1019
|
+
cleanupReport = produced.cleanup ?? cleanupReport;
|
|
1020
|
+
} else if (hasOkField(produced)) {
|
|
1021
|
+
if (produced.ok === false) {
|
|
1022
|
+
const error = Reflect.get(produced, "error");
|
|
1023
|
+
if (isToolRuntimeError(error)) {
|
|
1024
|
+
await settle({ outcome: "failed", failure: error, artifacts: [] });
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
await settle({
|
|
1028
|
+
outcome: "failed",
|
|
1029
|
+
failure: domainError(error),
|
|
1030
|
+
artifacts: []
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
result = Reflect.get(produced, "value");
|
|
1035
|
+
snapshotAfter = Reflect.get(produced, "snapshotAfter");
|
|
1036
|
+
artifacts = Reflect.get(produced, "artifacts") ?? [];
|
|
1037
|
+
}
|
|
1038
|
+
if (!isSerializableValue(result)) {
|
|
1039
|
+
await settle({
|
|
1040
|
+
outcome: "failed",
|
|
1041
|
+
failure: domainFailureError(
|
|
1042
|
+
"terminal-not-serializable",
|
|
1043
|
+
"the result to be JSON serializable",
|
|
1044
|
+
"Return plain JSON data and ArtifactRef values instead of live handles."
|
|
1045
|
+
),
|
|
1046
|
+
artifacts: []
|
|
1047
|
+
});
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const parsedResult = contribution.descriptor.resultSchema.parse(result);
|
|
1051
|
+
if (!parsedResult.ok) {
|
|
1052
|
+
await settle({
|
|
1053
|
+
outcome: "failed",
|
|
1054
|
+
failure: domainFailureError(
|
|
1055
|
+
"result-schema-invalid",
|
|
1056
|
+
"the result to satisfy resultSchema",
|
|
1057
|
+
parsedResult.error
|
|
1058
|
+
),
|
|
1059
|
+
artifacts: []
|
|
1060
|
+
});
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (!validateArtifactRefs(artifacts)) {
|
|
1064
|
+
await settle({
|
|
1065
|
+
outcome: "failed",
|
|
1066
|
+
failure: domainFailureError(
|
|
1067
|
+
"artifact-ref-invalid",
|
|
1068
|
+
"artifact refs to be serializable ArtifactRef values",
|
|
1069
|
+
"Return refs created by createArtifactRef."
|
|
1070
|
+
),
|
|
1071
|
+
artifacts: []
|
|
1072
|
+
});
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const requiredEvidence = [
|
|
1076
|
+
.../* @__PURE__ */ new Set([...contribution.descriptor.evidence ?? [], ...options.evidence ?? []])
|
|
1077
|
+
];
|
|
1078
|
+
const missingEvidence = requiredEvidence.filter(
|
|
1079
|
+
(kind) => !artifacts.some((artifact) => artifact.kind === kind)
|
|
1080
|
+
);
|
|
1081
|
+
if (missingEvidence.length > 0) {
|
|
1082
|
+
await settle({
|
|
1083
|
+
outcome: "failed",
|
|
1084
|
+
failure: artifactIncompleteError(missingEvidence, runId),
|
|
1085
|
+
artifacts
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
await settle({
|
|
1090
|
+
outcome: "succeeded",
|
|
1091
|
+
result: parsedResult.value,
|
|
1092
|
+
artifacts,
|
|
1093
|
+
...snapshotAfter === void 0 ? {} : { snapshotAfter }
|
|
1094
|
+
});
|
|
1095
|
+
} catch (cause) {
|
|
1096
|
+
await settle({
|
|
1097
|
+
outcome: "failed",
|
|
1098
|
+
failure: domainFailureError(
|
|
1099
|
+
"executor-threw",
|
|
1100
|
+
"the contribution executor to return a result",
|
|
1101
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
1102
|
+
),
|
|
1103
|
+
artifacts: []
|
|
1104
|
+
});
|
|
1105
|
+
} finally {
|
|
1106
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
1107
|
+
}
|
|
1108
|
+
})();
|
|
1109
|
+
const runtimeRun = {
|
|
1110
|
+
id: runId,
|
|
1111
|
+
events: channel.events,
|
|
1112
|
+
terminal,
|
|
1113
|
+
cancel,
|
|
1114
|
+
disconnect,
|
|
1115
|
+
providerExit
|
|
1116
|
+
};
|
|
1117
|
+
return runtimeRun;
|
|
1118
|
+
};
|
|
1119
|
+
const runtime = {
|
|
1120
|
+
list,
|
|
1121
|
+
describe: (id) => byId.get(id)?.descriptor,
|
|
1122
|
+
get: (id) => byId.get(id),
|
|
1123
|
+
run
|
|
1124
|
+
};
|
|
1125
|
+
return runtime;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/snapshot.ts
|
|
1129
|
+
function terminalSnapshot(terminal) {
|
|
1130
|
+
return terminal.snapshotAfter;
|
|
1131
|
+
}
|
|
1132
|
+
function isSnapshotRef(value) {
|
|
1133
|
+
return typeof value === "object" && value !== null && Number.isSafeInteger(Reflect.get(value, "revision")) && Reflect.get(value, "revision") >= 0 && typeof Reflect.get(value, "digest") === "string" && Reflect.get(value, "digest").length > 0;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/transport.ts
|
|
1137
|
+
var CarrierTransportError = class extends Error {
|
|
1138
|
+
code;
|
|
1139
|
+
expected;
|
|
1140
|
+
hint;
|
|
1141
|
+
detail;
|
|
1142
|
+
constructor(error) {
|
|
1143
|
+
super(error.code);
|
|
1144
|
+
this.name = "CarrierTransportError";
|
|
1145
|
+
this.code = error.code;
|
|
1146
|
+
if (error.expected !== void 0) this.expected = error.expected;
|
|
1147
|
+
if (error.hint !== void 0) this.hint = error.hint;
|
|
1148
|
+
if (error.detail !== void 0) this.detail = error.detail;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
function createAuthenticatedCarrierTransport(options) {
|
|
1152
|
+
const url = new URL(options.endpoint);
|
|
1153
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname))
|
|
1154
|
+
throw new TypeError("carrier transport endpoint must be loopback HTTP");
|
|
1155
|
+
if (options.bearerToken.length < 8) throw new TypeError("carrier bearer token is too short");
|
|
1156
|
+
let connected = true;
|
|
1157
|
+
async function request(path, payload) {
|
|
1158
|
+
if (!connected)
|
|
1159
|
+
throw new CarrierTransportError({
|
|
1160
|
+
code: "carrier-exited",
|
|
1161
|
+
expected: "a connected carrier transport",
|
|
1162
|
+
hint: "Request a fresh visible offer after provider exit."
|
|
1163
|
+
});
|
|
1164
|
+
let response;
|
|
1165
|
+
try {
|
|
1166
|
+
response = await fetch(`${options.endpoint}${path}`, {
|
|
1167
|
+
method: "POST",
|
|
1168
|
+
headers: {
|
|
1169
|
+
authorization: `Bearer ${options.bearerToken}`,
|
|
1170
|
+
"content-type": "application/json"
|
|
1171
|
+
},
|
|
1172
|
+
body: JSON.stringify(payload)
|
|
1173
|
+
});
|
|
1174
|
+
} catch (cause) {
|
|
1175
|
+
throw new CarrierTransportError({
|
|
1176
|
+
code: "carrier-provider-exit",
|
|
1177
|
+
expected: "the carrier provider to remain reachable",
|
|
1178
|
+
hint: "Offer a fresh carrier; do not fallback after started.",
|
|
1179
|
+
detail: { cause: cause instanceof Error ? cause.message : String(cause) }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const value = await response.json().catch(() => ({}));
|
|
1183
|
+
if (!response.ok || typeof value !== "object" || value === null || !("ok" in value) || value.ok !== true) {
|
|
1184
|
+
const error = typeof value === "object" && value !== null && "error" in value && typeof value.error === "object" && value.error !== null ? value.error : {
|
|
1185
|
+
code: "carrier-provider-exit",
|
|
1186
|
+
expected: "a successful carrier response",
|
|
1187
|
+
hint: "Offer a fresh carrier and retry from a serialized snapshot."
|
|
1188
|
+
};
|
|
1189
|
+
const normalized = {
|
|
1190
|
+
code: typeof error.code === "string" ? error.code : "carrier-provider-exit",
|
|
1191
|
+
...typeof error.expected === "string" ? { expected: error.expected } : {},
|
|
1192
|
+
...typeof error.hint === "string" ? { hint: error.hint } : {},
|
|
1193
|
+
...typeof error.detail === "object" && error.detail !== null ? { detail: error.detail } : {}
|
|
1194
|
+
};
|
|
1195
|
+
throw new CarrierTransportError(normalized);
|
|
1196
|
+
}
|
|
1197
|
+
return value;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
endpoint: options.endpoint,
|
|
1201
|
+
get connected() {
|
|
1202
|
+
return connected;
|
|
1203
|
+
},
|
|
1204
|
+
lease: (payload) => request("/lease", payload),
|
|
1205
|
+
started: (payload) => request("/start", payload),
|
|
1206
|
+
execute: (payload) => request("/execute", payload),
|
|
1207
|
+
exit: (payload) => request("/exit", payload),
|
|
1208
|
+
close() {
|
|
1209
|
+
connected = false;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function createLoopbackTransport(endpoint) {
|
|
1214
|
+
const url = new URL(endpoint);
|
|
1215
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname)) {
|
|
1216
|
+
throw new TypeError("carrier transport endpoint must be loopback HTTP");
|
|
1217
|
+
}
|
|
1218
|
+
let connected = true;
|
|
1219
|
+
return {
|
|
1220
|
+
endpoint,
|
|
1221
|
+
get connected() {
|
|
1222
|
+
return connected;
|
|
1223
|
+
},
|
|
1224
|
+
send(payload) {
|
|
1225
|
+
if (!connected || !validateRealmBootstrapPayload(payload).ok) return false;
|
|
1226
|
+
return true;
|
|
1227
|
+
},
|
|
1228
|
+
close() {
|
|
1229
|
+
connected = false;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
function createAuthenticatedLoopbackTransport(options) {
|
|
1234
|
+
const url = new URL(options.endpoint);
|
|
1235
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname)) {
|
|
1236
|
+
throw new TypeError("service transport endpoint must be loopback HTTP");
|
|
1237
|
+
}
|
|
1238
|
+
if (options.bearerToken.length < 8) throw new TypeError("service bearer token is too short");
|
|
1239
|
+
let connected = true;
|
|
1240
|
+
return {
|
|
1241
|
+
endpoint: options.endpoint,
|
|
1242
|
+
get connected() {
|
|
1243
|
+
return connected;
|
|
1244
|
+
},
|
|
1245
|
+
async request(request, bearerToken, requestOptions = {}) {
|
|
1246
|
+
if (!connected) throw new Error("service transport is disconnected");
|
|
1247
|
+
if (bearerToken !== options.bearerToken) throw new Error("service bearer token rejected");
|
|
1248
|
+
if (!validateRealmBootstrapPayload(request).ok) {
|
|
1249
|
+
throw new Error("service request is not structured-clone safe");
|
|
1250
|
+
}
|
|
1251
|
+
const response = await fetch(options.endpoint, {
|
|
1252
|
+
method: "POST",
|
|
1253
|
+
headers: {
|
|
1254
|
+
authorization: `Bearer ${bearerToken}`,
|
|
1255
|
+
"content-type": "application/json"
|
|
1256
|
+
},
|
|
1257
|
+
body: JSON.stringify(request),
|
|
1258
|
+
...requestOptions.signal === void 0 ? {} : { signal: requestOptions.signal }
|
|
1259
|
+
});
|
|
1260
|
+
const payload = await response.json();
|
|
1261
|
+
if (!response.ok) {
|
|
1262
|
+
const detail = typeof payload === "object" && payload !== null && "error" in payload ? String(payload.error) : `HTTP ${response.status}`;
|
|
1263
|
+
throw new Error(`service request failed: ${detail}`);
|
|
1264
|
+
}
|
|
1265
|
+
return payload;
|
|
1266
|
+
},
|
|
1267
|
+
close() {
|
|
1268
|
+
connected = false;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export { CarrierTransportError, artifactIncompleteError, cancellationError, capabilityUnavailableError, cleanupError, createArtifactManifest, createArtifactRef, createAuthenticatedCarrierTransport, createAuthenticatedLoopbackTransport, createCapabilityResolver, createCapabilityToken, createCarrierStateMachine, createExclusiveTiming, createLexicalLease, createLoopbackTransport, createMigrationRecipe, createPreviewArtifactManifest, createRealmCapabilityMatrix, createServiceCapability, createSnapshotRef, createToolRuntime, defineTool, defineToolCapability, disconnectedError, domainFailureError, finishToolTiming, invalidArgsError, isSerializableValue, isSnapshotRef, probeMigrationTarget, snapshotStaleError, startToolTiming, terminalError, terminalSnapshot, timeoutError, validateArtifactManifest, validateArtifactRefs, validateMigrationPayload, validatePreviewArtifactManifest, validateRealmBootstrapPayload };
|
|
1274
|
+
//# sourceMappingURL=index.mjs.map
|
|
1275
|
+
//# sourceMappingURL=index.mjs.map
|