@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.
- package/dist/agent-repo.d.ts +14 -2
- package/dist/agent-repo.js +17 -4
- package/dist/agent-state-kind.js +14 -63
- package/dist/asset-service.js +14 -10
- package/dist/credential-push.d.ts +48 -4
- package/dist/credential-push.js +138 -6
- package/dist/event-collector-registry.d.ts +2 -1
- package/dist/event-collector-registry.js +38 -9
- package/dist/event-collector.d.ts +11 -1
- package/dist/event-collector.js +36 -3
- package/dist/hub-session-lookups.d.ts +1 -1
- package/dist/hub-session-lookups.js +68 -72
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +13 -12
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +1 -0
- package/dist/repo-store/index.js +1 -0
- 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 +66 -121
- package/dist/session-service.js +444 -411
- 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 +29 -6
- package/dist/sidecar-allocation/contracts.js +7 -2
- package/dist/sidecar-allocation/index.d.ts +4 -3
- package/dist/sidecar-allocation/index.js +3 -2
- 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 +16 -3
- package/dist/sidecar-allocation/plugin-registry.js +36 -12
- package/dist/sidecar-allocation/reconciler.d.ts +16 -4
- package/dist/sidecar-allocation/reconciler.js +486 -92
- package/dist/skill-kind.js +8 -62
- package/dist/substrate.d.ts +1 -1
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +21 -15
- package/dist/workflow-allocation-service.js +440 -125
- package/dist/workflow-dispatch-service.d.ts +4 -2
- package/dist/workflow-dispatch-service.js +89 -26
- package/dist/workflow-kind.d.ts +12 -0
- package/dist/workflow-kind.js +17 -60
- package/dist/workflow-probe-gate.d.ts +99 -27
- package/dist/workflow-probe-gate.js +196 -21
- package/dist/workflow-run-kind.d.ts +112 -19
- package/dist/workflow-run-kind.js +626 -210
- package/dist/workflow-run-restore.d.ts +1 -0
- package/dist/workflow-run-restore.js +5 -1
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +1 -1
- package/dist/ws/index.js +1 -1
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +43 -29
- package/dist/ws/sidecar-events.js +0 -2
- package/dist/ws/sidecar-handler.d.ts +122 -85
- package/dist/ws/sidecar-handler.js +925 -878
- 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.js +37 -23
- package/package.json +13 -13
- package/dist/sidecar-allocation/placement-policy.d.ts +0 -11
- package/dist/sidecar-allocation/placement-policy.js +0 -21
|
@@ -1,120 +1,201 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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,
|
|
6
|
-
import { deriveRunAddress, deriveRunAgentId } from "@intx/workflow-deploy";
|
|
7
|
-
import {
|
|
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
|
-
|
|
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 = "
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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:
|
|
86
|
-
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: [
|
|
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:
|
|
104
|
-
tenantId:
|
|
105
|
-
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:
|
|
116
|
-
principalId:
|
|
117
|
-
resource: `workflow-run:${
|
|
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:
|
|
126
|
-
sessionId:
|
|
127
|
-
deploymentDomain:
|
|
128
|
-
sourceAuthorityPrincipalId:
|
|
206
|
+
anchorRunId: request.anchorRunId,
|
|
207
|
+
sessionId: request.sessionId,
|
|
208
|
+
deploymentDomain: request.deploymentDomain,
|
|
209
|
+
sourceAuthorityPrincipalId: request.sourceAuthorityPrincipalId,
|
|
129
210
|
frozenApprovalBundle,
|
|
130
|
-
sourceOfferingIds: [...
|
|
131
|
-
defaultSourceOfferingId:
|
|
132
|
-
deployContent:
|
|
133
|
-
...(
|
|
134
|
-
? { 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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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:
|
|
262
|
+
anchorRunId: request.anchorRunId,
|
|
151
263
|
deploymentAddress,
|
|
152
264
|
allocationId,
|
|
153
265
|
status: "pending",
|
|
154
266
|
};
|
|
155
267
|
}
|
|
156
|
-
async function
|
|
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
|
|
186
|
-
// definition id is the anchor's own
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
496
|
+
agentAddress: config.agentAddress,
|
|
231
497
|
source: bundle.source,
|
|
232
498
|
approved,
|
|
233
499
|
config,
|
|
234
500
|
allocationTarget,
|
|
235
|
-
|
|
501
|
+
credentialCipher,
|
|
236
502
|
});
|
|
237
503
|
}
|
|
238
|
-
|
|
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
|
}
|