@intx/hub-sessions 0.2.2 → 0.4.0

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 (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. package/dist/available-skills-stanza.js +0 -32
@@ -0,0 +1,554 @@
1
+ import { type } from "arktype";
2
+ import { sha256 } from "@intx/crypto";
3
+ import { createSidecarAllocationStore, createWorkflowProbeStore, createWorkflowRunLaunchSpecStore, resolveTenantSidecarCapabilityPolicies, resolveSourcesByOfferingIds, } from "@intx/db";
4
+ import { grant, workflowRun } from "@intx/db/schema";
5
+ import { eq } from "drizzle-orm";
6
+ import { generateId } from "@intx/hub-common";
7
+ import { hexEncode, SidecarCapabilityRule, } from "@intx/types";
8
+ import { approvalItemsFromSet, approvalSetFromItems, buildInertProjectionStepSources, deriveRunAddress, deriveRunAgentId, } from "@intx/workflow-deploy";
9
+ import { DestroySidecarResult, EnsureSidecarResult, } from "./sidecar-allocation/index.js";
10
+ import { SessionLaunchError, } from "./session-service.js";
11
+ import { buildReferencedWorkflowSourcePins } from "./workflow-source-pins.js";
12
+ import { DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS, runSidecarOperation, } from "./sidecar-allocation/operation.js";
13
+ export class WorkflowProvisioningError extends Error {
14
+ code;
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = "WorkflowProvisioningError";
18
+ this.code = code;
19
+ }
20
+ }
21
+ function randomAllocationId() {
22
+ return `sal_${hexEncode(crypto.getRandomValues(new Uint8Array(16)))}`;
23
+ }
24
+ function randomSidecarId() {
25
+ return `sc_${hexEncode(crypto.getRandomValues(new Uint8Array(16)))}`;
26
+ }
27
+ function randomToken() {
28
+ return `intx_sc_${hexEncode(crypto.getRandomValues(new Uint8Array(32)))}`;
29
+ }
30
+ function selectProvisioner(selection) {
31
+ if (selection.ok)
32
+ return selection.provisioner;
33
+ throw new WorkflowProvisioningError("provisioner_no_match", formatProvisionerMismatches(selection.mismatches));
34
+ }
35
+ function parseEnsureResult(value) {
36
+ const result = EnsureSidecarResult(value);
37
+ if (result instanceof type.errors) {
38
+ throw new Error(`Sidecar provisioner returned an invalid ensure result: ${result.summary}`);
39
+ }
40
+ return result;
41
+ }
42
+ function parseDestroyResult(value) {
43
+ const result = DestroySidecarResult(value);
44
+ if (result instanceof type.errors) {
45
+ throw new Error(`Sidecar provisioner returned an invalid destroy result: ${result.summary}`);
46
+ }
47
+ return result;
48
+ }
49
+ function createProvisionedHarnessConfig(args) {
50
+ const deploymentAddress = deriveRunAddress({
51
+ runId: args.anchorRunId,
52
+ domain: args.deploymentDomain,
53
+ });
54
+ return {
55
+ sessionId: args.sessionId,
56
+ agentId: deriveRunAgentId({ runId: args.anchorRunId }),
57
+ tenantId: args.tenantId,
58
+ principalId: args.sourceAuthorityPrincipalId,
59
+ agentAddress: deploymentAddress,
60
+ systemPrompt: "",
61
+ tools: [],
62
+ grants: [],
63
+ sources: args.sources,
64
+ defaultSource: args.defaultSource,
65
+ };
66
+ }
67
+ export function createWorkflowAllocationService({ db, deploymentPlugins, probePlugins, preparedDeployer, credentialCipher, probeCapabilityRules = [], allocationRouter, hubWebSocketUrl, createAllocationId = randomAllocationId, createSidecarId = randomSidecarId, createToken = randomToken, connectTimeoutMs = 120_000, operationTimeoutMs = DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS, now = () => new Date(), }) {
68
+ const allocationStore = createSidecarAllocationStore(db);
69
+ const probeStore = createWorkflowProbeStore(db);
70
+ const launchSpecStore = createWorkflowRunLaunchSpecStore(db);
71
+ const validatedProbeCapabilityRules = SidecarCapabilityRule.array()(probeCapabilityRules);
72
+ if (validatedProbeCapabilityRules instanceof type.errors) {
73
+ throw new Error(`Invalid workflow probe capability rules: ${validatedProbeCapabilityRules.summary}`);
74
+ }
75
+ const configuredProbeCapabilityRules = validatedProbeCapabilityRules.map((rule) => ({ ...rule }));
76
+ if (connectTimeoutMs <= 0) {
77
+ throw new Error("connectTimeoutMs must be positive");
78
+ }
79
+ if (!Number.isSafeInteger(operationTimeoutMs) || operationTimeoutMs <= 0) {
80
+ throw new Error("operationTimeoutMs must be a positive integer");
81
+ }
82
+ function matchingProvisioner(probe) {
83
+ const provisioner = probePlugins.getProvisioner(probe.provisionerId);
84
+ return provisioner !== null &&
85
+ provisioner.apiVersion === probe.provisionerApiVersion &&
86
+ provisioner.bindingFingerprint === probe.provisionerBindingFingerprint
87
+ ? provisioner
88
+ : null;
89
+ }
90
+ async function beginProbeRelease(probe, failure) {
91
+ if (probe.status === "releasing")
92
+ return probe;
93
+ return probeStore.transition(probe.id, ["pending", "provisioning", "probing"], "releasing", {
94
+ ...(failure !== undefined
95
+ ? {
96
+ failureCode: failure.code,
97
+ failureMessage: failure.message,
98
+ }
99
+ : {}),
100
+ now: now(),
101
+ });
102
+ }
103
+ const probeCleanupTasks = new Map();
104
+ function finishProbeRelease(probe, finalStatus) {
105
+ const existing = probeCleanupTasks.get(probe.id);
106
+ if (existing !== undefined)
107
+ return existing;
108
+ const task = cleanUpProbe(probe, finalStatus);
109
+ probeCleanupTasks.set(probe.id, task);
110
+ const settled = () => {
111
+ if (probeCleanupTasks.get(probe.id) === task)
112
+ probeCleanupTasks.delete(probe.id);
113
+ };
114
+ void task.then(settled, settled);
115
+ return task;
116
+ }
117
+ async function cleanUpProbe(probe, finalStatus) {
118
+ const releasing = await probeStore.get(probe.id);
119
+ if (releasing?.status === "succeeded" || releasing?.status === "failed")
120
+ return;
121
+ if (releasing?.status !== "releasing") {
122
+ throw new Error(`Workflow probe ${probe.id} is not releasing`);
123
+ }
124
+ const sidecarId = releasing.sidecarId;
125
+ if (sidecarId !== null) {
126
+ const provisioner = matchingProvisioner(releasing);
127
+ if (provisioner === null) {
128
+ throw new Error(`Cannot clean up workflow probe ${releasing.id}: provisioner binding ${releasing.provisionerId} is unavailable`);
129
+ }
130
+ const destroyed = parseDestroyResult(await runSidecarOperation("Probe destroy", operationTimeoutMs, (signal) => provisioner.destroy({
131
+ signal,
132
+ allocationId: releasing.id,
133
+ generation: releasing.generation,
134
+ sidecarId,
135
+ ...(releasing.externalRef !== null
136
+ ? { externalRef: releasing.externalRef }
137
+ : {}),
138
+ })));
139
+ if (destroyed.kind === "rejected") {
140
+ throw new Error(`Provisioner ${provisioner.id} rejected probe cleanup: ${destroyed.message}`);
141
+ }
142
+ allocationRouter.disconnectAllocation({
143
+ allocationId: releasing.id,
144
+ generation: releasing.generation,
145
+ });
146
+ }
147
+ const completed = await probeStore.transition(releasing.id, ["releasing"], finalStatus, { now: now() });
148
+ if (completed === null) {
149
+ throw new Error(`Workflow probe ${releasing.id} changed before cleanup completed`);
150
+ }
151
+ allocationRouter.retireAllocation({
152
+ allocationId: releasing.id,
153
+ generation: releasing.generation,
154
+ });
155
+ }
156
+ async function releaseProbe(probe, finalStatus, failure) {
157
+ const releasing = await beginProbeRelease(probe, failure);
158
+ if (releasing === null)
159
+ return;
160
+ await finishProbeRelease(releasing, finalStatus);
161
+ }
162
+ async function createDeployment(args) {
163
+ const { request, approved, provisioner, probe, adoptProbe } = args;
164
+ const probeSidecarId = probe.sidecarId;
165
+ if (adoptProbe && probeSidecarId === null) {
166
+ throw new Error(`Workflow probe ${probe.id} has no sidecar to adopt`);
167
+ }
168
+ const allocationId = adoptProbe ? probe.id : createAllocationId();
169
+ const createdAt = now();
170
+ const deploymentAddress = deriveRunAddress({
171
+ runId: request.anchorRunId,
172
+ domain: request.deploymentDomain,
173
+ });
174
+ const frozenApprovalBundle = {
175
+ source: request.source,
176
+ entry: request.entry,
177
+ projection: approved.projection,
178
+ closure: approved.closure,
179
+ approvedWireHash: approved.approval.approvedWireHash,
180
+ approvedGrants: [
181
+ ...approvalItemsFromSet(approved.approval.approvedSurface),
182
+ ],
183
+ };
184
+ await db.transaction(async (tx) => {
185
+ await tx.insert(workflowRun).values({
186
+ id: request.anchorRunId,
187
+ tenantId: request.tenantId,
188
+ anchorRunId: request.anchorRunId,
189
+ definitionId: approved.approval.definitionId,
190
+ address: deploymentAddress,
191
+ status: "deployed",
192
+ createdAt,
193
+ });
194
+ await tx.insert(grant).values({
195
+ id: generateId("grant"),
196
+ tenantId: request.tenantId,
197
+ principalId: request.sourceAuthorityPrincipalId,
198
+ resource: `workflow-run:${request.anchorRunId}`,
199
+ action: "read",
200
+ effect: "allow",
201
+ origin: "creator",
202
+ createdAt,
203
+ updatedAt: createdAt,
204
+ });
205
+ await launchSpecStore.create({
206
+ anchorRunId: request.anchorRunId,
207
+ sessionId: request.sessionId,
208
+ deploymentDomain: request.deploymentDomain,
209
+ sourceAuthorityPrincipalId: request.sourceAuthorityPrincipalId,
210
+ frozenApprovalBundle,
211
+ sourceOfferingIds: [...request.sourceOfferingIds],
212
+ defaultSourceOfferingId: request.defaultSourceOfferingId,
213
+ deployContent: request.deployContent,
214
+ ...(request.toolPackagePins !== undefined
215
+ ? { toolPackagePins: [...request.toolPackagePins] }
216
+ : {}),
217
+ createdAt,
218
+ }, tx);
219
+ if (adoptProbe) {
220
+ if (probeSidecarId === null) {
221
+ throw new Error(`Workflow probe ${probe.id} lost its sidecar`);
222
+ }
223
+ await allocationStore.createAdopted({
224
+ id: allocationId,
225
+ anchorRunId: request.anchorRunId,
226
+ tenantId: request.tenantId,
227
+ provisionerId: provisioner.id,
228
+ provisionerApiVersion: provisioner.apiVersion,
229
+ provisionerBindingFingerprint: provisioner.bindingFingerprint,
230
+ sidecarId: probeSidecarId,
231
+ generation: probe.generation,
232
+ ...(probe.externalRef !== null
233
+ ? { externalRef: probe.externalRef }
234
+ : {}),
235
+ connectDeadline: new Date(createdAt.getTime() + connectTimeoutMs),
236
+ now: createdAt,
237
+ }, tx);
238
+ const completed = await probeStore.transition(probe.id, ["probing"], "succeeded", { now: createdAt, tx });
239
+ if (completed === null) {
240
+ throw new Error(`Workflow probe ${probe.id} changed before adoption`);
241
+ }
242
+ }
243
+ else {
244
+ await allocationStore.createPending({
245
+ id: allocationId,
246
+ anchorRunId: request.anchorRunId,
247
+ tenantId: request.tenantId,
248
+ provisionerId: provisioner.id,
249
+ provisionerApiVersion: provisioner.apiVersion,
250
+ provisionerBindingFingerprint: provisioner.bindingFingerprint,
251
+ now: createdAt,
252
+ }, tx);
253
+ }
254
+ });
255
+ if (adoptProbe) {
256
+ allocationRouter.disconnectAllocation({
257
+ allocationId: probe.id,
258
+ generation: probe.generation,
259
+ });
260
+ }
261
+ return {
262
+ anchorRunId: request.anchorRunId,
263
+ deploymentAddress,
264
+ allocationId,
265
+ status: "pending",
266
+ };
267
+ }
268
+ async function prepareProvisionedDeployment(args) {
269
+ if (args.sourceOfferingIds.length === 0) {
270
+ throw new WorkflowProvisioningError("invalid_source_offerings", "Provisioned workflow deployment requires at least one catalog offering");
271
+ }
272
+ if (!args.sourceOfferingIds.includes(args.defaultSourceOfferingId)) {
273
+ throw new WorkflowProvisioningError("invalid_source_offerings", "The default source offering must be present in the source offering chain");
274
+ }
275
+ const [sourceCheck, tenantPolicies] = await Promise.all([
276
+ resolveSourcesByOfferingIds(db, args.tenantId, args.sourceOfferingIds, credentialCipher),
277
+ resolveTenantSidecarCapabilityPolicies(db, args.tenantId),
278
+ ]);
279
+ if (!sourceCheck.ok) {
280
+ throw new WorkflowProvisioningError("source_offering_unavailable", `Catalog offering ${sourceCheck.offeringId} cannot be used by the deployment authority`);
281
+ }
282
+ const defaultSource = sourceCheck.sources.find((source) => source.id === args.defaultSourceOfferingId);
283
+ if (defaultSource === undefined) {
284
+ throw new WorkflowProvisioningError("invalid_source_offerings", `Default offering ${args.defaultSourceOfferingId} was not resolved for deployment ${args.anchorRunId}`);
285
+ }
286
+ const probeProvisioner = selectProvisioner(await probePlugins.selectProvisioner({
287
+ tenantPolicies,
288
+ probeRules: configuredProbeCapabilityRules,
289
+ workflowRules: [],
290
+ }));
291
+ const probeId = createAllocationId();
292
+ let probe = await probeStore.create({
293
+ id: probeId,
294
+ tenantId: args.tenantId,
295
+ definitionAssetId: args.definitionAssetId,
296
+ source: args.source,
297
+ entry: args.entry,
298
+ ...(args.pin !== undefined ? { pin: args.pin } : {}),
299
+ provisionerId: probeProvisioner.id,
300
+ provisionerApiVersion: probeProvisioner.apiVersion,
301
+ provisionerBindingFingerprint: probeProvisioner.bindingFingerprint,
302
+ now: now(),
303
+ });
304
+ try {
305
+ const token = createToken();
306
+ const sidecarId = createSidecarId();
307
+ const bound = await probeStore.bindSidecar({
308
+ probeId,
309
+ sidecarId,
310
+ tokenHashSha256: await sha256(token),
311
+ now: now(),
312
+ });
313
+ if (bound === null) {
314
+ throw new Error(`Workflow probe ${probeId} changed before provisioning`);
315
+ }
316
+ probe = bound;
317
+ const allocationTarget = {
318
+ allocationId: probe.id,
319
+ generation: probe.generation,
320
+ };
321
+ allocationRouter.fenceAllocation(probe.id, probe.generation);
322
+ const ensured = parseEnsureResult(await runSidecarOperation("Probe ensure", operationTimeoutMs, (signal) => probeProvisioner.ensure({
323
+ signal,
324
+ allocationId: probe.id,
325
+ generation: probe.generation,
326
+ tenantId: probe.tenantId,
327
+ // The provisioner contract treats this as an opaque owner id. A
328
+ // probe has no workflow run, so its own id is the honest owner.
329
+ anchorRunId: probe.id,
330
+ sidecarId,
331
+ token,
332
+ hubWebSocketUrl,
333
+ })));
334
+ if (ensured.kind === "rejected") {
335
+ throw new WorkflowProvisioningError(ensured.code, ensured.message);
336
+ }
337
+ const probing = await probeStore.markProbing({
338
+ probeId,
339
+ ...(ensured.externalRef !== undefined
340
+ ? { externalRef: ensured.externalRef }
341
+ : {}),
342
+ now: now(),
343
+ });
344
+ if (probing === null) {
345
+ throw new Error(`Workflow probe ${probeId} changed after provisioning`);
346
+ }
347
+ probe = probing;
348
+ await allocationRouter.waitForAllocatedSidecar(allocationTarget, connectTimeoutMs);
349
+ let resultRecorded = false;
350
+ const approved = await preparedDeployer.installAndApproveWorkflowSource({
351
+ source: args.source,
352
+ entry: args.entry,
353
+ ...(args.pin !== undefined ? { pin: args.pin } : {}),
354
+ definitionAssetId: args.definitionAssetId,
355
+ allocationTarget,
356
+ onProbeResult: async (result) => {
357
+ if ((await probeStore.recordResult(probeId, result, now())) === null) {
358
+ throw new Error(`Workflow probe ${probeId} changed before result persistence`);
359
+ }
360
+ resultRecorded = true;
361
+ },
362
+ });
363
+ if (!resultRecorded) {
364
+ throw new Error(`Workflow probe ${probeId} completed without persisting its result`);
365
+ }
366
+ if (!approved.approval.ok) {
367
+ throw new Error("prepareProvisionedDeployment: install did not yield an approved definition");
368
+ }
369
+ const narrowed = {
370
+ ...approved,
371
+ approval: approved.approval,
372
+ };
373
+ const config = createProvisionedHarnessConfig({
374
+ tenantId: args.tenantId,
375
+ anchorRunId: args.anchorRunId,
376
+ deploymentDomain: args.deploymentDomain,
377
+ sessionId: args.sessionId,
378
+ sourceAuthorityPrincipalId: args.sourceAuthorityPrincipalId,
379
+ sources: sourceCheck.sources,
380
+ defaultSource: defaultSource.id,
381
+ });
382
+ buildInertProjectionStepSources({
383
+ projection: approved.projection,
384
+ config,
385
+ operatorApprovals: approved.approval.approvedSurface,
386
+ });
387
+ await buildReferencedWorkflowSourcePins({
388
+ projection: approved.projection,
389
+ config,
390
+ operatorApprovals: approved.approval.approvedSurface,
391
+ });
392
+ const deploymentProvisioner = selectProvisioner(await deploymentPlugins.selectProvisioner({
393
+ tenantPolicies,
394
+ workflowRules: approved.projection.sidecarPlacement?.capabilities ?? [],
395
+ }));
396
+ const adoptProbe = deploymentProvisioner.id === probeProvisioner.id &&
397
+ deploymentProvisioner.apiVersion === probeProvisioner.apiVersion &&
398
+ deploymentProvisioner.bindingFingerprint ===
399
+ probeProvisioner.bindingFingerprint;
400
+ if (!adoptProbe) {
401
+ await releaseProbe(probe, "succeeded");
402
+ }
403
+ return await createDeployment({
404
+ request: args,
405
+ approved: narrowed,
406
+ provisioner: deploymentProvisioner,
407
+ probe,
408
+ adoptProbe,
409
+ });
410
+ }
411
+ catch (error) {
412
+ try {
413
+ await releaseProbe(probe, "failed", {
414
+ code: "probe_failed",
415
+ message: error instanceof Error ? error.message : String(error),
416
+ });
417
+ }
418
+ catch {
419
+ // Preserve the primary probe/provisioning error. The durable
420
+ // `releasing` row remains discoverable for periodic cleanup.
421
+ }
422
+ throw error;
423
+ }
424
+ }
425
+ async function deployReadyAllocation(allocation, reconciliation) {
426
+ reconciliation.signal.throwIfAborted();
427
+ if (allocation.status !== "allocated" ||
428
+ allocation.ensureAcceptedGeneration !== allocation.generation) {
429
+ throw new Error(`Allocation ${allocation.id} generation ${String(allocation.generation)} is not accepted for deployment`);
430
+ }
431
+ const allocationTarget = {
432
+ allocationId: allocation.id,
433
+ generation: allocation.generation,
434
+ };
435
+ const anchor = await db.query.workflowRun.findFirst({
436
+ where: eq(workflowRun.id, allocation.anchorRunId),
437
+ columns: { publicKey: true, definitionId: true },
438
+ });
439
+ if (anchor === undefined) {
440
+ throw new Error(`Allocation ${allocation.id} has no workflow anchor run`);
441
+ }
442
+ if (await allocationRouter.isAllocatedWorkflowActive(allocationTarget)) {
443
+ if (anchor.publicKey !== null)
444
+ return null;
445
+ throw new SessionLaunchError("provision", new Error(`Allocation ${allocation.id} has an active workflow without a completed initialization key`), true);
446
+ }
447
+ if (anchor.definitionId === null) {
448
+ throw new Error(`Allocation ${allocation.id} anchor run has no frozen workflow definition`);
449
+ }
450
+ const spec = await launchSpecStore.get(allocation.anchorRunId);
451
+ if (spec === null) {
452
+ throw new Error(`Allocation ${allocation.id} has no workflow launch specification`);
453
+ }
454
+ // The frozen bundle deploys verbatim -- no re-probe. Rehydrate the approval
455
+ // hand-off from it: the persisted flat item list is partitioned back into
456
+ // the gate's `ApprovalSet`, and the frozen definition id is the anchor's own
457
+ // (set at prepare time from this freeze).
458
+ const bundle = spec.frozenApprovalBundle;
459
+ const approved = {
460
+ approval: {
461
+ ok: true,
462
+ definitionId: anchor.definitionId,
463
+ approvedWireHash: bundle.approvedWireHash,
464
+ approvedSurface: approvalSetFromItems(bundle.approvedGrants),
465
+ projection: bundle.projection,
466
+ },
467
+ projection: bundle.projection,
468
+ closure: bundle.closure,
469
+ };
470
+ // Re-resolve the inference chain from the catalog at launch time -- the
471
+ // launch spec stores offering ids, never resolved sources, so a rotated
472
+ // credential is picked up here and no secret was ever persisted.
473
+ const resolved = await resolveSourcesByOfferingIds(db, allocation.tenantId, spec.sourceOfferingIds, credentialCipher);
474
+ if (!resolved.ok) {
475
+ throw new Error(`Catalog offering ${resolved.offeringId} is unavailable while recovering allocation ${allocation.id}`);
476
+ }
477
+ const defaultSource = resolved.sources.find((source) => source.id === spec.defaultSourceOfferingId);
478
+ if (defaultSource === undefined) {
479
+ throw new Error(`Default offering ${spec.defaultSourceOfferingId} was not resolved for allocation ${allocation.id}`);
480
+ }
481
+ const config = createProvisionedHarnessConfig({
482
+ tenantId: allocation.tenantId,
483
+ anchorRunId: allocation.anchorRunId,
484
+ deploymentDomain: spec.deploymentDomain,
485
+ sessionId: spec.sessionId,
486
+ sourceAuthorityPrincipalId: spec.sourceAuthorityPrincipalId,
487
+ sources: resolved.sources,
488
+ defaultSource: defaultSource.id,
489
+ });
490
+ reconciliation.signal.throwIfAborted();
491
+ return preparedDeployer.deployPreparedCodeSourcedWorkflow({
492
+ reconciliation,
493
+ tenantId: allocation.tenantId,
494
+ anchorRunId: allocation.anchorRunId,
495
+ deploymentDomain: spec.deploymentDomain,
496
+ agentAddress: config.agentAddress,
497
+ source: bundle.source,
498
+ approved,
499
+ config,
500
+ allocationTarget,
501
+ credentialCipher,
502
+ });
503
+ }
504
+ function cleanupFinalStatus(probe) {
505
+ return probe.failureCode === null ? "succeeded" : "failed";
506
+ }
507
+ async function reconcileReleasingProbes() {
508
+ const failures = [];
509
+ for (const probe of await probeStore.listReleasing()) {
510
+ try {
511
+ await releaseProbe(probe, cleanupFinalStatus(probe));
512
+ }
513
+ catch (error) {
514
+ failures.push(error);
515
+ }
516
+ }
517
+ if (failures.length > 0) {
518
+ throw new AggregateError(failures, "Failed to clean up releasing workflow probes");
519
+ }
520
+ }
521
+ async function initialize() {
522
+ const failures = [];
523
+ for (const probe of await probeStore.listActive()) {
524
+ try {
525
+ await beginProbeRelease(probe, probe.status === "releasing"
526
+ ? undefined
527
+ : {
528
+ code: "probe_interrupted",
529
+ message: "Hub restarted before the workflow probe completed",
530
+ });
531
+ }
532
+ catch (error) {
533
+ failures.push(error);
534
+ }
535
+ }
536
+ if (failures.length > 0) {
537
+ throw new AggregateError(failures, "Failed to mark interrupted workflow probes for cleanup");
538
+ }
539
+ }
540
+ return {
541
+ initialize,
542
+ reconcileReleasingProbes,
543
+ prepareProvisionedDeployment,
544
+ deployReadyAllocation,
545
+ };
546
+ }
547
+ function formatProvisionerMismatches(mismatches) {
548
+ const details = Object.entries(mismatches)
549
+ .sort(([left], [right]) => left.localeCompare(right))
550
+ .flatMap(([provisionerId, failures]) => failures.map(({ capability, expected, actual }) => `${provisionerId}: ${capability} must be ${expected}, was ${actual}`));
551
+ return details.length === 0
552
+ ? "No sidecar provisioners are registered"
553
+ : `No sidecar provisioner satisfies the deployment (${details.join("; ")})`;
554
+ }
@@ -0,0 +1,106 @@
1
+ import { type PackumentFetcher, type RegistryConfig } from "@intx/tool-packaging";
2
+ import type { WorkflowSourceAssetMount } from "@intx/types/sidecar";
3
+ import type { ToolPackageManifest } from "@intx/types/tool-packages";
4
+ import type { WorkflowDefinitionAssetSource, WorkflowDefinitionRegistrySource } from "@intx/types/workflow-sources";
5
+ import { type SourceTreeReads } from "./workflow-source-closure.js";
6
+ /**
7
+ * Resolve a workflow definition's dependency closure against the npm registry
8
+ * the source names.
9
+ */
10
+ export type ResolveWorkflowClosureRegistryArgs = {
11
+ readonly source: WorkflowDefinitionRegistrySource;
12
+ /** A `name@range` spec for the workflow definition package. */
13
+ readonly pin: string;
14
+ /** URL and credentials for the registry named by `source.registry`. */
15
+ readonly registryConfig: RegistryConfig;
16
+ /**
17
+ * Test seam for packument fetches. Omitted in production, where the
18
+ * default `npm-registry-fetch` wrapper reaches the configured URL.
19
+ */
20
+ readonly fetchPackument?: PackumentFetcher;
21
+ };
22
+ /**
23
+ * Resolve a workflow definition's dependency closure against a single hub
24
+ * `package-registry` asset.
25
+ *
26
+ * The resolver is single-source by design: the asset is the sole and default
27
+ * registry, so a self-contained definition (its `@intx/*` imports bundled at
28
+ * author time) resolves to one `kind:"asset"` entry. A specifier the asset does
29
+ * not publish fails loud from `AssetRegistrySource` -- there is no silent
30
+ * fallback to npm. A definition that genuinely needs external npm dependencies
31
+ * would resolve through the mixed `AssetRegistrySource` + `HttpRegistrySource`
32
+ * map `session-service` already builds for tool packages; that path is not
33
+ * threaded here.
34
+ */
35
+ export type ResolveWorkflowClosureAssetTarballArgs = {
36
+ /** An asset source whose `package.format` is `"tarball"`. */
37
+ readonly source: WorkflowDefinitionAssetSource;
38
+ /** A `name@range` spec for the workflow definition package. */
39
+ readonly pin: string;
40
+ /** Reads a blob at `path` from the asset the definition is sourced from. */
41
+ readonly readBlob: (path: string) => Promise<Uint8Array>;
42
+ /** Lists the blob names directly under `dir` in that asset. */
43
+ readonly listBlobs: (dir: string) => Promise<string[]>;
44
+ };
45
+ /**
46
+ * Resolve a source-format workflow definition's dependency closure from the
47
+ * asset's git tree at the pinned commit. The workspace-local members become
48
+ * `format:"source"` entries and the external npm deps resolve through the
49
+ * npm registry the caller supplies; there is no `name@range` pin (the member
50
+ * is selected from `source.package.packageName`).
51
+ */
52
+ export type ResolveWorkflowClosureAssetSourceArgs = {
53
+ /** An asset source whose `package.format` is `"source"`. */
54
+ readonly source: WorkflowDefinitionAssetSource;
55
+ /** Git-tree reads pinned to `source.package.commitSha`. */
56
+ readonly reads: SourceTreeReads;
57
+ /**
58
+ * The registry name external deps are stamped with in the frozen closure.
59
+ * Must be a name the sidecar's registry map is keyed by (its npm registry),
60
+ * since the sidecar resolves each external entry's `source.registry` against
61
+ * that map at materialization.
62
+ */
63
+ readonly registryName: string;
64
+ /** URL and credentials for the npm registry external deps resolve against. */
65
+ readonly registryConfig: RegistryConfig;
66
+ /**
67
+ * Test seam for packument fetches. Omitted in production, where the
68
+ * default `npm-registry-fetch` wrapper reaches the configured URL.
69
+ */
70
+ readonly fetchPackument?: PackumentFetcher;
71
+ };
72
+ export type ResolveWorkflowClosureArgs = ResolveWorkflowClosureRegistryArgs | ResolveWorkflowClosureAssetTarballArgs | ResolveWorkflowClosureAssetSourceArgs;
73
+ /**
74
+ * Resolve a workflow definition's dependency closure to concrete versions
75
+ * and integrity SRIs, reusing the tool-packaging closure resolver.
76
+ *
77
+ * @returns the frozen closure: a `ToolPackageManifest` whose `topLevel`
78
+ * pins the definition package and whose `entries` carry the concrete
79
+ * version and integrity SRI of every transitive dependency.
80
+ */
81
+ export declare function resolveWorkflowClosure(args: ResolveWorkflowClosureArgs): Promise<ToolPackageManifest>;
82
+ /**
83
+ * The workspace-relative mount path a workflow definition's source asset is
84
+ * delivered under, keyed by asset id. Both the probe (inline delivery) and the
85
+ * deploy (streamed fan-out) build their `assetId -> mountPath` maps through
86
+ * this one helper, so the same asset lands at the same path on both paths.
87
+ */
88
+ export declare function workflowSourceAssetMountPath(assetId: string): string;
89
+ /**
90
+ * Resolves an asset (by id) to the git pack a source-ref frame delivers for it.
91
+ * The caller binds the ref: an asset source binds the asset's default ref, so
92
+ * the install/deploy glue stays ref-agnostic.
93
+ */
94
+ export type ResolveAssetAttachmentFn = (assetId: string) => Promise<{
95
+ pack: Uint8Array;
96
+ ref: string;
97
+ commitSha: string;
98
+ }>;
99
+ /**
100
+ * Build the inline asset mounts a source-ref frame delivers for an asset-sourced
101
+ * closure: one per distinct asset the closure's `kind:"asset"` entries name (so
102
+ * a multi-asset closure delivers every backing asset, not just the source), each
103
+ * carrying the git pack the caller resolves for that asset. `mountPath` is
104
+ * derived through the shared helper so probe and deploy agree on it.
105
+ */
106
+ export declare function buildSourceAssetMounts(closure: ToolPackageManifest, resolveAttachment: ResolveAssetAttachmentFn): Promise<WorkflowSourceAssetMount[]>;