@intx/hub-sessions 0.3.0 → 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 (67) hide show
  1. package/dist/agent-repo.d.ts +14 -2
  2. package/dist/agent-repo.js +17 -4
  3. package/dist/agent-state-kind.js +14 -63
  4. package/dist/asset-service.js +14 -10
  5. package/dist/credential-push.d.ts +48 -4
  6. package/dist/credential-push.js +138 -6
  7. package/dist/event-collector-registry.d.ts +2 -1
  8. package/dist/event-collector-registry.js +38 -9
  9. package/dist/event-collector.d.ts +11 -1
  10. package/dist/event-collector.js +36 -3
  11. package/dist/hub-session-lookups.d.ts +1 -1
  12. package/dist/hub-session-lookups.js +68 -72
  13. package/dist/hub-session-orchestrator.d.ts +2 -3
  14. package/dist/hub-session-orchestrator.js +13 -12
  15. package/dist/index.d.ts +7 -6
  16. package/dist/index.js +7 -6
  17. package/dist/reconciliation-scheduler.d.ts +14 -0
  18. package/dist/reconciliation-scheduler.js +55 -0
  19. package/dist/repo-store/index.d.ts +1 -0
  20. package/dist/repo-store/index.js +1 -0
  21. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  22. package/dist/repo-store/user-principal-gate.js +78 -0
  23. package/dist/session-service.d.ts +66 -121
  24. package/dist/session-service.js +444 -411
  25. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  26. package/dist/sidecar-allocation/capability-policy.js +124 -0
  27. package/dist/sidecar-allocation/contracts.d.ts +29 -6
  28. package/dist/sidecar-allocation/contracts.js +7 -2
  29. package/dist/sidecar-allocation/index.d.ts +4 -3
  30. package/dist/sidecar-allocation/index.js +3 -2
  31. package/dist/sidecar-allocation/operation.d.ts +10 -0
  32. package/dist/sidecar-allocation/operation.js +54 -0
  33. package/dist/sidecar-allocation/plugin-registry.d.ts +16 -3
  34. package/dist/sidecar-allocation/plugin-registry.js +36 -12
  35. package/dist/sidecar-allocation/reconciler.d.ts +16 -4
  36. package/dist/sidecar-allocation/reconciler.js +486 -92
  37. package/dist/skill-kind.js +8 -62
  38. package/dist/substrate.d.ts +1 -1
  39. package/dist/substrate.js +1 -1
  40. package/dist/workflow-allocation-service.d.ts +21 -15
  41. package/dist/workflow-allocation-service.js +440 -125
  42. package/dist/workflow-dispatch-service.d.ts +4 -2
  43. package/dist/workflow-dispatch-service.js +89 -26
  44. package/dist/workflow-kind.d.ts +12 -0
  45. package/dist/workflow-kind.js +17 -60
  46. package/dist/workflow-probe-gate.d.ts +99 -27
  47. package/dist/workflow-probe-gate.js +196 -21
  48. package/dist/workflow-run-kind.d.ts +112 -19
  49. package/dist/workflow-run-kind.js +626 -210
  50. package/dist/workflow-run-restore.d.ts +1 -0
  51. package/dist/workflow-run-restore.js +5 -1
  52. package/dist/workflow-source-pins.d.ts +8 -0
  53. package/dist/workflow-source-pins.js +14 -0
  54. package/dist/ws/index.d.ts +1 -1
  55. package/dist/ws/index.js +1 -1
  56. package/dist/ws/pending-tracker.d.ts +93 -0
  57. package/dist/ws/pending-tracker.js +132 -0
  58. package/dist/ws/sidecar-events.d.ts +43 -29
  59. package/dist/ws/sidecar-events.js +0 -2
  60. package/dist/ws/sidecar-handler.d.ts +122 -85
  61. package/dist/ws/sidecar-handler.js +925 -878
  62. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  63. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  64. package/dist/ws/sidecar-token-authenticator.js +37 -23
  65. package/package.json +13 -13
  66. package/dist/sidecar-allocation/placement-policy.d.ts +0 -11
  67. package/dist/sidecar-allocation/placement-policy.js +0 -21
@@ -1,120 +1,201 @@
1
- import { createSidecarAllocationStore, createWorkflowRunLaunchSpecStore, getAncestorChain, resolveSourcesByOfferingIds, } from "@intx/db";
2
- import { grant, tenant as tenantTable, workflowRun } from "@intx/db/schema";
3
- import { eq, inArray } from "drizzle-orm";
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";
4
6
  import { generateId } from "@intx/hub-common";
5
- import { hexEncode, TenantConfig, } from "@intx/types";
6
- import { deriveRunAddress, deriveRunAgentId } from "@intx/workflow-deploy";
7
- import { resolveEffectiveSidecarPlacement, } from "./sidecar-allocation/index.js";
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";
8
10
  import { SessionLaunchError, } from "./session-service.js";
9
- export class ExclusiveWorkflowPlacementError extends Error {
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 {
10
14
  code;
11
15
  constructor(code, message) {
12
16
  super(message);
13
- this.name = "ExclusiveWorkflowPlacementError";
17
+ this.name = "WorkflowProvisioningError";
14
18
  this.code = code;
15
19
  }
16
20
  }
17
21
  function randomAllocationId() {
18
22
  return `sal_${hexEncode(crypto.getRandomValues(new Uint8Array(16)))}`;
19
23
  }
20
- /** Resolve the tenant-inherited sidecar placement into one launch decision. */
21
- export async function resolveWorkflowSidecarPlacement(db, tenantId) {
22
- const tenantIds = await getAncestorChain(db, tenantId);
23
- const rows = await db.query.tenant.findMany({
24
- where: inArray(tenantTable.id, tenantIds),
25
- columns: { id: true, config: true },
26
- });
27
- const configsByTenantId = new Map(rows.map((row) => [
28
- row.id,
29
- row.config === null
30
- ? TenantConfig.assert({})
31
- : TenantConfig.assert(row.config),
32
- ]));
33
- const tenantConfigs = tenantIds.map((id) => {
34
- const config = configsByTenantId.get(id);
35
- if (config === undefined) {
36
- throw new Error(`Tenant ${id} disappeared while resolving sidecar placement`);
37
- }
38
- return config;
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,
39
53
  });
40
- return resolveEffectiveSidecarPlacement({ tenantConfigs });
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
+ };
41
66
  }
42
- export function createWorkflowAllocationService({ db, plugins, preparedDeployer, credentialCipher, allocationRouter, createAllocationId = randomAllocationId, now = () => new Date(), }) {
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(), }) {
43
68
  const allocationStore = createSidecarAllocationStore(db);
69
+ const probeStore = createWorkflowProbeStore(db);
44
70
  const launchSpecStore = createWorkflowRunLaunchSpecStore(db);
45
- async function prepareExclusiveDeployment(args) {
46
- // Fail closed before any probe: exclusive placement has no meaning without a
47
- // provisioner to stand up its dedicated sidecar, and the shared-capacity
48
- // probe below is wasted work if no provisioner exists. (In-tree this is
49
- // always null -- exclusive is dormant -- so a live prepare never runs here.)
50
- const provisioner = plugins.getDefaultProvisioner();
51
- if (provisioner === null) {
52
- throw new ExclusiveWorkflowPlacementError("exclusive_provisioner_unavailable", "No default sidecar provisioner is configured for exclusive workflows");
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`);
53
123
  }
54
- if (args.sourceOfferingIds.length === 0) {
55
- throw new ExclusiveWorkflowPlacementError("invalid_source_offerings", "Exclusive workflow deployment requires at least one catalog offering");
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
+ });
56
146
  }
57
- if (!args.sourceOfferingIds.includes(args.defaultSourceOfferingId)) {
58
- throw new ExclusiveWorkflowPlacementError("invalid_source_offerings", "The default source offering must be present in the source offering chain");
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`);
59
150
  }
60
- const sourceCheck = await resolveSourcesByOfferingIds(db, args.tenantId, args.sourceOfferingIds, credentialCipher);
61
- if (!sourceCheck.ok) {
62
- throw new ExclusiveWorkflowPlacementError("source_offering_unavailable", `Catalog offering ${sourceCheck.offeringId} cannot be used by the deployment authority`);
63
- }
64
- // Probe + gate + freeze the code-sourced definition ONCE, on shared
65
- // capacity, at request time. The freeze is sidecar-agnostic (a wire hash
66
- // over the inert projection plus the resolved closure), so the frozen bundle
67
- // deploys later to the dedicated allocation with no re-probe. A non-approval
68
- // surfaces as a `WorkflowDefinitionInvalidError` from the deployer, which the
69
- // route maps to a 409.
70
- const approved = await preparedDeployer.installAndApproveWorkflowSource({
71
- source: args.source,
72
- entry: args.entry,
73
- ...(args.pin !== undefined ? { pin: args.pin } : {}),
74
- definitionAssetId: args.definitionAssetId,
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,
75
173
  });
76
- if (!approved.approval.ok) {
77
- // `installAndApproveWorkflowSource` already fails closed on a non-approval;
78
- // restate the narrowing so the frozen bundle below reads the ok arm.
79
- throw new Error("prepareExclusiveDeployment: install did not yield an approved definition");
80
- }
81
- // Capture the narrowed values before the transaction: TS drops the
82
- // `approval.ok` narrowing inside the async callback below.
83
- const definitionId = approved.approval.definitionId;
84
174
  const frozenApprovalBundle = {
85
- source: args.source,
86
- entry: args.entry,
175
+ source: request.source,
176
+ entry: request.entry,
87
177
  projection: approved.projection,
88
178
  closure: approved.closure,
89
179
  approvedWireHash: approved.approval.approvedWireHash,
90
- approvedGrants: [...approved.approval.approvedGrants],
180
+ approvedGrants: [
181
+ ...approvalItemsFromSet(approved.approval.approvedSurface),
182
+ ],
91
183
  };
92
- const allocationId = createAllocationId();
93
- const createdAt = now();
94
- const deploymentAddress = deriveRunAddress({
95
- runId: args.anchorRunId,
96
- domain: args.deploymentDomain,
97
- });
98
184
  await db.transaction(async (tx) => {
99
- // The freeze already ensured (create-if-absent) the definition row keyed by
100
- // the approved wire hash and returned its id; anchor to THAT row rather
101
- // than re-ensuring, so the anchor's definition is exactly the one approved.
102
185
  await tx.insert(workflowRun).values({
103
- id: args.anchorRunId,
104
- tenantId: args.tenantId,
105
- anchorRunId: args.anchorRunId,
106
- definitionId,
186
+ id: request.anchorRunId,
187
+ tenantId: request.tenantId,
188
+ anchorRunId: request.anchorRunId,
189
+ definitionId: approved.approval.definitionId,
107
190
  address: deploymentAddress,
108
- // Born "deployed": the anchor is live but pre-trigger. The first trigger
109
- // flips it to "running" (see `anchorWithPrincipal`).
110
191
  status: "deployed",
111
192
  createdAt,
112
193
  });
113
194
  await tx.insert(grant).values({
114
195
  id: generateId("grant"),
115
- tenantId: args.tenantId,
116
- principalId: args.sourceAuthorityPrincipalId,
117
- resource: `workflow-run:${args.anchorRunId}`,
196
+ tenantId: request.tenantId,
197
+ principalId: request.sourceAuthorityPrincipalId,
198
+ resource: `workflow-run:${request.anchorRunId}`,
118
199
  action: "read",
119
200
  effect: "allow",
120
201
  origin: "creator",
@@ -122,38 +203,227 @@ export function createWorkflowAllocationService({ db, plugins, preparedDeployer,
122
203
  updatedAt: createdAt,
123
204
  });
124
205
  await launchSpecStore.create({
125
- anchorRunId: args.anchorRunId,
126
- sessionId: args.sessionId,
127
- deploymentDomain: args.deploymentDomain,
128
- sourceAuthorityPrincipalId: args.sourceAuthorityPrincipalId,
206
+ anchorRunId: request.anchorRunId,
207
+ sessionId: request.sessionId,
208
+ deploymentDomain: request.deploymentDomain,
209
+ sourceAuthorityPrincipalId: request.sourceAuthorityPrincipalId,
129
210
  frozenApprovalBundle,
130
- sourceOfferingIds: [...args.sourceOfferingIds],
131
- defaultSourceOfferingId: args.defaultSourceOfferingId,
132
- deployContent: args.deployContent,
133
- ...(args.toolPackagePins !== undefined
134
- ? { toolPackagePins: [...args.toolPackagePins] }
211
+ sourceOfferingIds: [...request.sourceOfferingIds],
212
+ defaultSourceOfferingId: request.defaultSourceOfferingId,
213
+ deployContent: request.deployContent,
214
+ ...(request.toolPackagePins !== undefined
215
+ ? { toolPackagePins: [...request.toolPackagePins] }
135
216
  : {}),
136
217
  createdAt,
137
218
  }, tx);
138
- await allocationStore.createPending({
139
- id: allocationId,
140
- anchorRunId: args.anchorRunId,
141
- tenantId: args.tenantId,
142
- provisionerId: provisioner.id,
143
- provisionerApiVersion: provisioner.apiVersion,
144
- provisionerBindingFingerprint: provisioner.bindingFingerprint,
145
- placement: args.placement,
146
- now: createdAt,
147
- }, 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
+ }
148
254
  });
255
+ if (adoptProbe) {
256
+ allocationRouter.disconnectAllocation({
257
+ allocationId: probe.id,
258
+ generation: probe.generation,
259
+ });
260
+ }
149
261
  return {
150
- anchorRunId: args.anchorRunId,
262
+ anchorRunId: request.anchorRunId,
151
263
  deploymentAddress,
152
264
  allocationId,
153
265
  status: "pending",
154
266
  };
155
267
  }
156
- async function deployReadyAllocation(allocation) {
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();
157
427
  if (allocation.status !== "allocated" ||
158
428
  allocation.ensureAcceptedGeneration !== allocation.generation) {
159
429
  throw new Error(`Allocation ${allocation.id} generation ${String(allocation.generation)} is not accepted for deployment`);
@@ -182,15 +452,16 @@ export function createWorkflowAllocationService({ db, plugins, preparedDeployer,
182
452
  throw new Error(`Allocation ${allocation.id} has no workflow launch specification`);
183
453
  }
184
454
  // The frozen bundle deploys verbatim -- no re-probe. Rehydrate the approval
185
- // hand-off from it: the approved grant set becomes a `Set`, and the frozen
186
- // definition id is the anchor's own (set at prepare time from this freeze).
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).
187
458
  const bundle = spec.frozenApprovalBundle;
188
459
  const approved = {
189
460
  approval: {
190
461
  ok: true,
191
462
  definitionId: anchor.definitionId,
192
463
  approvedWireHash: bundle.approvedWireHash,
193
- approvedGrants: new Set(bundle.approvedGrants),
464
+ approvedSurface: approvalSetFromItems(bundle.approvedGrants),
194
465
  projection: bundle.projection,
195
466
  },
196
467
  projection: bundle.projection,
@@ -207,33 +478,77 @@ export function createWorkflowAllocationService({ db, plugins, preparedDeployer,
207
478
  if (defaultSource === undefined) {
208
479
  throw new Error(`Default offering ${spec.defaultSourceOfferingId} was not resolved for allocation ${allocation.id}`);
209
480
  }
210
- const deploymentAddress = deriveRunAddress({
211
- runId: allocation.anchorRunId,
212
- domain: spec.deploymentDomain,
213
- });
214
- const config = {
215
- sessionId: spec.sessionId,
216
- agentId: deriveRunAgentId({ runId: allocation.anchorRunId }),
481
+ const config = createProvisionedHarnessConfig({
217
482
  tenantId: allocation.tenantId,
218
- principalId: spec.sourceAuthorityPrincipalId,
219
- agentAddress: deploymentAddress,
220
- systemPrompt: "",
221
- tools: [],
222
- grants: [],
483
+ anchorRunId: allocation.anchorRunId,
484
+ deploymentDomain: spec.deploymentDomain,
485
+ sessionId: spec.sessionId,
486
+ sourceAuthorityPrincipalId: spec.sourceAuthorityPrincipalId,
223
487
  sources: resolved.sources,
224
488
  defaultSource: defaultSource.id,
225
- };
489
+ });
490
+ reconciliation.signal.throwIfAborted();
226
491
  return preparedDeployer.deployPreparedCodeSourcedWorkflow({
492
+ reconciliation,
227
493
  tenantId: allocation.tenantId,
228
494
  anchorRunId: allocation.anchorRunId,
229
495
  deploymentDomain: spec.deploymentDomain,
230
- agentAddress: deploymentAddress,
496
+ agentAddress: config.agentAddress,
231
497
  source: bundle.source,
232
498
  approved,
233
499
  config,
234
500
  allocationTarget,
235
- ...(credentialCipher !== undefined ? { credentialCipher } : {}),
501
+ credentialCipher,
236
502
  });
237
503
  }
238
- return { prepareExclusiveDeployment, deployReadyAllocation };
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("; ")})`;
239
554
  }