@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
package/dist/session-service.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
|
-
import { and, eq } from "drizzle-orm";
|
|
3
|
-
import { createDefaultDirectorRegistry, } from "@intx/agent";
|
|
2
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
3
|
import { getLogger } from "@intx/log";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { generateId } from "@intx/hub-common";
|
|
10
|
-
import { sessionAsset as sessionAssetTable, } from "@intx/db/schema";
|
|
4
|
+
import { buildCredentialDelivery, createSidecarAllocationStore, listAssetsForTenant, resolveInferenceMaterials, } from "@intx/db";
|
|
5
|
+
import { workflowDefinition as workflowDefinitionTable, workflowRun as workflowRunTable, } from "@intx/db/schema";
|
|
6
|
+
import { hexEncode } from "@intx/types";
|
|
7
|
+
import { sessionAsset as sessionAssetTable } from "@intx/db/schema";
|
|
11
8
|
import { AssetRegistrySource, HttpRegistrySource, ManifestInvalidError, createClosureResolver, } from "@intx/tool-packaging";
|
|
12
9
|
import { ToolPackageManifest, } from "@intx/types/tool-packages";
|
|
13
|
-
import {
|
|
14
|
-
import { createWorkflowDeployOrchestrator, deriveDeploymentAddress, walkCapabilities, wrapHarnessAsSingleStepWorkflow, } from "@intx/workflow-deploy";
|
|
10
|
+
import { buildInertProjectionStepSources, collectAgentBearingStepIds, deriveRunAddress, WorkflowDefinitionInvalidError, } from "@intx/workflow-deploy";
|
|
15
11
|
import { DEFAULT_ASSET_REF, } from "./asset-service.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
12
|
+
import { isDeployFrameFailure } from "./ws/sidecar-handler.js";
|
|
13
|
+
import { buildSourceAssetMounts, } from "./workflow-closure-resolution.js";
|
|
14
|
+
import { restoreWorkflowRunToAllocation } from "./workflow-run-restore.js";
|
|
15
|
+
import { committedReadsToSourceTree } from "./committed-source-tree.js";
|
|
16
|
+
import { installAndApproveWorkflowDefinition, } from "./workflow-probe-gate.js";
|
|
17
|
+
import { buildReferencedWorkflowSourcePins } from "./workflow-source-pins.js";
|
|
18
18
|
const logger = getLogger(["interchange", "hub", "session-service"]);
|
|
19
19
|
export class SessionLaunchError extends Error {
|
|
20
20
|
/** Which phase failed: "write", "provision", "pack", or "start". */
|
|
@@ -29,9 +29,9 @@ export class SessionLaunchError extends Error {
|
|
|
29
29
|
this.leakedAgent = leakedAgent;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
// Hub-side principal for reading
|
|
33
|
-
// hub itself, and
|
|
34
|
-
// packs for delivery to a sidecar
|
|
32
|
+
// Hub-side principal for reading asset repos. Assets are signed by the
|
|
33
|
+
// hub itself, and the launch fan-out reads them on the hub to assemble
|
|
34
|
+
// packs for delivery to a sidecar -- so the hub principal is correct.
|
|
35
35
|
const HUB_PRINCIPAL = { kind: "hub" };
|
|
36
36
|
async function createPackSha(pack) {
|
|
37
37
|
const digest = await crypto.subtle.digest("SHA-256",
|
|
@@ -59,75 +59,6 @@ function collectDistinctAssetIds(manifest) {
|
|
|
59
59
|
}
|
|
60
60
|
return out;
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Dedup the union of `direct` and `resolved` attachments by asset id
|
|
64
|
-
* (taken from `repoId.id`), with `direct` taking precedence whenever
|
|
65
|
-
* both name the same asset.
|
|
66
|
-
*
|
|
67
|
-
* The package-registry "both name the same asset" case is refused
|
|
68
|
-
* upstream at the resolver block (a direct attachment plus a resolver
|
|
69
|
-
* pin for the same package-registry asset would emit assetMounts at
|
|
70
|
-
* the resolver's ref while the direct attachment materializes at the
|
|
71
|
-
* operator's chosen ref, leaving the loader to resolve manifest
|
|
72
|
-
* entries against tarballs that do not exist at the materialized
|
|
73
|
-
* mount). Skill attachments cannot collide via the resolver path —
|
|
74
|
-
* the resolver only emits package-registry entries — so the dedup
|
|
75
|
-
* still has to handle skill self-collisions defensively and to fall
|
|
76
|
-
* through cleanly when both sources happen to name an asset the
|
|
77
|
-
* upstream check has not flagged.
|
|
78
|
-
*
|
|
79
|
-
* The function takes the two sources as named parameters rather than a
|
|
80
|
-
* pre-merged list so the precedence rule is structural: a future
|
|
81
|
-
* refactor cannot accidentally swap the order by re-arranging an
|
|
82
|
-
* intermediate spread.
|
|
83
|
-
*/
|
|
84
|
-
function dedupAttachmentsByAssetId(args) {
|
|
85
|
-
const seen = new Set();
|
|
86
|
-
const out = [];
|
|
87
|
-
for (const att of args.direct) {
|
|
88
|
-
if (seen.has(att.repoId.id))
|
|
89
|
-
continue;
|
|
90
|
-
seen.add(att.repoId.id);
|
|
91
|
-
out.push(att);
|
|
92
|
-
}
|
|
93
|
-
for (const att of args.resolved) {
|
|
94
|
-
if (seen.has(att.repoId.id))
|
|
95
|
-
continue;
|
|
96
|
-
seen.add(att.repoId.id);
|
|
97
|
-
out.push(att);
|
|
98
|
-
}
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Compute the materialization path for an attachment from the asset's
|
|
103
|
-
* kind and name. v1 does not let users override the path — the path is
|
|
104
|
-
* a function of the asset, full stop. Today only `skill` has a defined
|
|
105
|
-
* mapping (`skills/<asset.name>/`); other kinds reach this code path
|
|
106
|
-
* via the `never` branch and throw, per the defensive-coding rule that
|
|
107
|
-
* we never silently invent a default for an unhandled kind.
|
|
108
|
-
*
|
|
109
|
-
* Asset names are validated lowercase-kebab at `createAsset`, which is
|
|
110
|
-
* the only entry path into this function, so the resulting path is
|
|
111
|
-
* safe under `applyAssetPack`'s per-segment validator.
|
|
112
|
-
*/
|
|
113
|
-
function resolveMountPath(row) {
|
|
114
|
-
switch (row.asset.kind) {
|
|
115
|
-
case "skill":
|
|
116
|
-
return `skills/${row.asset.name}/`;
|
|
117
|
-
case "package-registry":
|
|
118
|
-
return `package-registries/${row.asset.name}/`;
|
|
119
|
-
case "agent-state":
|
|
120
|
-
throw new Error(`mount_path_required: agent_asset row ${row.id} references agent-state asset ${row.asset.id}; agent-state attachments are not supported`);
|
|
121
|
-
case "workflow":
|
|
122
|
-
throw new Error("kind handler not yet registered: workflow");
|
|
123
|
-
case "workflow-run":
|
|
124
|
-
throw new Error("kind handler not yet registered: workflow-run");
|
|
125
|
-
default: {
|
|
126
|
-
const exhaustive = row.asset.kind;
|
|
127
|
-
throw new Error(`mount_path_required: no default mountPath for asset kind ${String(exhaustive)} on row ${row.id}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
62
|
/**
|
|
132
63
|
* Translate the orchestrator's structural `DeployContent` (which types
|
|
133
64
|
* `toolPackageManifest` as `unknown`) back into the hub-sessions
|
|
@@ -155,118 +86,503 @@ export function bridgeOrchestratorDeployContent(content) {
|
|
|
155
86
|
return bridged;
|
|
156
87
|
}
|
|
157
88
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* sidecar's `agent.deploy.ack` is threaded back as the
|
|
164
|
-
* `MultiStepDeployResult.publicKey`.
|
|
89
|
+
* Emit the source-ref deploy frame onto `SidecarRouter.sendAgentDeploy`. The
|
|
90
|
+
* router accepts an optional `workflow` projection on the deploy frame; the
|
|
91
|
+
* sidecar's deploy router uses field presence to route the frame to the
|
|
92
|
+
* workflow deploy path, and returns the supervisor public key on the
|
|
93
|
+
* `agent.deploy.ack`.
|
|
165
94
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
95
|
+
* The gate/freeze layer already hashed the inert projection, so the frozen hash
|
|
96
|
+
* and the inert projection ride the frame verbatim -- this never recomputes the
|
|
97
|
+
* content hash. Recomputing over a live wire lineage would diverge from the
|
|
98
|
+
* inert projection the child re-verifies against.
|
|
99
|
+
*
|
|
100
|
+
* Exported so the co-located caller-site test can assert that the constructed
|
|
101
|
+
* closure reaches the wire surface via `sendAgentDeploy` with a `workflow`
|
|
102
|
+
* field structurally matching the `AgentDeployFrame.workflow` schema.
|
|
170
103
|
*/
|
|
171
|
-
export async function sendMultiStepDeployFrame(args) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
? { state: args.definition.state }
|
|
104
|
+
export async function sendMultiStepDeployFrame(args, signal, beforeSend) {
|
|
105
|
+
signal?.throwIfAborted();
|
|
106
|
+
const workflow = {
|
|
107
|
+
// The deploy frame carries no inline definition: the sidecar evaluates the
|
|
108
|
+
// pinned code closure from `sourceRef` and re-verifies it against
|
|
109
|
+
// `approvedWireHash`. Only the gate-frozen hash and the pin ride the frame.
|
|
110
|
+
sources: args.sources,
|
|
111
|
+
approvedWireHash: args.approvedWireHash,
|
|
112
|
+
sourceRef: args.sourceRef,
|
|
113
|
+
...(args.credentials !== undefined
|
|
114
|
+
? { credentials: args.credentials }
|
|
115
|
+
: {}),
|
|
116
|
+
...(args.referencedDefinitions !== undefined &&
|
|
117
|
+
args.referencedDefinitions.length > 0
|
|
118
|
+
? { referencedDefinitions: [...args.referencedDefinitions] }
|
|
119
|
+
: {}),
|
|
120
|
+
...(args.assets !== undefined && args.assets.length > 0
|
|
121
|
+
? { assets: [...args.assets] }
|
|
190
122
|
: {}),
|
|
191
123
|
};
|
|
192
|
-
return args.
|
|
193
|
-
|
|
194
|
-
|
|
124
|
+
return args.sidecarAllocationRouter.sendAgentDeployToAllocation(args.allocationTarget, args.agentAddress, args.config, workflow, signal, beforeSend);
|
|
125
|
+
}
|
|
126
|
+
function isAssetDeployArgs(args) {
|
|
127
|
+
return args.source.kind === "asset";
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The single public composition entrypoint for a code-sourced (npm) deploy. It
|
|
131
|
+
* consumes the approve output and builds the source-ref deploy frame internally,
|
|
132
|
+
* so the security-load-bearing hand-off -- frozen wire hash, inert projection,
|
|
133
|
+
* frozen closure -- is assembled in one place from one cohesive object rather
|
|
134
|
+
* than reassembled by each caller. The frozen approval's hash and projection
|
|
135
|
+
* ride the frame verbatim: nothing here recomputes the hash or re-resolves the
|
|
136
|
+
* closure, so the child re-verify over the inert projection matches the gate's
|
|
137
|
+
* freeze.
|
|
138
|
+
*
|
|
139
|
+
* Credential MATERIAL rides ONE `CredentialDelivery` delivered to the child on
|
|
140
|
+
* the frame, unioned from three rails and deduped by credentialId: tool bindings
|
|
141
|
+
* (grant-scoped, resolved here via `buildCredentialDelivery`); every top-level
|
|
142
|
+
* inference source; and every inline body step's inference source. Both inference
|
|
143
|
+
* rails are resolved HERE from the DB under the tenant-ownership authority
|
|
144
|
+
* (`resolveInferenceMaterials`), so the deploy is self-contained -- no caller
|
|
145
|
+
* pre-supplies material, and a spawned body child finds its secret in the cell.
|
|
146
|
+
* The merge is a post-authz union of already-cleared material (tool material is
|
|
147
|
+
* grant-scoped, inference material is tenant-ownership-scoped), never a shared
|
|
148
|
+
* authz check.
|
|
149
|
+
* Credential GRANT enforcement is a SEPARATE layer: the `credential:{id}` /
|
|
150
|
+
* `use` grant the runtime gate checks is minted per-run by run-grant
|
|
151
|
+
* materialization into `runs/<runId>/grants.json`, not carried on this frame --
|
|
152
|
+
* the deploy-time `config.grants` spawn-time snapshot is suppressed once the
|
|
153
|
+
* sidecar wires per-run grant pushes, so it is not the enforcement transport.
|
|
154
|
+
*
|
|
155
|
+
* A gate outcome that did not approve cannot deploy: an unapproved `approval`
|
|
156
|
+
* fails closed here rather than shipping an unfrozen definition.
|
|
157
|
+
*
|
|
158
|
+
* This does the READ-ONLY preparation ONLY: it runs the guards, resolves
|
|
159
|
+
* credential material, pins the body sources, and builds the asset mounts, then
|
|
160
|
+
* returns the frozen definition id and the assembled send args. It emits NO
|
|
161
|
+
* frame and writes NO row, so it has no side effect to unwind. The ordinary path
|
|
162
|
+
* (`deployCodeSourcedWorkflow`) sequences prepare -> INSERT anchor -> emit so
|
|
163
|
+
* the anchor is visible before the frame spawns the child; `emitSourceRefDeployFrame`
|
|
164
|
+
* composes prepare -> emit for the prepared provisioned path, whose anchor row
|
|
165
|
+
* already exists from prepare time. It returns the frozen definition id so each
|
|
166
|
+
* caller writes the same content-addressed identity the gate persisted.
|
|
167
|
+
*/
|
|
168
|
+
async function prepareSourceRefDeploy(args) {
|
|
169
|
+
const { approval, projection, closure } = args.approved;
|
|
170
|
+
if (!approval.ok) {
|
|
171
|
+
throw new Error(`deployCodeSourcedWorkflow: refusing to deploy an unapproved workflow (gate reason: ${approval.reason})`);
|
|
172
|
+
}
|
|
173
|
+
// Fail-closed persisted-definition guard. The anchor row this writes carries
|
|
174
|
+
// an FK to `workflow_definition`, so a phantom `definitionId` would otherwise
|
|
175
|
+
// reach the INSERT and fail with a raw constraint violation. A mis-wired
|
|
176
|
+
// caller -- or a test double that skips the approve step's DB writer -- could
|
|
177
|
+
// pass an approval whose definition was never persisted; verify it exists and
|
|
178
|
+
// fail with a domain error before deploying, rather than deploying and then
|
|
179
|
+
// failing the anchor insert into a deployed-but-unanchored state.
|
|
180
|
+
const persistedDefinition = await args.db.query.workflowDefinition.findFirst({
|
|
181
|
+
where: eq(workflowDefinitionTable.id, approval.definitionId),
|
|
182
|
+
columns: { id: true },
|
|
183
|
+
});
|
|
184
|
+
if (persistedDefinition === undefined) {
|
|
185
|
+
throw new Error(`deployCodeSourcedWorkflow: approval.definitionId ${approval.definitionId} does not reference a persisted workflow_definition row`);
|
|
186
|
+
}
|
|
187
|
+
// Coherence guard, run BEFORE the deploy frame: the anchor row's id and its
|
|
188
|
+
// routing address must name the same run. The deployment mail address is
|
|
189
|
+
// frozen into the approved package bytes at authoring time, so its run id is
|
|
190
|
+
// fixed before this runs and the caller owns `anchorRunId`. A mismatched
|
|
191
|
+
// (anchorRunId, agentAddress) pair would let run-grant materialization find
|
|
192
|
+
// the anchor by `address` while `deriveRunAddress` from `anchorRunId` names a
|
|
193
|
+
// different run -- a silent grant-identity split. Fail closed here, before the
|
|
194
|
+
// frame is sent or any row is persisted, rather than deploying an incoherent
|
|
195
|
+
// pair.
|
|
196
|
+
const derivedAddress = deriveRunAddress({
|
|
197
|
+
runId: args.anchorRunId,
|
|
198
|
+
domain: args.deploymentDomain,
|
|
199
|
+
});
|
|
200
|
+
if (derivedAddress !== args.agentAddress) {
|
|
201
|
+
throw new Error(`deployCodeSourcedWorkflow: anchorRunId ${args.anchorRunId} derives address ${derivedAddress} but agentAddress is ${args.agentAddress}`);
|
|
202
|
+
}
|
|
203
|
+
// Resolve the operator-approved credential bindings into delivered material.
|
|
204
|
+
// Tenant-owned resolution keys off the definition's tenant and walks up the
|
|
205
|
+
// hierarchy; it does not consult creator/invoker (the only locator today is
|
|
206
|
+
// `tenant`). A code-sourced deployment has no single authenticated invoker,
|
|
207
|
+
// so invoker is null; when principal-owned locators arrive, the asset creator
|
|
208
|
+
// must be resolved and passed here. A resolution failure is fail-closed.
|
|
209
|
+
const bindings = projection.credentialBindings ?? [];
|
|
210
|
+
let credentials;
|
|
211
|
+
if (bindings.length > 0) {
|
|
212
|
+
if (args.credentialCipher === undefined) {
|
|
213
|
+
throw new Error("deployCodeSourcedWorkflow: definition carries credential bindings but " +
|
|
214
|
+
"no credentialCipher was supplied; cannot resolve credential material");
|
|
215
|
+
}
|
|
216
|
+
const delivery = await buildCredentialDelivery({
|
|
217
|
+
db: args.db,
|
|
218
|
+
tenantId: args.tenantId,
|
|
219
|
+
bindings,
|
|
220
|
+
creatorPrincipalId: null,
|
|
221
|
+
invokerPrincipalId: null,
|
|
222
|
+
credentialCipher: args.credentialCipher,
|
|
223
|
+
});
|
|
224
|
+
if (!delivery.ok) {
|
|
225
|
+
throw new Error(`deployCodeSourcedWorkflow: credential binding resolution failed: ${delivery.reason.message}`);
|
|
226
|
+
}
|
|
227
|
+
credentials = delivery.delivery;
|
|
228
|
+
}
|
|
229
|
+
// Pin per-step inference sources for the projection's inline trigger bodies
|
|
230
|
+
// -- onTrigger sections and childWorkflow children, enumerated transitively.
|
|
231
|
+
// The hub holds only the frozen inert projection, so it enumerates the inline
|
|
232
|
+
// bodies from the wire form and resolves each body step's source through the
|
|
233
|
+
// same resolver + operator-approval gate the top-level steps use
|
|
234
|
+
// (`pickStepInferenceSource` against `approval.approvedSurface`). Each body's
|
|
235
|
+
// wire hash is recomputed from the inert body verbatim, so a body child's
|
|
236
|
+
// re-verify over the re-evaluated closure clears the same barrier a top-level
|
|
237
|
+
// re-verify does. The pinned sources ride OUTSIDE the hash; their trust comes
|
|
238
|
+
// from being resolved here under the approval gate, which is why the pin stays
|
|
239
|
+
// hub-side and is never caller-supplied.
|
|
240
|
+
//
|
|
241
|
+
// These entries ride the `referencedDefinitions` wire field. Each entry's
|
|
242
|
+
// `definition` is the approved inert body def straight from the frozen,
|
|
243
|
+
// hash-covered projection (id set to the ref); the sidecar reads that id to
|
|
244
|
+
// key the per-body approved hash and to stage the body's `sources.json`, which
|
|
245
|
+
// the body child reads to pin its steps. The body child resolves the body
|
|
246
|
+
// DEFINITION itself in-memory from the re-verified closure and hard-fails
|
|
247
|
+
// rather than reading it off disk, so no body workflow.json is staged (see the
|
|
248
|
+
// staging loop in workflow-host-wiring.ts and the anti-fallback guard in
|
|
249
|
+
// workflow-host run-child.ts).
|
|
250
|
+
const referencedDefinitions = await buildReferencedWorkflowSourcePins({
|
|
251
|
+
projection,
|
|
252
|
+
config: args.config,
|
|
253
|
+
operatorApprovals: approval.approvedSurface,
|
|
195
254
|
});
|
|
255
|
+
// Assemble the ONE credential delivery. Its `materials` cover three rails, each
|
|
256
|
+
// authorized upstream on its own terms, deduped by credentialId into one cell:
|
|
257
|
+
// - tool bindings, grant-scoped through `buildCredentialDelivery` above;
|
|
258
|
+
// - the inference source pinned to each top-level step that can actually
|
|
259
|
+
// invoke inference, tenant-owned;
|
|
260
|
+
// - the same for each inline body step (onTrigger/childWorkflow bodies
|
|
261
|
+
// pinned above), tenant-owned.
|
|
262
|
+
// Which steps those are is read from the hash-covered projection, never from
|
|
263
|
+
// the pinned map: every step carries a pin because the wire shape demands one,
|
|
264
|
+
// but a step that cannot issue a request has no use for a secret. Delivering
|
|
265
|
+
// one anyway decrypts a tenant credential, seals it to the sidecar, and
|
|
266
|
+
// re-delivers it on every reconnect, on behalf of a step that never makes a
|
|
267
|
+
// call.
|
|
268
|
+
// The inference rails are resolved HERE from the DB under the tenant-ownership
|
|
269
|
+
// authority, so this deploy is self-contained: a direct deploy (a test) that
|
|
270
|
+
// seeds the credentials in the DB -- rather than pre-supplying material -- still
|
|
271
|
+
// fills the cell, and a spawned body finds its secret rather than failing closed
|
|
272
|
+
// at resolve time. Precedence on a shared credentialId is tool material first
|
|
273
|
+
// (grant-scoped), then the inference material (tenant-ownership-scoped): the
|
|
274
|
+
// first material for an id wins. Inference sources carry NO binding descriptor
|
|
275
|
+
// -- they reference their credential by id directly.
|
|
276
|
+
const materials = new Map();
|
|
277
|
+
for (const material of credentials?.materials ?? []) {
|
|
278
|
+
materials.set(material.credentialId, material);
|
|
279
|
+
}
|
|
280
|
+
const inferenceCredentialIds = new Set();
|
|
281
|
+
const addAgentBearingCredentials = (definition, pinned, context) => {
|
|
282
|
+
for (const stepId of collectAgentBearingStepIds({ definition, context })) {
|
|
283
|
+
const stepSources = pinned[stepId];
|
|
284
|
+
if (stepSources === undefined) {
|
|
285
|
+
throw new Error(`${context}step ${stepId} can invoke inference but carries no pinned source`);
|
|
286
|
+
}
|
|
287
|
+
for (const source of stepSources) {
|
|
288
|
+
inferenceCredentialIds.add(source.credentialId);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
addAgentBearingCredentials(projection, args.sources, "deployCodeSourcedWorkflow: ");
|
|
293
|
+
for (const body of referencedDefinitions) {
|
|
294
|
+
addAgentBearingCredentials(body.definition, body.sources, `deployCodeSourcedWorkflow body ${body.definition.id}: `);
|
|
295
|
+
}
|
|
296
|
+
if (inferenceCredentialIds.size > 0) {
|
|
297
|
+
if (args.credentialCipher === undefined) {
|
|
298
|
+
throw new Error("deployCodeSourcedWorkflow: pinned inference sources reference credentials " +
|
|
299
|
+
"but no credentialCipher was supplied to resolve them");
|
|
300
|
+
}
|
|
301
|
+
const inferenceMaterials = await resolveInferenceMaterials(args.db, args.tenantId, inferenceCredentialIds, args.credentialCipher);
|
|
302
|
+
for (const material of inferenceMaterials) {
|
|
303
|
+
if (!materials.has(material.credentialId)) {
|
|
304
|
+
materials.set(material.credentialId, material);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// A delivery with tool bindings always carries their material, so an empty map
|
|
309
|
+
// means no rail contributed anything -- send no delivery. (Tool bindings never
|
|
310
|
+
// produce a descriptor without a material, so bindings-without-materials cannot
|
|
311
|
+
// occur.)
|
|
312
|
+
const credentialDelivery = materials.size > 0
|
|
313
|
+
? {
|
|
314
|
+
bindings: credentials?.bindings ?? [],
|
|
315
|
+
materials: [...materials.values()],
|
|
316
|
+
}
|
|
317
|
+
: undefined;
|
|
318
|
+
// An asset-sourced pin's `kind:"asset"` closure entries read from source
|
|
319
|
+
// assets the sidecar cannot fetch itself; deliver them inline on the frame so
|
|
320
|
+
// the sidecar checks them out into its durable per-deployment source store. A
|
|
321
|
+
// registry pin fetches its tarballs over HTTP and delivers none.
|
|
322
|
+
const assets = isAssetDeployArgs(args)
|
|
323
|
+
? await buildSourceAssetMounts(closure, args.resolveAttachment)
|
|
324
|
+
: [];
|
|
325
|
+
const sendArgs = {
|
|
326
|
+
lineage: "source-ref",
|
|
327
|
+
sidecarAllocationRouter: args.sidecarAllocationRouter,
|
|
328
|
+
allocationTarget: args.allocationTarget,
|
|
329
|
+
agentAddress: args.agentAddress,
|
|
330
|
+
config: args.config,
|
|
331
|
+
sources: args.sources,
|
|
332
|
+
approvedWireHash: approval.approvedWireHash,
|
|
333
|
+
sourceRef: { source: args.source, closure },
|
|
334
|
+
...(credentialDelivery !== undefined
|
|
335
|
+
? { credentials: credentialDelivery }
|
|
336
|
+
: {}),
|
|
337
|
+
...(referencedDefinitions.length > 0 ? { referencedDefinitions } : {}),
|
|
338
|
+
...(assets.length > 0 ? { assets } : {}),
|
|
339
|
+
};
|
|
340
|
+
return { definitionId: approval.definitionId, sendArgs };
|
|
196
341
|
}
|
|
197
342
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
343
|
+
* Prepare then emit the source-ref deploy frame, for the prepared provisioned
|
|
344
|
+
* path whose anchor `workflow_run` row already exists (inserted at prepare
|
|
345
|
+
* time). It emits the frame but does NOT touch the anchor row: the caller
|
|
346
|
+
* (`deployPreparedCodeSourcedWorkflow`) stamps the acked key under the
|
|
347
|
+
* allocation-ownership lock. A tagged `DeployFrameFailure` is converted to the
|
|
348
|
+
* `SessionLaunchError` disposition the allocation reconciler consumes, while
|
|
349
|
+
* untagged preparation errors remain safe same-generation retries. The ordinary
|
|
350
|
+
* path does NOT use this wrapper -- it must interleave the anchor INSERT between
|
|
351
|
+
* prepare and emit, so it drives `prepareSourceRefDeploy` and
|
|
352
|
+
* `sendMultiStepDeployFrame` directly.
|
|
204
353
|
*/
|
|
205
|
-
|
|
354
|
+
// The non-secret projection of a delivery, persisted on the anchor run so the
|
|
355
|
+
// reconnect resync can re-resolve current materials. Secrets never land here.
|
|
356
|
+
function credentialRefsFromDelivery(delivery) {
|
|
206
357
|
return {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
358
|
+
credentialIds: delivery.materials.map((material) => material.credentialId),
|
|
359
|
+
bindings: delivery.bindings,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async function emitSourceRefDeployFrame(args, reconciliation) {
|
|
363
|
+
const { signal, leaseId } = reconciliation;
|
|
364
|
+
signal.throwIfAborted();
|
|
365
|
+
const { definitionId, sendArgs } = await prepareSourceRefDeploy(args);
|
|
366
|
+
signal.throwIfAborted();
|
|
367
|
+
const allocationStore = createSidecarAllocationStore(args.db);
|
|
368
|
+
const initialization = {
|
|
369
|
+
allocationId: sendArgs.allocationTarget.allocationId,
|
|
370
|
+
generation: sendArgs.allocationTarget.generation,
|
|
371
|
+
anchorRunId: args.anchorRunId,
|
|
372
|
+
tenantId: args.tenantId,
|
|
373
|
+
leaseId,
|
|
374
|
+
signal,
|
|
215
375
|
};
|
|
376
|
+
let previousPublicKey;
|
|
377
|
+
try {
|
|
378
|
+
const result = await sendMultiStepDeployFrame(sendArgs, signal, async () => {
|
|
379
|
+
const reserved = await allocationStore.beginInitialization(initialization);
|
|
380
|
+
if (reserved === null) {
|
|
381
|
+
throw new Error("Allocation no longer permits this initialization attempt");
|
|
382
|
+
}
|
|
383
|
+
previousPublicKey = reserved.previousPublicKey;
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
publicKey: result.publicKey,
|
|
387
|
+
definitionId,
|
|
388
|
+
...(sendArgs.credentials !== undefined
|
|
389
|
+
? { credentialRefs: credentialRefsFromDelivery(sendArgs.credentials) }
|
|
390
|
+
: {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (cause) {
|
|
394
|
+
if (!isDeployFrameFailure(cause))
|
|
395
|
+
throw cause;
|
|
396
|
+
// Only a confirmed reservation gives us the key to restore. An ambiguous
|
|
397
|
+
// reservation response leaves its durable marker for conservative cleanup.
|
|
398
|
+
// A confirmed unsent attempt can roll back even after lease cancellation.
|
|
399
|
+
if (!cause.frameSent && previousPublicKey !== undefined) {
|
|
400
|
+
try {
|
|
401
|
+
const cleared = await allocationStore.clearUnsentInitialization({
|
|
402
|
+
...initialization,
|
|
403
|
+
previousPublicKey,
|
|
404
|
+
});
|
|
405
|
+
if (cleared)
|
|
406
|
+
args.onUnsentInitializationCleared(previousPublicKey);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
logger.warn `Could not clear unsent initialization for ${initialization.allocationId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
throw new SessionLaunchError("start", cause, cause.frameSent);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* A direct allocation-bound composition entrypoint for tests and low-level
|
|
417
|
+
* callers: prepare, INSERT the deployment's anchor `workflow_run` row, THEN emit
|
|
418
|
+
* the source-ref frame. The anchor row is the deployment's first-class record
|
|
419
|
+
* that owns its routing address and public key. Run-grant materialization keys
|
|
420
|
+
* off this row (address + live status), so WITHOUT it no per-run grants (tool,
|
|
421
|
+
* capability, OR credential) ever materialize for a source-ref deployment. Born
|
|
422
|
+
* "deployed" (live but pre-trigger) with a null public key: the first trigger's
|
|
423
|
+
* materialization flips it to "running" via `anchorWithPrincipal`'s guarded
|
|
424
|
+
* update, which a row born "running" would skip. Its `anchorRunId` equals its
|
|
425
|
+
* own id, so the anchor references itself. The deployer read grant is deferred
|
|
426
|
+
* to the production route, which carries the authenticated deployer principal.
|
|
427
|
+
*
|
|
428
|
+
* ORDERING IS LOAD-BEARING. The anchor row must be committed and visible to the
|
|
429
|
+
* pack-receipt connection BEFORE the frame reaches the wire: the frame spawns
|
|
430
|
+
* the child, whose first events pack races the ack back, and
|
|
431
|
+
* `receiveWorkflowRunPack` fails closed on a missing live anchor. Emitting first
|
|
432
|
+
* (the previous order) rejected that first pack and never bootstrapped the log.
|
|
433
|
+
* This works because `args.db` is the autocommit handle (`DB["db"]`, which the
|
|
434
|
+
* type forbids from being a transaction) and the INSERT is NOT wrapped in a
|
|
435
|
+
* transaction with the emit -- so the row is durably visible the instant the
|
|
436
|
+
* INSERT statement returns. Do NOT relax `db` to a transaction executor or wrap
|
|
437
|
+
* anchor+emit in one transaction to make them atomic: that reopens the race.
|
|
438
|
+
*
|
|
439
|
+
* On emit failure the anchor row is rolled back or fenced by the `frameSent`
|
|
440
|
+
* evidence from the transport. `leakedAgent: false` (safe to fully roll back) is
|
|
441
|
+
* the STRONG claim and is made only on positive proof the frame never reached
|
|
442
|
+
* the wire (`isDeployFrameFailure && frameSent === false`); every other failure
|
|
443
|
+
* -- a sent-but-unacked frame OR any untagged error -- is treated as
|
|
444
|
+
* possibly-live: the anchor is fenced `deployed` -> `failed` and the error is
|
|
445
|
+
* `leakedAgent: true`.
|
|
446
|
+
*
|
|
447
|
+
* The prepared provisioned path does NOT use this composition: its anchor row
|
|
448
|
+
* already exists from prepare time, so it drives `emitSourceRefDeployFrame` and
|
|
449
|
+
* an UPDATE-under-allocation-lock instead.
|
|
450
|
+
*/
|
|
451
|
+
export async function deployCodeSourcedWorkflow(args) {
|
|
452
|
+
const { definitionId, sendArgs } = await prepareSourceRefDeploy(args);
|
|
453
|
+
// INSERT the anchor before the frame. A collision or DB error here spawned
|
|
454
|
+
// nothing (no frame went out), so it is a clean, non-leaking failure.
|
|
455
|
+
try {
|
|
456
|
+
await args.db.insert(workflowRunTable).values({
|
|
457
|
+
id: args.anchorRunId,
|
|
458
|
+
tenantId: args.tenantId,
|
|
459
|
+
anchorRunId: args.anchorRunId,
|
|
460
|
+
definitionId,
|
|
461
|
+
address: args.agentAddress,
|
|
462
|
+
publicKey: null,
|
|
463
|
+
status: "deployed",
|
|
464
|
+
createdAt: new Date(),
|
|
465
|
+
// Persist the non-secret shape of the delivery so the reconnect resync
|
|
466
|
+
// can re-resolve current materials for these ids. Secrets never land here.
|
|
467
|
+
...(sendArgs.credentials !== undefined
|
|
468
|
+
? { credentialRefs: credentialRefsFromDelivery(sendArgs.credentials) }
|
|
469
|
+
: {}),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (cause) {
|
|
473
|
+
throw new SessionLaunchError("start", cause, false);
|
|
474
|
+
}
|
|
475
|
+
let publicKey;
|
|
476
|
+
try {
|
|
477
|
+
const result = await sendMultiStepDeployFrame(sendArgs);
|
|
478
|
+
publicKey = result.publicKey;
|
|
479
|
+
}
|
|
480
|
+
catch (cause) {
|
|
481
|
+
if (isDeployFrameFailure(cause) && cause.frameSent === false) {
|
|
482
|
+
// Positive proof the frame never reached the wire: nothing spawned, so
|
|
483
|
+
// fully roll the anchor back. The guard (`deployed`, null key) is a
|
|
484
|
+
// tripwire on the `frameSent: false` contract -- a 0-row delete means the
|
|
485
|
+
// row advanced or vanished, so the contract lied and a child may be live;
|
|
486
|
+
// surface that loudly and refuse to claim it is safe to roll back.
|
|
487
|
+
const deleted = await args.db
|
|
488
|
+
.delete(workflowRunTable)
|
|
489
|
+
.where(and(eq(workflowRunTable.id, args.anchorRunId), eq(workflowRunTable.anchorRunId, args.anchorRunId), eq(workflowRunTable.tenantId, args.tenantId), eq(workflowRunTable.status, "deployed"), isNull(workflowRunTable.publicKey)))
|
|
490
|
+
.returning({ id: workflowRunTable.id });
|
|
491
|
+
if (deleted.length === 0) {
|
|
492
|
+
logger.error `anchor-before-frame rollback found no deployed/null-key row for ${args.anchorRunId} after a frameSent:false failure; the never-sent contract was violated and a child may be live`;
|
|
493
|
+
throw new SessionLaunchError("start", cause, true);
|
|
494
|
+
}
|
|
495
|
+
throw new SessionLaunchError("start", cause, false);
|
|
496
|
+
}
|
|
497
|
+
// A sent-but-unacked frame, OR any untagged/unexpected error: no positive
|
|
498
|
+
// proof of a clean send, so treat the agent as possibly-live. Fence the
|
|
499
|
+
// anchor `deployed` -> `failed` (guarded so a self-flip to "running" by a
|
|
500
|
+
// trigger that already landed is left alone). Do NOT delete: a live child
|
|
501
|
+
// needs the anchor to bootstrap.
|
|
502
|
+
const flipped = await args.db
|
|
503
|
+
.update(workflowRunTable)
|
|
504
|
+
.set({ status: "failed" })
|
|
505
|
+
.where(and(eq(workflowRunTable.id, args.anchorRunId), eq(workflowRunTable.anchorRunId, args.anchorRunId), eq(workflowRunTable.tenantId, args.tenantId), eq(workflowRunTable.status, "deployed"), isNull(workflowRunTable.publicKey)))
|
|
506
|
+
.returning({ id: workflowRunTable.id });
|
|
507
|
+
if (flipped.length === 0) {
|
|
508
|
+
// The anchor already advanced past deployed -- a trigger flipped it to
|
|
509
|
+
// "running", so the deploy actually succeeded and the run is progressing
|
|
510
|
+
// despite the ack failure. Leave it; the leaked-agent disposition still
|
|
511
|
+
// holds because the frame was (or may have been) sent.
|
|
512
|
+
logger.warn `anchor-before-frame: anchor ${args.anchorRunId} already advanced past deployed on an unacked/failed emit; the agent is live and the run is progressing despite the ack failure`;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
logger.warn `anchor-before-frame: fenced anchor ${args.anchorRunId} deployed->failed on an unacked/failed emit; the agent may be leaked but the run is dead`;
|
|
516
|
+
}
|
|
517
|
+
throw new SessionLaunchError("start", cause, true);
|
|
518
|
+
}
|
|
519
|
+
// Emit succeeded: stamp the acked key. No status guard -- the key is a fact
|
|
520
|
+
// regardless of whether the pack-ack race already flipped the row to
|
|
521
|
+
// "running", and skipping the stamp there would strand a live run with a null
|
|
522
|
+
// key. A 0-row update is an anomaly (nothing should remove a deployed anchor
|
|
523
|
+
// on the success path), but the deploy succeeded, so log it rather than
|
|
524
|
+
// failing a live run.
|
|
525
|
+
const stamped = await args.db
|
|
526
|
+
.update(workflowRunTable)
|
|
527
|
+
.set({ publicKey })
|
|
528
|
+
.where(and(eq(workflowRunTable.id, args.anchorRunId), eq(workflowRunTable.anchorRunId, args.anchorRunId), eq(workflowRunTable.tenantId, args.tenantId)))
|
|
529
|
+
.returning({ id: workflowRunTable.id });
|
|
530
|
+
if (stamped.length === 0) {
|
|
531
|
+
logger.error `anchor-before-frame: anchor ${args.anchorRunId} vanished before its public key could be stamped on a successful deploy`;
|
|
532
|
+
}
|
|
533
|
+
return { publicKey };
|
|
534
|
+
}
|
|
535
|
+
/** Resolve deferred sender mail after claiming the previous initializer's lease. */
|
|
536
|
+
export async function recoverSenderDeploy(args) {
|
|
537
|
+
const { allocation, reconciliation } = args;
|
|
538
|
+
reconciliation.signal.throwIfAborted();
|
|
539
|
+
// A proven-unsent clear may still restore the previous key after this claim.
|
|
540
|
+
// Do not fail its mail using the claim's stale marker. Cleanup rechecks under
|
|
541
|
+
// the allocation lock; an advanced fence settles failure, while a rolled-back
|
|
542
|
+
// attempt is resolved by its caller or the next claim's completed key.
|
|
543
|
+
if (allocation.initializationLeaseId !== undefined)
|
|
544
|
+
return;
|
|
545
|
+
// Claiming the lease prevents the previous attempt from publishing. Its
|
|
546
|
+
// marker and key now distinguish a committed initialization from failure,
|
|
547
|
+
// even before the worker reconnects or the old response arrives.
|
|
548
|
+
const anchor = await args.db.query.workflowRun.findFirst({
|
|
549
|
+
where: eq(workflowRunTable.id, allocation.anchorRunId),
|
|
550
|
+
columns: { publicKey: true },
|
|
551
|
+
});
|
|
552
|
+
reconciliation.signal.throwIfAborted();
|
|
553
|
+
args.sidecarRouter.noteSenderDeploySettled({ allocationId: allocation.id, generation: allocation.generation }, anchor !== undefined && anchor.publicKey !== null
|
|
554
|
+
? { recorded: anchor.publicKey }
|
|
555
|
+
: { failed: "Previous deployment initialization did not complete" });
|
|
216
556
|
}
|
|
217
557
|
export function createSessionService(deps) {
|
|
218
|
-
const { sidecarRouter, agentRepoStore, assetService, db, toolPackageRegistries, } = deps;
|
|
558
|
+
const { sidecarRouter, sidecarAllocationRouter, agentRepoStore, assetService, db, toolPackageRegistries, } = deps;
|
|
219
559
|
if (assetService !== undefined && db === undefined) {
|
|
220
560
|
throw new Error("createSessionService: db is required when assetService is set");
|
|
221
561
|
}
|
|
222
562
|
if (toolPackageRegistries !== undefined && db === undefined) {
|
|
223
563
|
throw new Error("createSessionService: db is required when toolPackageRegistries is set");
|
|
224
564
|
}
|
|
565
|
+
function requireAllocationRouter() {
|
|
566
|
+
if (sidecarAllocationRouter === undefined) {
|
|
567
|
+
throw new Error("Provisioned deployment routing is not configured");
|
|
568
|
+
}
|
|
569
|
+
return sidecarAllocationRouter;
|
|
570
|
+
}
|
|
225
571
|
/**
|
|
226
|
-
* Stage
|
|
227
|
-
* the deploy tree, provision the
|
|
228
|
-
* packs (Phases 0-2b). Phase 1
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* record hub key), and unbinds the route once the packs land. No
|
|
235
|
-
* child.
|
|
236
|
-
* A call with neither is rejected -- the legacy warm-harness path
|
|
237
|
-
* is gone.
|
|
572
|
+
* Stage one per-step deploy on the sidecar: resolve assets and tool
|
|
573
|
+
* packages, write the deploy tree, provision the step, and deliver the
|
|
574
|
+
* deploy + asset packs (Phases 0-2b). Phase 1 binds a transient route for
|
|
575
|
+
* the step address, fires a no-spawn provision frame (init repo + record
|
|
576
|
+
* hub key), and unbinds the route once the packs land -- no warm harness and
|
|
577
|
+
* no child. The deployment-level workflow frame, sent once after every step
|
|
578
|
+
* is staged, spawns the child. A call without `stageOnly` is rejected -- the
|
|
579
|
+
* legacy warm-harness and single-step-head paths are gone.
|
|
238
580
|
*/
|
|
239
581
|
async function executeLaunchPhases(params) {
|
|
240
|
-
const { agentAddress, agentId,
|
|
582
|
+
const { agentAddress, agentId, runId, config, deployContent } = params;
|
|
241
583
|
const toolPackagePins = params.toolPackagePins ?? [];
|
|
242
584
|
const stageOnly = params.stageOnly ?? false;
|
|
243
|
-
|
|
244
|
-
throw new Error("executeLaunchPhases: workflowFrame and stageOnly are mutually exclusive");
|
|
245
|
-
}
|
|
246
|
-
const workflowFrame = params.workflowFrame;
|
|
247
|
-
// Phase 0: Resolve attached assets first so the skill index is in
|
|
248
|
-
// hand before the deploy tree is written. The `<available_skills>`
|
|
249
|
-
// stanza describing every attached skill must land in
|
|
250
|
-
// `deploy/prompt.md`, so it has to be composed before
|
|
251
|
-
// `writeDeployTree` produces the on-disk tree.
|
|
252
|
-
let attachments = [];
|
|
253
|
-
let availableSkills = [];
|
|
254
|
-
if (assetService !== undefined) {
|
|
255
|
-
try {
|
|
256
|
-
attachments = await resolveAttachments(assetService, agentId);
|
|
257
|
-
availableSkills = collectAvailableSkills(attachments);
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
throw new SessionLaunchError("write", err, false);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const stanza = buildAvailableSkillsStanza(availableSkills);
|
|
264
|
-
let effectiveDeployContent = stanza.length === 0
|
|
265
|
-
? deployContent
|
|
266
|
-
: {
|
|
267
|
-
...deployContent,
|
|
268
|
-
systemPrompt: `${deployContent.systemPrompt}\n\n${stanza}\n`,
|
|
269
|
-
};
|
|
585
|
+
let effectiveDeployContent = deployContent;
|
|
270
586
|
// Phase 0a-bis: Resolve the agent's tool-package pins into a full
|
|
271
587
|
// closure manifest. Empty pins skip the resolver entirely. A
|
|
272
588
|
// ManifestInvalidError (e.g. unsatisfied peer dependency) is a
|
|
@@ -308,25 +624,6 @@ export function createSessionService(deps) {
|
|
|
308
624
|
}
|
|
309
625
|
const assetMounts = new Map();
|
|
310
626
|
try {
|
|
311
|
-
// Refuse to mix a direct package-registry attachment with a
|
|
312
|
-
// resolver-driven pin against the same asset id. The resolver
|
|
313
|
-
// path emits an `assetMounts` entry pointing at the asset's
|
|
314
|
-
// DEFAULT_ASSET_REF tip, but a direct attachment may carry any
|
|
315
|
-
// ref the operator chose at attach time. The downstream dedup
|
|
316
|
-
// in `dedupAttachmentsByAssetId` lets the direct attachment win
|
|
317
|
-
// — its bytes would materialize at the operator's chosen ref
|
|
318
|
-
// while `assetMounts` still names the resolver's ref, leaving
|
|
319
|
-
// the loader to resolve manifest entries against tarballs that
|
|
320
|
-
// do not exist at the materialized mount. Surface the conflict
|
|
321
|
-
// at launch as a manifest-shaped violation rather than letting
|
|
322
|
-
// the integrity mismatch surface deep inside the sidecar apply.
|
|
323
|
-
const directPackageRegistryAttachments = attachments.filter((att) => att.assetKind === "package-registry");
|
|
324
|
-
for (const assetId of collectDistinctAssetIds(manifest)) {
|
|
325
|
-
const conflict = directPackageRegistryAttachments.find((att) => att.repoId.id === assetId);
|
|
326
|
-
if (conflict !== undefined) {
|
|
327
|
-
throw new ManifestInvalidError(`package-registry asset ${conflict.assetKind}/${conflict.assetName} (${assetId}) is both directly attached to the agent and selected by the tool-package resolver; attach OR pin via tenancy, not both`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
627
|
for (const assetId of collectDistinctAssetIds(manifest)) {
|
|
331
628
|
const asset = assetIndex.get(assetId);
|
|
332
629
|
if (asset === undefined) {
|
|
@@ -339,7 +636,7 @@ export function createSessionService(deps) {
|
|
|
339
636
|
}
|
|
340
637
|
const mountPath = `package-registries/${asset.name}/`;
|
|
341
638
|
assetMounts.set(assetId, mountPath);
|
|
342
|
-
manifestAssetAttachments.push(await
|
|
639
|
+
manifestAssetAttachments.push(await resolveAssetAttachment({
|
|
343
640
|
asset,
|
|
344
641
|
mountPath,
|
|
345
642
|
}));
|
|
@@ -372,192 +669,76 @@ export function createSessionService(deps) {
|
|
|
372
669
|
// route is held only for the pack window and dropped in the `finally`.
|
|
373
670
|
if (stageOnly) {
|
|
374
671
|
try {
|
|
375
|
-
|
|
672
|
+
await requireAllocationRouter().bindAllocatedStepRoute(params.allocationTarget, agentAddress);
|
|
376
673
|
}
|
|
377
674
|
catch (err) {
|
|
378
675
|
throw new SessionLaunchError("provision", err, false);
|
|
379
676
|
}
|
|
380
677
|
}
|
|
381
678
|
try {
|
|
382
|
-
// Phase 1: Provision on sidecar. A
|
|
383
|
-
// the deployment `agent.deploy` frame carrying the workflow definition
|
|
384
|
-
// + source pins: the sidecar's deploy router initializes the head repo
|
|
385
|
-
// on receipt (so the Phase 2 pack has a repo to apply into) and spawns
|
|
386
|
-
// the workflow-process child. A stage-only per-step deploy sends a
|
|
679
|
+
// Phase 1: Provision on sidecar. A stage-only per-step deploy sends a
|
|
387
680
|
// no-spawn provision frame: the sidecar inits the step's agent-state
|
|
388
681
|
// repo and records the hub key, but spawns nothing. Firing the frame
|
|
389
682
|
// before the Phase 2 pack is the ordering barrier -- the repo must
|
|
390
|
-
// exist before the pack applies.
|
|
391
|
-
// supervisor public key to the caller.
|
|
392
|
-
let deployAckPublicKey;
|
|
683
|
+
// exist before the pack applies.
|
|
393
684
|
try {
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
sidecarRouter,
|
|
397
|
-
agentAddress,
|
|
398
|
-
config,
|
|
399
|
-
definition: workflowFrame.definition,
|
|
400
|
-
sources: workflowFrame.sources,
|
|
401
|
-
});
|
|
402
|
-
deployAckPublicKey = ack.publicKey;
|
|
403
|
-
}
|
|
404
|
-
else if (stageOnly) {
|
|
405
|
-
await sidecarRouter.sendProvisionStep(agentAddress, config);
|
|
685
|
+
if (stageOnly) {
|
|
686
|
+
await requireAllocationRouter().sendProvisionStepToAllocation(params.allocationTarget, agentAddress, config);
|
|
406
687
|
}
|
|
407
688
|
else {
|
|
408
|
-
// Every caller supplies `
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
throw new Error("executeLaunchPhases: a deploy requires either workflowFrame or stageOnly");
|
|
689
|
+
// Every caller supplies `stageOnly`. A deploy without it has no
|
|
690
|
+
// provisioning shape -- the legacy warm-harness and single-step-head
|
|
691
|
+
// paths are gone -- so fail loud rather than ship a deploy pack the
|
|
692
|
+
// sidecar never provisioned a repo for.
|
|
693
|
+
throw new Error("executeLaunchPhases: a deploy requires stageOnly");
|
|
414
694
|
}
|
|
415
695
|
}
|
|
416
696
|
catch (err) {
|
|
417
697
|
throw new SessionLaunchError("provision", err, false);
|
|
418
698
|
}
|
|
419
|
-
// Phase 2: Pack delivery.
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
// leak on the exceptional staging-failure path, not a live-path cost.
|
|
699
|
+
// Phase 2: Pack delivery. A stage-only step has no supervisor to
|
|
700
|
+
// undeploy, so on failure it only drops its transient route (in the
|
|
701
|
+
// `finally`). The step's inited agent-state repo is left on the sidecar:
|
|
702
|
+
// the deploy aborts before the deployment frame is sent, so there is
|
|
703
|
+
// nothing to undeploy, and a redeploy of the same deployment overwrites
|
|
704
|
+
// the orphaned repo. This is an acceptable minor leak on the exceptional
|
|
705
|
+
// staging-failure path, not a live-path cost.
|
|
427
706
|
try {
|
|
428
|
-
await
|
|
707
|
+
await requireAllocationRouter().sendPackToAllocation(params.allocationTarget, agentAddress, pack, ref, commitSha);
|
|
429
708
|
}
|
|
430
709
|
catch (err) {
|
|
431
|
-
|
|
432
|
-
await attemptCleanup(agentAddress, "pack", err);
|
|
433
|
-
throw new SessionLaunchError("pack", err, false);
|
|
710
|
+
throw new SessionLaunchError("pack", err, !stageOnly);
|
|
434
711
|
}
|
|
435
712
|
// Phase 2b: Asset-pack fan-out. For each attached asset, build a
|
|
436
|
-
// pack,
|
|
437
|
-
//
|
|
713
|
+
// pack, reserve the manifest row, then send the pack. The manifest
|
|
714
|
+
// reservation MUST happen before the pack send: if the sidecar acks
|
|
438
715
|
// but the row is missing, the session has materialization without
|
|
439
|
-
// a recorded manifest.
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// The fan-out covers two sources: the agent's direct attachments
|
|
443
|
-
// (skills, today) and the package-registry assets the tool-package
|
|
444
|
-
// resolver picked from. The latter live behind tenant inheritance
|
|
445
|
-
// rather than a per-agent attachment row, so the session service
|
|
446
|
-
// synthesizes the attachment view in `manifestAssetAttachments`.
|
|
716
|
+
// a recorded manifest. An allocated replacement may reuse the exact row
|
|
717
|
+
// its predecessor recorded. If reservation fails, no pack is sent.
|
|
447
718
|
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
// the audit query joins against. The resolver-derived row would
|
|
455
|
-
// produce the same materialized contents, so dropping it is
|
|
456
|
-
// semantically lossless.
|
|
457
|
-
const fanOut = dedupAttachmentsByAssetId({
|
|
458
|
-
direct: attachments,
|
|
459
|
-
resolved: manifestAssetAttachments,
|
|
460
|
-
});
|
|
719
|
+
// The fan-out materializes the package-registry assets the
|
|
720
|
+
// tool-package resolver picked. They live behind tenant
|
|
721
|
+
// inheritance rather than a per-agent attachment row, so the
|
|
722
|
+
// session service synthesizes the attachment view in
|
|
723
|
+
// `manifestAssetAttachments`.
|
|
724
|
+
const fanOut = manifestAssetAttachments;
|
|
461
725
|
if (assetService !== undefined && fanOut.length > 0) {
|
|
462
|
-
// Track every successfully committed attachment so a later
|
|
463
|
-
// fan-out failure can roll back the earlier rows in lockstep
|
|
464
|
-
// with the sidecar undeploy. Without this, fan-out[0] succeeds,
|
|
465
|
-
// fan-out[1] fails, attemptCleanup tears down the sidecar — but
|
|
466
|
-
// fan-out[0]'s session_asset row survives and a future
|
|
467
|
-
// materialization query reads a manifest the sidecar no longer
|
|
468
|
-
// honors.
|
|
469
|
-
const committed = [];
|
|
470
726
|
for (const att of fanOut) {
|
|
471
727
|
try {
|
|
472
|
-
await sendAttachmentPack(
|
|
473
|
-
committed.push(att);
|
|
728
|
+
await sendAttachmentPack(runId, agentAddress, att, params.allocationTarget);
|
|
474
729
|
}
|
|
475
730
|
catch (err) {
|
|
476
|
-
|
|
477
|
-
if (!stageOnly)
|
|
478
|
-
await attemptCleanup(agentAddress, "pack", err);
|
|
479
|
-
throw new SessionLaunchError("pack", err, false);
|
|
731
|
+
throw new SessionLaunchError("pack", err, !stageOnly);
|
|
480
732
|
}
|
|
481
733
|
}
|
|
482
734
|
}
|
|
483
|
-
return deployAckPublicKey === undefined
|
|
484
|
-
? undefined
|
|
485
|
-
: { publicKey: deployAckPublicKey };
|
|
486
735
|
}
|
|
487
736
|
finally {
|
|
488
737
|
if (stageOnly) {
|
|
489
|
-
|
|
738
|
+
requireAllocationRouter().unbindAllocatedStepRoute(params.allocationTarget, agentAddress);
|
|
490
739
|
}
|
|
491
740
|
}
|
|
492
741
|
}
|
|
493
|
-
/**
|
|
494
|
-
* Deploy a one-step workflow once at the head. Reuses the full
|
|
495
|
-
* launch-phase machinery (deploy-tree write, pack, asset fan-out) via
|
|
496
|
-
* `executeLaunchPhases`, swapping the Phase 1 provision frame for the
|
|
497
|
-
* workflow frame. The workflow frame makes the sidecar initialize the
|
|
498
|
-
* head repo and spawn the workflow-process child; the follow-up pack
|
|
499
|
-
* lands the head's deploy tree. Returns the supervisor's principal
|
|
500
|
-
* public key from the frame's ack. A workflow-frame launch always
|
|
501
|
-
* yields a deploy-ack key; its absence is a wiring bug, not a
|
|
502
|
-
* tolerable case.
|
|
503
|
-
*/
|
|
504
|
-
const deploySingleStepAtHead = async (deployParams) => {
|
|
505
|
-
const result = await executeLaunchPhases({
|
|
506
|
-
agentAddress: deployParams.agentAddress,
|
|
507
|
-
agentId: deployParams.agentId,
|
|
508
|
-
instanceId: deployParams.instanceId,
|
|
509
|
-
config: deployParams.config,
|
|
510
|
-
deployContent: bridgeOrchestratorDeployContent(deployParams.deployContent),
|
|
511
|
-
workflowFrame: {
|
|
512
|
-
definition: deployParams.definition,
|
|
513
|
-
sources: deployParams.sources,
|
|
514
|
-
},
|
|
515
|
-
...(deployParams.toolPackagePins !== undefined
|
|
516
|
-
? { toolPackagePins: deployParams.toolPackagePins }
|
|
517
|
-
: {}),
|
|
518
|
-
});
|
|
519
|
-
if (result === undefined) {
|
|
520
|
-
throw new Error("single-step deploy at head: executeLaunchPhases returned no deploy-ack public key for a workflow-frame deploy");
|
|
521
|
-
}
|
|
522
|
-
return result;
|
|
523
|
-
};
|
|
524
|
-
/**
|
|
525
|
-
* Build the workflow-deploy orchestrator (with its launch-session and
|
|
526
|
-
* multi-step callbacks) and run one deploy. Shared by `launchSession`
|
|
527
|
-
* and `deployWorkflowDefinition`, which differ only in the workflow
|
|
528
|
-
* repo writer, the director registry, and the deploy args.
|
|
529
|
-
*/
|
|
530
|
-
async function runWorkflowDeploy(args) {
|
|
531
|
-
// The per-step launcher: stage each step's deploy tree WITHOUT a warm
|
|
532
|
-
// harness (the supervised child runs the step), with the orchestrator's
|
|
533
|
-
// structural `DeployContent` narrowed back to the hub-sessions shape
|
|
534
|
-
// first.
|
|
535
|
-
const launchSessionCallback = (orchestratorParams) => stageWorkflowStep({
|
|
536
|
-
agentAddress: orchestratorParams.agentAddress,
|
|
537
|
-
agentId: orchestratorParams.agentId,
|
|
538
|
-
instanceId: orchestratorParams.instanceId,
|
|
539
|
-
config: orchestratorParams.config,
|
|
540
|
-
deployContent: bridgeOrchestratorDeployContent(orchestratorParams.deployContent),
|
|
541
|
-
...(orchestratorParams.toolPackagePins !== undefined
|
|
542
|
-
? { toolPackagePins: orchestratorParams.toolPackagePins }
|
|
543
|
-
: {}),
|
|
544
|
-
});
|
|
545
|
-
const sendMultiStepDeployCallback = (deployParams) => sendMultiStepDeployFrame({
|
|
546
|
-
sidecarRouter,
|
|
547
|
-
agentAddress: deployParams.agentAddress,
|
|
548
|
-
config: deployParams.config,
|
|
549
|
-
definition: deployParams.definition,
|
|
550
|
-
sources: deployParams.sources,
|
|
551
|
-
});
|
|
552
|
-
const orchestrator = createWorkflowDeployOrchestrator({
|
|
553
|
-
directorRegistry: args.directorRegistry,
|
|
554
|
-
workflowRepo: args.workflowRepo,
|
|
555
|
-
launchSession: launchSessionCallback,
|
|
556
|
-
sendMultiStepDeploy: sendMultiStepDeployCallback,
|
|
557
|
-
deploySingleStepAtHead,
|
|
558
|
-
});
|
|
559
|
-
return orchestrator.deployWorkflow(args.deployArgs);
|
|
560
|
-
}
|
|
561
742
|
/**
|
|
562
743
|
* Stage one step of a multi-step workflow deploy: bind a transient route
|
|
563
744
|
* for the step address, fire a no-spawn provision frame (the sidecar inits
|
|
@@ -572,246 +753,363 @@ export function createSessionService(deps) {
|
|
|
572
753
|
await executeLaunchPhases({
|
|
573
754
|
agentAddress: params.agentAddress,
|
|
574
755
|
agentId: params.agentId,
|
|
575
|
-
|
|
756
|
+
runId: params.runId,
|
|
576
757
|
config: params.config,
|
|
577
758
|
deployContent: params.deployContent,
|
|
578
759
|
stageOnly: true,
|
|
579
760
|
...(params.toolPackagePins !== undefined
|
|
580
761
|
? { toolPackagePins: params.toolPackagePins }
|
|
581
762
|
: {}),
|
|
763
|
+
allocationTarget: params.allocationTarget,
|
|
582
764
|
});
|
|
583
765
|
}
|
|
766
|
+
// Resolve the npm registry config a code-sourced install resolves external
|
|
767
|
+
// deps against, by the registry name. A code-sourced deploy needs the
|
|
768
|
+
// registry map configured; a hub that mounts the deploy surface without it is
|
|
769
|
+
// mis-wired, so this fails loud rather than defaulting a registry URL.
|
|
770
|
+
function requireRegistryConfig(registryName) {
|
|
771
|
+
if (toolPackageRegistries === undefined) {
|
|
772
|
+
throw new Error("deployWorkflowFromSource: the session service has no toolPackageRegistries configured; a code-sourced deploy cannot resolve its dependency closure");
|
|
773
|
+
}
|
|
774
|
+
const config = toolPackageRegistries.httpRegistries.get(registryName);
|
|
775
|
+
if (config === undefined) {
|
|
776
|
+
throw new Error(`deployWorkflowFromSource: no HTTP registry named ${JSON.stringify(registryName)} is configured`);
|
|
777
|
+
}
|
|
778
|
+
return config;
|
|
779
|
+
}
|
|
780
|
+
// Build the git-pack resolver a source/tarball asset arm delivers inline. The
|
|
781
|
+
// pin names one backing asset, so the resolver binds that asset's repo (its
|
|
782
|
+
// kind fixed by the arm) and its default ref; a request for any OTHER asset id
|
|
783
|
+
// is a closure that reaches beyond its single backing asset and fails loud
|
|
784
|
+
// rather than silently packing the wrong repo.
|
|
785
|
+
function bindAssetAttachmentResolver(assetId, repoKind) {
|
|
786
|
+
return async (requestedAssetId) => {
|
|
787
|
+
if (requestedAssetId !== assetId) {
|
|
788
|
+
throw new Error(`deployWorkflowFromSource: closure references asset ${requestedAssetId}, but only the pinned source asset ${assetId} is deliverable`);
|
|
789
|
+
}
|
|
790
|
+
const repoId = { kind: repoKind, id: assetId };
|
|
791
|
+
const commitSha = await agentRepoStore.repoStore.resolveRef(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
|
|
792
|
+
if (commitSha === null) {
|
|
793
|
+
throw new Error(`deployWorkflowFromSource: source asset ${assetId} has no commit on ${DEFAULT_ASSET_REF}`);
|
|
794
|
+
}
|
|
795
|
+
const { pack, ref } = await agentRepoStore.repoStore.createPack(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
|
|
796
|
+
return { pack, ref, commitSha };
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
// Assemble the install args for the concrete source arm. Mirrors the
|
|
800
|
+
// `isAssetSourceInstallArgs`/`isAssetTarballInstallArgs` guards the probe gate
|
|
801
|
+
// narrows on: an asset-`source` arm binds committed reads at the pinned commit
|
|
802
|
+
// plus the npm registry for external deps; an asset-`tarball` arm binds the
|
|
803
|
+
// asset's blob reads and a pin; a `registry` arm carries only its registry
|
|
804
|
+
// config and a pin. A `pin` missing where the arm requires it fails closed.
|
|
805
|
+
async function buildInstallArgs(params, resolveAttachment) {
|
|
806
|
+
if (db === undefined) {
|
|
807
|
+
throw new Error("deployWorkflowFromSource requires a db handle to freeze the approval");
|
|
808
|
+
}
|
|
809
|
+
const dbHandle = db;
|
|
810
|
+
const allocationTarget = params.allocationTarget;
|
|
811
|
+
const common = {
|
|
812
|
+
entry: params.entry,
|
|
813
|
+
assetId: params.definitionAssetId,
|
|
814
|
+
approvals: { kind: "approve-probed" },
|
|
815
|
+
router: {
|
|
816
|
+
sendProbe: (args) => requireAllocationRouter().sendProbeToAllocation(allocationTarget, args),
|
|
817
|
+
},
|
|
818
|
+
...(params.onProbeResult !== undefined
|
|
819
|
+
? { onProbeResult: params.onProbeResult }
|
|
820
|
+
: {}),
|
|
821
|
+
db: dbHandle,
|
|
822
|
+
};
|
|
823
|
+
const source = params.source;
|
|
824
|
+
if (source.kind === "asset") {
|
|
825
|
+
if (resolveAttachment === null) {
|
|
826
|
+
throw new Error("deployWorkflowFromSource: an asset-sourced deploy requires an attachment resolver");
|
|
827
|
+
}
|
|
828
|
+
if (source.package.format === "source") {
|
|
829
|
+
const committed = await agentRepoStore.repoStore.openCommittedReadsAtCommit(HUB_PRINCIPAL, { kind: "workflow", id: source.assetId }, source.package.commitSha);
|
|
830
|
+
if (committed === null) {
|
|
831
|
+
throw new Error(`deployWorkflowFromSource: source asset ${source.assetId} has no commit ${source.package.commitSha}`);
|
|
832
|
+
}
|
|
833
|
+
const registryName = requireDefaultRegistryName();
|
|
834
|
+
return {
|
|
835
|
+
...common,
|
|
836
|
+
source,
|
|
837
|
+
reads: committedReadsToSourceTree(committed),
|
|
838
|
+
registryName,
|
|
839
|
+
registryConfig: requireRegistryConfig(registryName),
|
|
840
|
+
resolveAttachment,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
if (params.pin === undefined) {
|
|
844
|
+
throw new Error("deployWorkflowFromSource: an asset-tarball deploy requires a name@range pin");
|
|
845
|
+
}
|
|
846
|
+
if (assetService === undefined) {
|
|
847
|
+
throw new Error("deployWorkflowFromSource: an asset-tarball deploy requires an asset service to read the package blobs");
|
|
848
|
+
}
|
|
849
|
+
const tarballAssetId = source.assetId;
|
|
850
|
+
const tarballService = assetService;
|
|
851
|
+
return {
|
|
852
|
+
...common,
|
|
853
|
+
source,
|
|
854
|
+
pin: params.pin,
|
|
855
|
+
readBlob: (path) => tarballService.readAssetBlob({ assetId: tarballAssetId, path }),
|
|
856
|
+
listBlobs: (dir) => tarballService.listAssetBlobs({ assetId: tarballAssetId, dir }),
|
|
857
|
+
resolveAttachment,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (params.pin === undefined) {
|
|
861
|
+
throw new Error("deployWorkflowFromSource: a registry deploy requires a name@range pin");
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
...common,
|
|
865
|
+
source,
|
|
866
|
+
pin: params.pin,
|
|
867
|
+
registryConfig: requireRegistryConfig(source.registry),
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function requireDefaultRegistryName() {
|
|
871
|
+
if (toolPackageRegistries === undefined) {
|
|
872
|
+
throw new Error("deployWorkflowFromSource: the session service has no toolPackageRegistries configured; a code-sourced deploy cannot resolve its dependency closure");
|
|
873
|
+
}
|
|
874
|
+
return toolPackageRegistries.defaultRegistry;
|
|
875
|
+
}
|
|
876
|
+
// Bind the pack resolver an asset arm delivers inline. An asset arm delivers
|
|
877
|
+
// its backing repo (its kind fixed by `package.format`); a registry arm
|
|
878
|
+
// fetches its tarballs over HTTP and delivers no asset, so it binds nothing.
|
|
879
|
+
// Both the install (probe) and the deploy rebind the SAME resolver from the
|
|
880
|
+
// source, so a prepared deploy reconstructs it from the frozen `source`.
|
|
881
|
+
function bindSourceAttachmentResolver(source) {
|
|
882
|
+
return source.kind === "asset"
|
|
883
|
+
? bindAssetAttachmentResolver(source.assetId, source.package.format === "source" ? "workflow" : "package-registry")
|
|
884
|
+
: null;
|
|
885
|
+
}
|
|
886
|
+
// Install + probe + gate + freeze a code-sourced definition, returning the
|
|
887
|
+
// frozen bundle and the (asset-only) attachment resolver. The gate outcome is
|
|
888
|
+
// NOT asserted here: `deployWorkflowFromSource` and `installAndApproveWorkflowSource`
|
|
889
|
+
// each surface a non-approval as their own domain error. This is the common
|
|
890
|
+
// freeze used by direct tests and provisioned prepare runs.
|
|
891
|
+
async function prepareCodeSourcedApproval(params) {
|
|
892
|
+
const resolveAttachment = bindSourceAttachmentResolver(params.source);
|
|
893
|
+
const installArgs = await buildInstallArgs(params, resolveAttachment);
|
|
894
|
+
const approved = await installAndApproveWorkflowDefinition(installArgs);
|
|
895
|
+
return { approved, resolveAttachment };
|
|
896
|
+
}
|
|
897
|
+
// Freeze a code-sourced approval WITHOUT deploying it. The provisioned
|
|
898
|
+
// prepare path persists the returned bundle and deploys it to an allocation
|
|
899
|
+
// later. A non-approval fails closed as an invalid
|
|
900
|
+
// definition.
|
|
901
|
+
async function installAndApproveWorkflowSource(params) {
|
|
902
|
+
const { approved } = await prepareCodeSourcedApproval(params);
|
|
903
|
+
if (!approved.approval.ok) {
|
|
904
|
+
throw new WorkflowDefinitionInvalidError(approved.projection.id, `code-sourced workflow install did not approve (reason: ${approved.approval.reason})`);
|
|
905
|
+
}
|
|
906
|
+
return approved;
|
|
907
|
+
}
|
|
584
908
|
/**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
* `
|
|
593
|
-
* (`row.id`), NOT a `deriveDeploymentAgentId(deploymentId)` -- the child
|
|
594
|
-
* resolves skills, deploy tree, and tool-package pins by `agentId`, so
|
|
595
|
-
* collapsing it to the deployment id would strip the instance's attachments.
|
|
596
|
-
* It writes no `workflow_deployment` row (a plain instance has no workflow
|
|
597
|
-
* asset). Returns the head's agent-key ack.
|
|
909
|
+
* Update a prepared anchor run's `publicKey` under the allocation-ownership
|
|
910
|
+
* lock. The anchor row was inserted at prepare time; this stamps the
|
|
911
|
+
* supervisor key returned by the deploy ack, but only while the allocation
|
|
912
|
+
* still names this exact accepted generation and unexpired reconciliation
|
|
913
|
+
* lease for this anchor. Lost ownership or cancellation fails closed as a
|
|
914
|
+
* leaked-agent `SessionLaunchError` -- the deploy already reached the sidecar,
|
|
915
|
+
* so the caller must treat the sidecar agent as possibly live. Used by the
|
|
916
|
+
* `deployPreparedCodeSourcedWorkflow` prepared path.
|
|
598
917
|
*/
|
|
599
|
-
async function
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
918
|
+
async function updateAnchorPublicKeyUnderAllocationLock(args) {
|
|
919
|
+
if (db === undefined) {
|
|
920
|
+
throw new Error("updateAnchorPublicKeyUnderAllocationLock requires a db handle");
|
|
921
|
+
}
|
|
922
|
+
const dbHandle = db;
|
|
923
|
+
try {
|
|
924
|
+
const updated = await createSidecarAllocationStore(dbHandle).completeInitialization({
|
|
925
|
+
allocationId: args.allocationTarget.allocationId,
|
|
926
|
+
generation: args.allocationTarget.generation,
|
|
927
|
+
anchorRunId: args.anchorRunId,
|
|
928
|
+
tenantId: args.tenantId,
|
|
929
|
+
leaseId: args.reconciliation.leaseId,
|
|
930
|
+
signal: args.reconciliation.signal,
|
|
931
|
+
publicKey: args.publicKey,
|
|
932
|
+
...(args.credentialRefs !== undefined
|
|
933
|
+
? { credentialRefs: args.credentialRefs }
|
|
934
|
+
: {}),
|
|
935
|
+
});
|
|
936
|
+
if (!updated) {
|
|
937
|
+
throw new Error(`Prepared anchor run ${args.anchorRunId} lost allocation ownership before initialization completed`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
throw new SessionLaunchError("start", error, true);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Deploy a previously-frozen code-sourced approval bundle to a dedicated
|
|
946
|
+
* allocation. The anchor `workflow_run` row already exists from prepare time
|
|
947
|
+
* (with its `definitionId` set), so this UPDATES it under the
|
|
948
|
+
* allocation-ownership lock
|
|
949
|
+
* rather than inserting. No re-probe: the frozen projection/hash/closure ride
|
|
950
|
+
* verbatim from `params.approved`, and the per-step inference sources are
|
|
951
|
+
* re-pinned from the re-resolved chain (deliberately NOT frozen, since a
|
|
952
|
+
* resolved source carries a credential secret).
|
|
953
|
+
*/
|
|
954
|
+
async function deployPreparedCodeSourcedWorkflow(params) {
|
|
955
|
+
const { signal } = params.reconciliation;
|
|
956
|
+
signal.throwIfAborted();
|
|
957
|
+
if (db === undefined) {
|
|
958
|
+
throw new Error("deployPreparedCodeSourcedWorkflow requires a db handle to update the prepared anchor run");
|
|
959
|
+
}
|
|
960
|
+
const dbHandle = db;
|
|
961
|
+
const approval = params.approved.approval;
|
|
962
|
+
if (!approval.ok) {
|
|
963
|
+
throw new Error("deployPreparedCodeSourcedWorkflow: refusing to deploy an unapproved workflow bundle");
|
|
964
|
+
}
|
|
965
|
+
const allocationRouter = requireAllocationRouter();
|
|
966
|
+
const source = params.source;
|
|
967
|
+
const resolveAttachment = bindSourceAttachmentResolver(source);
|
|
968
|
+
// Re-pin every top-level step's inference source from the re-resolved chain
|
|
969
|
+
// under the frozen approval -- the same pin the source-ref deploy computes.
|
|
970
|
+
const sources = buildInertProjectionStepSources({
|
|
971
|
+
projection: params.approved.projection,
|
|
972
|
+
config: params.config,
|
|
973
|
+
operatorApprovals: approval.approvedSurface,
|
|
604
974
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
975
|
+
// Restore the Hub-authoritative run ref onto the exact allocation generation
|
|
976
|
+
// before its address is routed.
|
|
977
|
+
await restoreWorkflowRunToAllocation({
|
|
978
|
+
agentRepoStore,
|
|
979
|
+
allocationRouter,
|
|
980
|
+
allocationTarget: params.allocationTarget,
|
|
981
|
+
agentAddress: params.agentAddress,
|
|
982
|
+
signal,
|
|
609
983
|
});
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (config.sources.length === 0) {
|
|
629
|
-
throw new Error(`instance deploy for ${agentAddress}: config.sources is empty; at least the default source is required`);
|
|
630
|
-
}
|
|
631
|
-
if (config.sources[0]?.id !== config.defaultSource) {
|
|
632
|
-
throw new Error(`instance deploy for ${agentAddress}: config.sources[0] (${JSON.stringify(config.sources[0]?.id)}) must be the default source ${JSON.stringify(config.defaultSource)}; the reactor fails over forward from the default and would otherwise skip the head`);
|
|
633
|
-
}
|
|
634
|
-
return deploySingleStepAtHead({
|
|
635
|
-
agentAddress,
|
|
636
|
-
agentId,
|
|
637
|
-
instanceId,
|
|
638
|
-
config,
|
|
639
|
-
deployContent,
|
|
640
|
-
definition: workflow,
|
|
641
|
-
sources: { [stepId]: config.sources },
|
|
642
|
-
hubPublicKey: hexEncode(agentRepoStore.getSigningPublicKey()),
|
|
643
|
-
...(params.toolPackagePins !== undefined
|
|
644
|
-
? { toolPackagePins: params.toolPackagePins }
|
|
984
|
+
signal.throwIfAborted();
|
|
985
|
+
let restoredPublicKey;
|
|
986
|
+
const commonEmit = {
|
|
987
|
+
onUnsentInitializationCleared(publicKey) {
|
|
988
|
+
restoredPublicKey = publicKey;
|
|
989
|
+
},
|
|
990
|
+
approved: params.approved,
|
|
991
|
+
sidecarAllocationRouter: allocationRouter,
|
|
992
|
+
allocationTarget: params.allocationTarget,
|
|
993
|
+
agentAddress: params.agentAddress,
|
|
994
|
+
config: params.config,
|
|
995
|
+
sources,
|
|
996
|
+
db: dbHandle,
|
|
997
|
+
tenantId: params.tenantId,
|
|
998
|
+
anchorRunId: params.anchorRunId,
|
|
999
|
+
deploymentDomain: params.deploymentDomain,
|
|
1000
|
+
...(params.credentialCipher !== undefined
|
|
1001
|
+
? { credentialCipher: params.credentialCipher }
|
|
645
1002
|
: {}),
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1003
|
+
};
|
|
1004
|
+
// Branch on the source discriminant so the emit args match the asset/registry
|
|
1005
|
+
// arms: an asset arm carries the rebuilt attachment resolver (asserted
|
|
1006
|
+
// non-null to satisfy the union), a registry arm carries none.
|
|
1007
|
+
let result;
|
|
1008
|
+
const senderAttempt = {
|
|
1009
|
+
...params.allocationTarget,
|
|
1010
|
+
leaseId: params.reconciliation.leaseId,
|
|
1011
|
+
};
|
|
1012
|
+
signal.throwIfAborted();
|
|
1013
|
+
sidecarRouter.noteSenderDeployStarted(params.agentAddress, senderAttempt);
|
|
1014
|
+
try {
|
|
1015
|
+
// Bracket the allocated pre-ack window: mark the sender's key-record as
|
|
1016
|
+
// mid-flight before the deploy emit so a run that sends mail before its
|
|
1017
|
+
// anchor key is committed parks rather than delivering keyless. The settle
|
|
1018
|
+
// follows durable completion; cancellation leaves the outcome to recovery.
|
|
1019
|
+
if (source.kind === "asset") {
|
|
1020
|
+
if (resolveAttachment === null) {
|
|
1021
|
+
throw new Error("deployPreparedCodeSourcedWorkflow: asset source deploy is missing its attachment resolver");
|
|
1022
|
+
}
|
|
1023
|
+
result = await emitSourceRefDeployFrame({ ...commonEmit, source, resolveAttachment }, params.reconciliation);
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
result = await emitSourceRefDeployFrame({ ...commonEmit, source }, params.reconciliation);
|
|
1027
|
+
}
|
|
1028
|
+
await updateAnchorPublicKeyUnderAllocationLock({
|
|
1029
|
+
tenantId: params.tenantId,
|
|
1030
|
+
anchorRunId: params.anchorRunId,
|
|
1031
|
+
allocationTarget: params.allocationTarget,
|
|
1032
|
+
reconciliation: params.reconciliation,
|
|
1033
|
+
publicKey: result.publicKey,
|
|
1034
|
+
...(result.credentialRefs !== undefined
|
|
1035
|
+
? { credentialRefs: result.credentialRefs }
|
|
673
1036
|
: {}),
|
|
674
|
-
},
|
|
675
|
-
});
|
|
676
|
-
if (db === undefined) {
|
|
677
|
-
throw new Error("deployWorkflowDefinition requires a db handle to record the workflow_deployment projection row");
|
|
678
|
-
}
|
|
679
|
-
const now = new Date();
|
|
680
|
-
await db.transaction(async (tx) => {
|
|
681
|
-
await tx.insert(workflowDeploymentTable).values({
|
|
682
|
-
id: deploymentId,
|
|
683
|
-
tenantId,
|
|
684
|
-
definitionAssetId,
|
|
685
|
-
address: deriveDeploymentAddress({ deploymentId, deploymentDomain }),
|
|
686
|
-
// publicKey is left null here; the sidecar's deploy-ack persists the
|
|
687
|
-
// deployment's minted key once the child has spawned.
|
|
688
|
-
status: "deployed",
|
|
689
|
-
createdAt: now,
|
|
690
1037
|
});
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
resource: `workflow-run:${deploymentId}`,
|
|
701
|
-
action: "read",
|
|
702
|
-
effect: "allow",
|
|
703
|
-
origin: "creator",
|
|
704
|
-
createdAt: now,
|
|
705
|
-
updatedAt: now,
|
|
1038
|
+
// The anchor's public key is now durable. Wake any mail the run parked
|
|
1039
|
+
// while pre-ack so it delivers with the sender key co-delivered, closing
|
|
1040
|
+
// the window where a run sends before its key is recorded. The write above
|
|
1041
|
+
// happens-before this settle, so a re-drive resolves the recorded key.
|
|
1042
|
+
// `params.agentAddress` is the run's deploy address, byte-identical to the
|
|
1043
|
+
// sender address its mail was sent under (asserted against the anchor at
|
|
1044
|
+
// deploy time), so a settle matches the parked entries.
|
|
1045
|
+
sidecarRouter.noteSenderDeploySettled(senderAttempt, {
|
|
1046
|
+
recorded: result.publicKey,
|
|
706
1047
|
});
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (db === undefined)
|
|
719
|
-
return;
|
|
720
|
-
if (committed.length === 0)
|
|
721
|
-
return;
|
|
722
|
-
// Per-row try/catch so a single rollback failure does not stop the
|
|
723
|
-
// sweep — every committed row needs to come off the books before
|
|
724
|
-
// the caller emits the original sendPack error.
|
|
725
|
-
for (const att of committed) {
|
|
726
|
-
try {
|
|
727
|
-
await db
|
|
728
|
-
.delete(sessionAssetTable)
|
|
729
|
-
.where(and(eq(sessionAssetTable.instanceId, instanceId), eq(sessionAssetTable.mountPath, att.mountPath)));
|
|
1048
|
+
return {
|
|
1049
|
+
anchorRunId: params.anchorRunId,
|
|
1050
|
+
deploymentAddress: params.agentAddress,
|
|
1051
|
+
publicKey: result.publicKey,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
if (restoredPublicKey !== undefined) {
|
|
1056
|
+
sidecarRouter.noteSenderDeploySettled(senderAttempt, restoredPublicKey === null
|
|
1057
|
+
? { failed: error instanceof Error ? error.message : String(error) }
|
|
1058
|
+
: { recorded: restoredPublicKey });
|
|
730
1059
|
}
|
|
731
|
-
|
|
732
|
-
|
|
1060
|
+
// A sent deploy or cancelled publication may have committed despite its
|
|
1061
|
+
// lost response, as may an unsent rollback. Without a confirmed rollback,
|
|
1062
|
+
// recovery owns transport failures too. Only an uncancelled preparation
|
|
1063
|
+
// failure can settle definitively here.
|
|
1064
|
+
else if (!signal.aborted && !(error instanceof SessionLaunchError)) {
|
|
1065
|
+
sidecarRouter.noteSenderDeploySettled(senderAttempt, {
|
|
1066
|
+
failed: error instanceof Error ? error.message : String(error),
|
|
1067
|
+
});
|
|
733
1068
|
}
|
|
1069
|
+
throw error;
|
|
734
1070
|
}
|
|
735
1071
|
}
|
|
736
|
-
async function sendAttachmentPack(
|
|
1072
|
+
async function sendAttachmentPack(runId, agentAddress, attachment, allocationTarget) {
|
|
737
1073
|
if (db === undefined) {
|
|
738
1074
|
// Guarded at construction; reassert defensively so the
|
|
739
1075
|
// narrowing is visible to readers and a future refactor cannot
|
|
740
1076
|
// accidentally invoke this without a db.
|
|
741
1077
|
throw new Error("sendAttachmentPack invoked without a db handle");
|
|
742
1078
|
}
|
|
743
|
-
const {
|
|
1079
|
+
const { mountPath, sourceCommitSha, repoId, pack, ref } = attachment;
|
|
744
1080
|
const assetPackSha = await createPackSha(pack);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// resolver-derived materializations write a row; the `source`
|
|
748
|
-
// column records which path produced it, and `agentAssetId` is
|
|
749
|
-
// null for resolver-derived rows.
|
|
750
|
-
await db.insert(sessionAssetTable).values({
|
|
751
|
-
instanceId,
|
|
752
|
-
agentAssetId,
|
|
1081
|
+
const record = {
|
|
1082
|
+
runId,
|
|
753
1083
|
mountPath,
|
|
754
1084
|
assetPackSha,
|
|
755
1085
|
sourceCommitSha,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1086
|
+
};
|
|
1087
|
+
// Reserve durable recovery intent before the pack send. A replacement
|
|
1088
|
+
// generation may reuse the exact row its predecessor recorded.
|
|
1089
|
+
const inserted = await db
|
|
1090
|
+
.insert(sessionAssetTable)
|
|
1091
|
+
.values({ ...record, materializedAt: new Date() })
|
|
1092
|
+
.onConflictDoNothing({
|
|
1093
|
+
target: [sessionAssetTable.runId, sessionAssetTable.mountPath],
|
|
1094
|
+
})
|
|
1095
|
+
.returning({ runId: sessionAssetTable.runId });
|
|
1096
|
+
if (inserted.length === 0) {
|
|
1097
|
+
const existing = await db.query.sessionAsset.findFirst({
|
|
1098
|
+
where: and(eq(sessionAssetTable.runId, runId), eq(sessionAssetTable.mountPath, mountPath)),
|
|
1099
|
+
columns: {
|
|
1100
|
+
assetPackSha: true,
|
|
1101
|
+
sourceCommitSha: true,
|
|
1102
|
+
},
|
|
763
1103
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// Roll back the manifest row when the send fails so the manifest
|
|
767
|
-
// and the materialized state on the sidecar can never disagree.
|
|
768
|
-
// The forensic value of a manifest-without-materialization row is
|
|
769
|
-
// negligible because no agent will read against it. Wrap the
|
|
770
|
-
// rollback in its own try/catch so a rollback failure (DB gone,
|
|
771
|
-
// connection killed mid-launch) is logged rather than masking the
|
|
772
|
-
// primary sendPack error — the caller needs to see the original
|
|
773
|
-
// failure, not the secondary one.
|
|
774
|
-
try {
|
|
775
|
-
await db
|
|
776
|
-
.delete(sessionAssetTable)
|
|
777
|
-
.where(and(eq(sessionAssetTable.instanceId, instanceId), eq(sessionAssetTable.mountPath, mountPath)));
|
|
1104
|
+
if (existing === undefined) {
|
|
1105
|
+
throw new Error(`session_asset ${runId}/${mountPath} disappeared after its insert conflicted`);
|
|
778
1106
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
: String(rollbackErr);
|
|
783
|
-
logger.warn `session_asset rollback failed for instance=${instanceId} mountPath=${mountPath}: ${msg}`;
|
|
1107
|
+
if (existing.assetPackSha !== assetPackSha ||
|
|
1108
|
+
existing.sourceCommitSha !== sourceCommitSha) {
|
|
1109
|
+
throw new Error(`session_asset ${runId}/${mountPath} conflicts with the allocated workflow's restored asset`);
|
|
784
1110
|
}
|
|
785
|
-
throw err;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
async function resolveAttachments(service, agentId) {
|
|
789
|
-
const rows = await service.listAgentAssets(agentId);
|
|
790
|
-
const resolved = [];
|
|
791
|
-
for (const row of rows) {
|
|
792
|
-
resolved.push(await resolveAttachment(row));
|
|
793
1111
|
}
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
async function resolveAttachment(row) {
|
|
797
|
-
const mountPath = resolveMountPath(row);
|
|
798
|
-
const repoId = { kind: row.asset.kind, id: row.asset.id };
|
|
799
|
-
const sourceCommitSha = await agentRepoStore.repoStore.resolveRef(HUB_PRINCIPAL, repoId, row.ref);
|
|
800
|
-
if (sourceCommitSha === null) {
|
|
801
|
-
throw new Error(`attachment_ref_unresolved: ${row.asset.kind}/${row.asset.id} has no commit on ${row.ref}`);
|
|
802
|
-
}
|
|
803
|
-
const { pack, ref: returnedRef } = await agentRepoStore.repoStore.createPack(HUB_PRINCIPAL, repoId, row.ref);
|
|
804
|
-
return {
|
|
805
|
-
agentAssetId: row.id,
|
|
806
|
-
source: "direct",
|
|
807
|
-
assetName: row.asset.name,
|
|
808
|
-
assetKind: row.asset.kind,
|
|
809
|
-
mountPath,
|
|
810
|
-
sourceCommitSha,
|
|
811
|
-
repoId,
|
|
812
|
-
pack,
|
|
813
|
-
ref: returnedRef,
|
|
814
|
-
};
|
|
1112
|
+
await requireAllocationRouter().sendPackToAllocation(allocationTarget, agentAddress, pack, ref, sourceCommitSha, { mountPath, repoId });
|
|
815
1113
|
}
|
|
816
1114
|
/**
|
|
817
1115
|
* Build a per-agent `ClosureResolver` from the tenant's visible
|
|
@@ -895,13 +1193,11 @@ export function createSessionService(deps) {
|
|
|
895
1193
|
return { manifest, assetIndex };
|
|
896
1194
|
}
|
|
897
1195
|
/**
|
|
898
|
-
* Build a `ResolvedAttachment` for an asset the resolver
|
|
899
|
-
* from
|
|
900
|
-
*
|
|
901
|
-
* tarballs from), and `agentAssetId` is `null` so the fan-out path
|
|
902
|
-
* knows to skip the `session_asset` insert.
|
|
1196
|
+
* Build a `ResolvedAttachment` for an asset the tool-package resolver
|
|
1197
|
+
* picked from. The pack is read from the asset's main ref (the same
|
|
1198
|
+
* ref the resolver consumed tarballs from).
|
|
903
1199
|
*/
|
|
904
|
-
async function
|
|
1200
|
+
async function resolveAssetAttachment(args) {
|
|
905
1201
|
const repoId = { kind: args.asset.kind, id: args.asset.id };
|
|
906
1202
|
const sourceCommitSha = await agentRepoStore.repoStore.resolveRef(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
|
|
907
1203
|
if (sourceCommitSha === null) {
|
|
@@ -909,10 +1205,6 @@ export function createSessionService(deps) {
|
|
|
909
1205
|
}
|
|
910
1206
|
const { pack, ref: returnedRef } = await agentRepoStore.repoStore.createPack(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
|
|
911
1207
|
return {
|
|
912
|
-
agentAssetId: null,
|
|
913
|
-
source: "resolved",
|
|
914
|
-
assetName: args.asset.name,
|
|
915
|
-
assetKind: args.asset.kind,
|
|
916
1208
|
mountPath: args.mountPath,
|
|
917
1209
|
sourceCommitSha,
|
|
918
1210
|
repoId,
|
|
@@ -920,78 +1212,13 @@ export function createSessionService(deps) {
|
|
|
920
1212
|
ref: returnedRef,
|
|
921
1213
|
};
|
|
922
1214
|
}
|
|
923
|
-
function collectAvailableSkills(resolved) {
|
|
924
|
-
const entries = [];
|
|
925
|
-
for (const att of resolved) {
|
|
926
|
-
if (att.assetKind !== "skill")
|
|
927
|
-
continue;
|
|
928
|
-
const index = getSkillIndex(att.repoId.id, att.ref);
|
|
929
|
-
for (const entry of index) {
|
|
930
|
-
entries.push({
|
|
931
|
-
qualifiedName: `${att.assetName}/${entry.name}`,
|
|
932
|
-
description: entry.description,
|
|
933
|
-
workspacePath: `workspace/${att.mountPath}${entry.workspaceSubpath}`,
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return entries;
|
|
938
|
-
}
|
|
939
|
-
async function attemptCleanup(agentAddress, failedPhase, originalErr) {
|
|
940
|
-
try {
|
|
941
|
-
await sidecarRouter.sendAgentUndeploy(agentAddress, failedPhase);
|
|
942
|
-
}
|
|
943
|
-
catch (cleanupErr) {
|
|
944
|
-
logger.error `Failed to clean up agent ${agentAddress} after ${failedPhase} failure: ${String(cleanupErr)}`;
|
|
945
|
-
// Preserve the original error as cause so the root cause is not
|
|
946
|
-
// lost when the cleanup also fails.
|
|
947
|
-
throw new SessionLaunchError(failedPhase, originalErr, true);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
async function sendUserMessage(params) {
|
|
951
|
-
const { agentAddress, from, messageId, date, content, attachments, inReplyTo, references, sessionId, tenantId, cryptoProvider, } = params;
|
|
952
|
-
const headers = {
|
|
953
|
-
from,
|
|
954
|
-
to: [agentAddress],
|
|
955
|
-
cc: undefined,
|
|
956
|
-
date,
|
|
957
|
-
messageId,
|
|
958
|
-
subject: undefined,
|
|
959
|
-
inReplyTo,
|
|
960
|
-
references,
|
|
961
|
-
mimeVersion: "1.0",
|
|
962
|
-
interchangeType: "conversation.message",
|
|
963
|
-
interchangeCorrelationId: undefined,
|
|
964
|
-
interchangeTenantId: tenantId,
|
|
965
|
-
interchangeAgentId: undefined,
|
|
966
|
-
interchangeSessionId: sessionId,
|
|
967
|
-
interchangeOfferingId: undefined,
|
|
968
|
-
interchangeSchemaVersion: undefined,
|
|
969
|
-
traceparent: undefined,
|
|
970
|
-
tracestate: undefined,
|
|
971
|
-
};
|
|
972
|
-
const signedContent = assembleSignedContent({
|
|
973
|
-
kind: "conversation",
|
|
974
|
-
text: content,
|
|
975
|
-
...(attachments !== undefined ? { attachments } : {}),
|
|
976
|
-
});
|
|
977
|
-
const signature = await createDetachedSignatureFromProvider(signedContent, cryptoProvider);
|
|
978
|
-
const rawMessage = assembleMessage(headers, signedContent, signature);
|
|
979
|
-
const base64 = base64Encode(rawMessage);
|
|
980
|
-
const delivered = sidecarRouter.routeMail(agentAddress, base64);
|
|
981
|
-
if (!delivered) {
|
|
982
|
-
throw new Error(`Failed to deliver message to ${agentAddress}: agent is unreachable`);
|
|
983
|
-
}
|
|
984
|
-
return rawMessage;
|
|
985
|
-
}
|
|
986
1215
|
async function endSession(agentAddress, reason) {
|
|
987
1216
|
await sidecarRouter.sendAgentUndeploy(agentAddress, reason);
|
|
988
1217
|
}
|
|
989
1218
|
return {
|
|
990
1219
|
stageWorkflowStep,
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
deployWorkflowDefinition,
|
|
994
|
-
sendUserMessage,
|
|
1220
|
+
installAndApproveWorkflowSource,
|
|
1221
|
+
deployPreparedCodeSourcedWorkflow,
|
|
995
1222
|
endSession,
|
|
996
1223
|
};
|
|
997
1224
|
}
|