@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.
- package/README.md +3 -5
- package/dist/agent-repo.d.ts +23 -7
- package/dist/agent-repo.js +19 -6
- package/dist/agent-state-kind.js +18 -63
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +19 -97
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +52 -7
- package/dist/credential-push.js +178 -22
- package/dist/event-collector-registry.d.ts +3 -2
- package/dist/event-collector-registry.js +42 -13
- package/dist/event-collector.d.ts +12 -2
- package/dist/event-collector.js +45 -4
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +541 -86
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +22 -56
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +2 -1
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +252 -126
- package/dist/session-service.js +851 -624
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +101 -0
- package/dist/sidecar-allocation/contracts.js +26 -0
- package/dist/sidecar-allocation/index.d.ts +5 -0
- package/dist/sidecar-allocation/index.js +4 -0
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
- package/dist/sidecar-allocation/plugin-registry.js +61 -0
- package/dist/sidecar-allocation/reconciler.d.ts +54 -0
- package/dist/sidecar-allocation/reconciler.js +825 -0
- package/dist/skill-kind.js +12 -62
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +64 -0
- package/dist/workflow-allocation-service.js +554 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +42 -0
- package/dist/workflow-dispatch-service.js +209 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +29 -1
- package/dist/workflow-kind.js +143 -139
- package/dist/workflow-probe-gate.d.ts +286 -0
- package/dist/workflow-probe-gate.js +382 -0
- package/dist/workflow-run-kind.d.ts +239 -32
- package/dist/workflow-run-kind.js +874 -188
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +16 -0
- package/dist/workflow-run-restore.js +30 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +2 -2
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +139 -37
- package/dist/ws/sidecar-events.js +2 -2
- package/dist/ws/sidecar-handler.d.ts +218 -60
- package/dist/ws/sidecar-handler.js +1713 -732
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +78 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- 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[]>;
|