@intx/hub-sessions 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +84 -1
  3. package/dist/agent-repo.d.ts +89 -0
  4. package/dist/agent-repo.js +109 -0
  5. package/dist/agent-state-kind.d.ts +12 -0
  6. package/dist/agent-state-kind.js +185 -0
  7. package/dist/asset-service.d.ts +123 -0
  8. package/dist/asset-service.js +349 -0
  9. package/dist/available-skills-stanza.d.ts +21 -0
  10. package/dist/available-skills-stanza.js +32 -0
  11. package/dist/credential-push.d.ts +32 -0
  12. package/dist/credential-push.js +85 -0
  13. package/dist/event-collector-registry.d.ts +20 -0
  14. package/dist/event-collector-registry.js +115 -0
  15. package/dist/event-collector.d.ts +39 -0
  16. package/dist/event-collector.js +357 -0
  17. package/dist/hub-session-lookups.d.ts +17 -0
  18. package/dist/hub-session-lookups.js +204 -0
  19. package/dist/hub-session-orchestrator.d.ts +25 -0
  20. package/dist/hub-session-orchestrator.js +122 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +16 -0
  23. package/dist/package-registry-kind.d.ts +70 -0
  24. package/dist/package-registry-kind.js +260 -0
  25. package/dist/repo-store/index.d.ts +4 -0
  26. package/dist/repo-store/index.js +3 -0
  27. package/dist/repo-store/store.d.ts +41 -0
  28. package/dist/repo-store/store.js +1692 -0
  29. package/dist/repo-store/subscribe-kind.d.ts +53 -0
  30. package/dist/repo-store/subscribe-kind.js +179 -0
  31. package/dist/repo-store/types.d.ts +483 -0
  32. package/dist/repo-store/types.js +42 -0
  33. package/dist/session-service.d.ts +235 -0
  34. package/dist/session-service.js +997 -0
  35. package/dist/skill-kind.d.ts +41 -0
  36. package/dist/skill-kind.js +288 -0
  37. package/dist/substrate.d.ts +8 -0
  38. package/dist/substrate.js +21 -0
  39. package/dist/workflow-kind.d.ts +21 -0
  40. package/dist/workflow-kind.js +263 -0
  41. package/dist/workflow-run-event-log.d.ts +21 -0
  42. package/dist/workflow-run-event-log.js +51 -0
  43. package/dist/workflow-run-kind.d.ts +326 -0
  44. package/dist/workflow-run-kind.js +2646 -0
  45. package/dist/workflow-run-reader.d.ts +47 -0
  46. package/dist/workflow-run-reader.js +157 -0
  47. package/dist/ws/index.d.ts +3 -0
  48. package/dist/ws/index.js +3 -0
  49. package/dist/ws/sidecar-events.d.ts +134 -0
  50. package/dist/ws/sidecar-events.js +70 -0
  51. package/dist/ws/sidecar-handler.d.ts +184 -0
  52. package/dist/ws/sidecar-handler.js +1603 -0
  53. package/dist/ws/sidecar-token-authenticator.d.ts +15 -0
  54. package/dist/ws/sidecar-token-authenticator.js +24 -0
  55. package/package.json +34 -12
  56. package/src/agent-repo.test.ts +0 -310
  57. package/src/agent-repo.ts +0 -165
  58. package/src/agent-state-kind.test.ts +0 -247
  59. package/src/agent-state-kind.ts +0 -204
  60. package/src/asset-service.test.ts +0 -540
  61. package/src/asset-service.ts +0 -378
  62. package/src/available-skills-stanza.test.ts +0 -87
  63. package/src/available-skills-stanza.ts +0 -47
  64. package/src/credential-push.ts +0 -65
  65. package/src/event-collector-registry.test.ts +0 -73
  66. package/src/event-collector-registry.ts +0 -171
  67. package/src/event-collector.test.ts +0 -1387
  68. package/src/event-collector.ts +0 -424
  69. package/src/hub-session-lookups.ts +0 -206
  70. package/src/hub-session-orchestrator.test.ts +0 -510
  71. package/src/hub-session-orchestrator.ts +0 -213
  72. package/src/index.ts +0 -78
  73. package/src/repo-store/index.ts +0 -15
  74. package/src/repo-store/store.test.ts +0 -1169
  75. package/src/repo-store/store.ts +0 -428
  76. package/src/repo-store/types.ts +0 -253
  77. package/src/session-service.test.ts +0 -895
  78. package/src/session-service.ts +0 -464
  79. package/src/skill-kind.test.ts +0 -599
  80. package/src/skill-kind.ts +0 -350
  81. package/src/ws/index.ts +0 -18
  82. package/src/ws/sidecar-events.test.ts +0 -96
  83. package/src/ws/sidecar-events.ts +0 -231
  84. package/src/ws/sidecar-handler.test.ts +0 -2217
  85. package/src/ws/sidecar-handler.ts +0 -1574
  86. package/tsconfig.json +0 -4
  87. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,997 @@
1
+ import { type } from "arktype";
2
+ import { and, eq } from "drizzle-orm";
3
+ import { createDefaultDirectorRegistry, } from "@intx/agent";
4
+ import { getLogger } from "@intx/log";
5
+ import { assembleMessage, assembleSignedContent, createDetachedSignatureFromProvider, } from "@intx/mime";
6
+ import { listAssetsForTenant } from "@intx/db";
7
+ import { grant as grantTable, workflowDeployment as workflowDeploymentTable, } from "@intx/db/schema";
8
+ import { base64Encode, hexEncode } from "@intx/types";
9
+ import { generateId } from "@intx/hub-common";
10
+ import { sessionAsset as sessionAssetTable, } from "@intx/db/schema";
11
+ import { AssetRegistrySource, HttpRegistrySource, ManifestInvalidError, createClosureResolver, } from "@intx/tool-packaging";
12
+ import { ToolPackageManifest, } from "@intx/types/tool-packages";
13
+ import { defineWorkflow, } from "@intx/workflow/definition";
14
+ import { createWorkflowDeployOrchestrator, deriveDeploymentAddress, walkCapabilities, wrapHarnessAsSingleStepWorkflow, } from "@intx/workflow-deploy";
15
+ import { DEFAULT_ASSET_REF, } from "./asset-service.js";
16
+ import { buildAvailableSkillsStanza, } from "./available-skills-stanza.js";
17
+ import { getSkillIndex } from "./skill-kind.js";
18
+ const logger = getLogger(["interchange", "hub", "session-service"]);
19
+ export class SessionLaunchError extends Error {
20
+ /** Which phase failed: "write", "provision", "pack", or "start". */
21
+ phase;
22
+ /** True if the sidecar has a provisioned agent that could not be cleaned up. */
23
+ leakedAgent;
24
+ constructor(phase, cause, leakedAgent) {
25
+ const msg = cause instanceof Error ? cause.message : "Session launch failed";
26
+ super(msg, { cause });
27
+ this.name = "SessionLaunchError";
28
+ this.phase = phase;
29
+ this.leakedAgent = leakedAgent;
30
+ }
31
+ }
32
+ // Hub-side principal for reading skill repos. Skills are signed by the
33
+ // hub itself, and listAgentAssets is being called on the hub to assemble
34
+ // packs for delivery to a sidecar — so the hub principal is correct.
35
+ const HUB_PRINCIPAL = { kind: "hub" };
36
+ async function createPackSha(pack) {
37
+ const digest = await crypto.subtle.digest("SHA-256",
38
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ArrayBuffer-backed at the call site; Web Crypto's BufferSource type rejects Uint8Array<ArrayBufferLike> under TS 5.9 (microsoft/TypeScript#62240)
39
+ pack);
40
+ return hexEncode(new Uint8Array(digest));
41
+ }
42
+ /**
43
+ * Walk a resolved tool-package manifest and return every distinct
44
+ * `assetId` referenced by a `kind: "asset"` entry. Order is the
45
+ * resolver's BFS order so the fan-out below is deterministic for
46
+ * tests; a `Set` would be wrong here because tests assert specific
47
+ * orderings.
48
+ */
49
+ function collectDistinctAssetIds(manifest) {
50
+ const seen = new Set();
51
+ const out = [];
52
+ for (const entry of manifest.entries) {
53
+ if (entry.source.kind !== "asset")
54
+ continue;
55
+ if (seen.has(entry.source.assetId))
56
+ continue;
57
+ seen.add(entry.source.assetId);
58
+ out.push(entry.source.assetId);
59
+ }
60
+ return out;
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
+ /**
132
+ * Translate the orchestrator's structural `DeployContent` (which types
133
+ * `toolPackageManifest` as `unknown`) back into the hub-sessions
134
+ * `DeployContent` shape. The orchestrator round-trips whatever the
135
+ * caller supplied, but the surface type widens `toolPackageManifest` to
136
+ * `unknown`; the validator narrows it back to the canonical shape
137
+ * `agentRepoStore.writeDeployTree` consumes.
138
+ *
139
+ * Exported so a test fixture that forwards orchestrator-shaped deploy
140
+ * content into `launchSession` narrows it the same validated way the
141
+ * production multi-step callback does, rather than casting `unknown`.
142
+ */
143
+ export function bridgeOrchestratorDeployContent(content) {
144
+ const bridged = { systemPrompt: content.systemPrompt };
145
+ if (content.toolPackageManifest !== undefined) {
146
+ const validated = ToolPackageManifest(content.toolPackageManifest);
147
+ if (validated instanceof type.errors) {
148
+ throw new Error(`orchestrator deploy content carries an invalid toolPackageManifest: ${validated.summary}`);
149
+ }
150
+ bridged.toolPackageManifest = validated;
151
+ }
152
+ if (content.assetMounts !== undefined) {
153
+ bridged.assetMounts = content.assetMounts;
154
+ }
155
+ return bridged;
156
+ }
157
+ /**
158
+ * Wire the workflow-deploy orchestrator's `sendMultiStepDeploy`
159
+ * dependency against `SidecarRouter.sendAgentDeploy`. The router
160
+ * accepts an optional `workflow` projection on the deploy frame; the
161
+ * sidecar's deploy router uses field presence to route the frame to
162
+ * the workflow deploy path. The supervisor public key returned by the
163
+ * sidecar's `agent.deploy.ack` is threaded back as the
164
+ * `MultiStepDeployResult.publicKey`.
165
+ *
166
+ * Exported so the co-located caller-site test can assert that the
167
+ * closure constructed in `launchSession` reaches the wire surface via
168
+ * `sendAgentDeploy` with a `workflow` field structurally matching the
169
+ * `AgentDeployFrame.workflow` schema.
170
+ */
171
+ export async function sendMultiStepDeployFrame(args) {
172
+ // The wire validator's projection types `stepOrder` and `triggers`
173
+ // as mutable arrays while `WorkflowDefinition` declares them as
174
+ // `readonly`. The wire serializer never mutates the arrays; the
175
+ // shallow copies pay the readonly-widen at the boundary. Every
176
+ // field listed here must match the structural envelope the
177
+ // workflow-process child re-validates against on materialization
178
+ // (`workflowDefinitionEnvelopeSchema`): `id`, `triggers`, `steps`,
179
+ // `stepOrder`, optional `state`. The sidecar deploy router
180
+ // serializes this object verbatim into `workflow.json`; a missing
181
+ // envelope-required field here would round-trip into the child's
182
+ // envelope rejection on disk.
183
+ const wireDefinition = {
184
+ id: args.definition.id,
185
+ triggers: [...args.definition.triggers],
186
+ stepOrder: [...args.definition.stepOrder],
187
+ steps: args.definition.steps,
188
+ ...(args.definition.state !== undefined
189
+ ? { state: args.definition.state }
190
+ : {}),
191
+ };
192
+ return args.sidecarRouter.sendAgentDeploy(args.agentAddress, args.config, {
193
+ definition: wireDefinition,
194
+ sources: args.sources,
195
+ });
196
+ }
197
+ /**
198
+ * `WorkflowRepoWriter` backed by the hub's repo substrate. Writes the
199
+ * orchestrator-produced workflow tree (`workflow.json`,
200
+ * `capability-declarations.json`, `.gitignore`) into a `workflow`-kind
201
+ * repo keyed by the workflow definition id, committing on the published
202
+ * asset ref. The hub principal is the only writer of the workflow repo,
203
+ * matching `workflowAuthorize`'s hub-writes / sidecar-reads split.
204
+ */
205
+ function createHubWorkflowRepoWriter(agentRepoStore) {
206
+ return {
207
+ async writeWorkflowRepo(args) {
208
+ const repoId = { kind: "workflow", id: args.workflowRepoId };
209
+ const files = {};
210
+ for (const [path, contents] of args.files) {
211
+ files[path] = contents;
212
+ }
213
+ await agentRepoStore.repoStore.writeTree(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF, { files, message: "Write workflow deploy tree" });
214
+ },
215
+ };
216
+ }
217
+ export function createSessionService(deps) {
218
+ const { sidecarRouter, agentRepoStore, assetService, db, toolPackageRegistries, } = deps;
219
+ if (assetService !== undefined && db === undefined) {
220
+ throw new Error("createSessionService: db is required when assetService is set");
221
+ }
222
+ if (toolPackageRegistries !== undefined && db === undefined) {
223
+ throw new Error("createSessionService: db is required when toolPackageRegistries is set");
224
+ }
225
+ /**
226
+ * Stage a deploy on the sidecar: resolve assets and tool packages, write
227
+ * the deploy tree, provision the agent, and deliver the deploy + asset
228
+ * packs (Phases 0-2b). Phase 1's provision has two shapes:
229
+ * - `workflowFrame` set: the single-step head hand-off fires the
230
+ * deployment `agent.deploy` frame that spawns the workflow-process
231
+ * child. Returns the supervisor public key.
232
+ * - `stageOnly` set: a multi-step per-step stage binds a transient route
233
+ * for the step address, fires a no-spawn provision frame (init repo +
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.
238
+ */
239
+ async function executeLaunchPhases(params) {
240
+ const { agentAddress, agentId, instanceId, config, deployContent } = params;
241
+ const toolPackagePins = params.toolPackagePins ?? [];
242
+ const stageOnly = params.stageOnly ?? false;
243
+ if (params.workflowFrame !== undefined && stageOnly) {
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
+ };
270
+ // Phase 0a-bis: Resolve the agent's tool-package pins into a full
271
+ // closure manifest. Empty pins skip the resolver entirely. A
272
+ // ManifestInvalidError (e.g. unsatisfied peer dependency) is a
273
+ // launch-time failure — the deploy never ships and the sidecar
274
+ // is not touched.
275
+ //
276
+ // The resolver runs once per launch with no cross-launch caching;
277
+ // the packument cache scopes only within a single closure walk.
278
+ // Acceptable at the current N (handful of agents, small pin sets
279
+ // per agent) — a tenant-scoped packument cache or a per-pin set
280
+ // resolved-manifest cache would be the obvious scaling lever
281
+ // when launch latency becomes the bottleneck.
282
+ const manifestAssetAttachments = [];
283
+ if (toolPackagePins.length > 0) {
284
+ if (toolPackageRegistries === undefined) {
285
+ throw new SessionLaunchError("write", new Error(`agent ${agentId} has ${String(toolPackagePins.length)} pinned tool package(s) but the session service has no toolPackageRegistries configured`), false);
286
+ }
287
+ if (assetService === undefined) {
288
+ throw new SessionLaunchError("write", new Error(`agent ${agentId} has pinned tool packages but the session service has no assetService configured for asset-backed registries`), false);
289
+ }
290
+ let manifest;
291
+ let assetIndex;
292
+ try {
293
+ const built = await buildAndResolve({
294
+ agentId,
295
+ tenantId: config.tenantId,
296
+ pins: toolPackagePins,
297
+ registries: toolPackageRegistries,
298
+ assetService,
299
+ });
300
+ manifest = built.manifest;
301
+ assetIndex = built.assetIndex;
302
+ }
303
+ catch (err) {
304
+ if (err instanceof ManifestInvalidError) {
305
+ logger.warn `tool-package manifest validation failed for agent ${agentId}: ${err.message}`;
306
+ }
307
+ throw new SessionLaunchError("write", err, false);
308
+ }
309
+ const assetMounts = new Map();
310
+ 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
+ for (const assetId of collectDistinctAssetIds(manifest)) {
331
+ const asset = assetIndex.get(assetId);
332
+ if (asset === undefined) {
333
+ // The asset id appears in the manifest but is not in the
334
+ // tenant-visible asset set. This can only happen if the
335
+ // resolver's registry map and the asset index disagree —
336
+ // the same scan populated both, so reaching this branch
337
+ // would indicate an upstream invariant violation.
338
+ throw new Error(`resolved tool-package manifest references asset ${assetId} which is not visible to tenant ${config.tenantId}`);
339
+ }
340
+ const mountPath = `package-registries/${asset.name}/`;
341
+ assetMounts.set(assetId, mountPath);
342
+ manifestAssetAttachments.push(await resolveDirectAssetAttachment({
343
+ asset,
344
+ mountPath,
345
+ }));
346
+ }
347
+ }
348
+ catch (err) {
349
+ throw new SessionLaunchError("write", err, false);
350
+ }
351
+ effectiveDeployContent = {
352
+ ...effectiveDeployContent,
353
+ toolPackageManifest: manifest,
354
+ ...(assetMounts.size > 0 ? { assetMounts } : {}),
355
+ };
356
+ }
357
+ // Phase 0b: Write deploy tree and produce packfile (hub-local, no
358
+ // sidecar state to clean up if this fails).
359
+ let pack;
360
+ let commitSha;
361
+ let ref;
362
+ try {
363
+ await agentRepoStore.writeDeployTree(agentId, effectiveDeployContent);
364
+ ({ pack, commitSha, ref } =
365
+ await agentRepoStore.createDeployPack(agentId));
366
+ }
367
+ catch (err) {
368
+ throw new SessionLaunchError("write", err, false);
369
+ }
370
+ // A stage-only per-step deploy binds a transient route for the step
371
+ // address so the packs below route to the deployment's sidecar; the
372
+ // route is held only for the pack window and dropped in the `finally`.
373
+ if (stageOnly) {
374
+ try {
375
+ sidecarRouter.bindStepRoute(agentAddress);
376
+ }
377
+ catch (err) {
378
+ throw new SessionLaunchError("provision", err, false);
379
+ }
380
+ }
381
+ try {
382
+ // Phase 1: Provision on sidecar. A single-step workflow deploy sends
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
387
+ // no-spawn provision frame: the sidecar inits the step's agent-state
388
+ // repo and records the hub key, but spawns nothing. Firing the frame
389
+ // before the Phase 2 pack is the ordering barrier -- the repo must
390
+ // exist before the pack applies. A workflow frame's ack surfaces the
391
+ // supervisor public key to the caller.
392
+ let deployAckPublicKey;
393
+ try {
394
+ if (workflowFrame !== undefined) {
395
+ const ack = await sendMultiStepDeployFrame({
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);
406
+ }
407
+ else {
408
+ // Every caller supplies `workflowFrame` (single-step head) or
409
+ // `stageOnly` (multi-step per-step). A deploy with neither has no
410
+ // provisioning shape -- the legacy warm-harness path is gone -- so
411
+ // fail loud rather than ship a deploy pack the sidecar never
412
+ // provisioned a repo for.
413
+ throw new Error("executeLaunchPhases: a deploy requires either workflowFrame or stageOnly");
414
+ }
415
+ }
416
+ catch (err) {
417
+ throw new SessionLaunchError("provision", err, false);
418
+ }
419
+ // Phase 2: Pack delivery. On failure, the warm/workflow paths tear the
420
+ // sidecar deployment down; a stage-only step has no supervisor to
421
+ // undeploy, so it only drops its transient route (in the `finally`).
422
+ // The step's inited agent-state repo is left on the sidecar: the
423
+ // orchestrator aborts the whole deploy before the deployment frame is
424
+ // sent, so there is nothing to undeploy, and a redeploy of the same
425
+ // deployment overwrites the orphaned repo. This is an acceptable minor
426
+ // leak on the exceptional staging-failure path, not a live-path cost.
427
+ try {
428
+ await sidecarRouter.sendPack(agentAddress, pack, ref, commitSha);
429
+ }
430
+ catch (err) {
431
+ if (!stageOnly)
432
+ await attemptCleanup(agentAddress, "pack", err);
433
+ throw new SessionLaunchError("pack", err, false);
434
+ }
435
+ // Phase 2b: Asset-pack fan-out. For each attached asset, build a
436
+ // pack, insert the manifest row, then send the pack. The manifest
437
+ // insert MUST happen before the pack send: if the sidecar acks
438
+ // but the row is missing, the session has materialization without
439
+ // a recorded manifest. If the row insert fails, the pack send
440
+ // must not happen.
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`.
447
+ //
448
+ // Both sources can name the same `package-registry` asset — a
449
+ // direct attachment and a resolver pin would each compute
450
+ // `mountPath = "package-registries/<asset.name>/"` and collide on
451
+ // the `(instanceId, mountPath)` PK in `session_asset`. Dedup by
452
+ // asset id BEFORE the inserts and let the direct attachment win:
453
+ // it is an explicit operator action and carries an `agentAssetId`
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
+ });
461
+ 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
+ for (const att of fanOut) {
471
+ try {
472
+ await sendAttachmentPack(instanceId, agentAddress, att);
473
+ committed.push(att);
474
+ }
475
+ catch (err) {
476
+ await rollbackCommittedAttachments(instanceId, committed);
477
+ if (!stageOnly)
478
+ await attemptCleanup(agentAddress, "pack", err);
479
+ throw new SessionLaunchError("pack", err, false);
480
+ }
481
+ }
482
+ }
483
+ return deployAckPublicKey === undefined
484
+ ? undefined
485
+ : { publicKey: deployAckPublicKey };
486
+ }
487
+ finally {
488
+ if (stageOnly) {
489
+ sidecarRouter.unbindStepRoute(agentAddress);
490
+ }
491
+ }
492
+ }
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
+ /**
562
+ * Stage one step of a multi-step workflow deploy: bind a transient route
563
+ * for the step address, fire a no-spawn provision frame (the sidecar inits
564
+ * the step's agent-state repo and records the hub key), deliver the deploy
565
+ * and asset packs, and unbind the route -- no warm harness. The multi-step
566
+ * branch stages every step this way, then fires ONE deployment-level
567
+ * workflow frame that writes the step grants and spawns the supervised
568
+ * workflow-process child; the child reads each step's staged deploy tree
569
+ * from disk and runs the step itself.
570
+ */
571
+ async function stageWorkflowStep(params) {
572
+ await executeLaunchPhases({
573
+ agentAddress: params.agentAddress,
574
+ agentId: params.agentId,
575
+ instanceId: params.instanceId,
576
+ config: params.config,
577
+ deployContent: params.deployContent,
578
+ stageOnly: true,
579
+ ...(params.toolPackagePins !== undefined
580
+ ? { toolPackagePins: params.toolPackagePins }
581
+ : {}),
582
+ });
583
+ }
584
+ /**
585
+ * Deploy a single-agent instance through the single-step-at-head path: wrap
586
+ * the harness as a one-step workflow (the same wrap `launchSession` uses) and
587
+ * route it through `deploySingleStepAtHead` with the instance's REAL identity
588
+ * -- so the head address IS the instance address and the deploy runs as a
589
+ * supervised workflow-process child.
590
+ *
591
+ * Unlike the orchestrator's `runSingleStepAtHead`, this calls
592
+ * `deploySingleStepAtHead` directly with the route's real `agentId`
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.
598
+ */
599
+ async function deployInstanceAtHead(params) {
600
+ const { agentAddress, agentId, instanceId, config, deployContent } = params;
601
+ const singleStepAgent = wrapHarnessAsSingleStepWorkflow({
602
+ config,
603
+ deployContent,
604
+ });
605
+ const workflow = defineWorkflow({
606
+ id: `wf_${agentId}`,
607
+ agent: singleStepAgent,
608
+ trigger: { type: "mail", to: agentAddress },
609
+ });
610
+ // The sole step's id, read off the built definition.
611
+ const stepId = workflow.stepOrder[0];
612
+ if (stepId === undefined) {
613
+ throw new Error(`instance deploy for ${agentAddress}: the wrapped single-step workflow has an empty stepOrder`);
614
+ }
615
+ // Pin the step's inference sources to the instance's FULL ordered source
616
+ // chain so the workflow-process child's reactor fails over across it at
617
+ // runtime. The route already resolved and authorized `config.sources`
618
+ // against the tenant catalog, so it is pinned directly rather than re-run
619
+ // through the orchestrator's operator-approval gate.
620
+ //
621
+ // Fail loud on the invariant the reactor depends on: the reactor resolves
622
+ // its initial source by id (`defaultSource`) and fails over FORWARD-ONLY
623
+ // with no wrap, so the default must be element 0 or part of the chain is
624
+ // unreachable -- and if the default were last, failover would silently
625
+ // no-op. The route guarantees `config.sources[0].id === config.defaultSource`
626
+ // (head = active); assert it here so a future reordering fails loudly
627
+ // rather than silently disabling failover.
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 }
645
+ : {}),
646
+ });
647
+ }
648
+ async function deployWorkflowDefinition(params) {
649
+ const { tenantId, deploymentId, deploymentDomain, definition, definitionAssetId, config, deployContent, } = params;
650
+ // The deploy is initiated by an authorized tenant operator against a
651
+ // workflow asset they authored; approve exactly the grant surface the
652
+ // definition declares. The same director registry feeds both this
653
+ // approval-set derivation and the orchestrator's gate so the walk the
654
+ // route approves and the walk the orchestrator enforces are identical.
655
+ const directorRegistry = createDefaultDirectorRegistry();
656
+ const walk = walkCapabilities(definition, directorRegistry);
657
+ const operatorApprovals = new Set([...walk.perStep.values()].flatMap((declarations) => [
658
+ ...declarations.grants,
659
+ ]));
660
+ const result = await runWorkflowDeploy({
661
+ workflowRepo: createHubWorkflowRepoWriter(agentRepoStore),
662
+ directorRegistry,
663
+ deployArgs: {
664
+ workflow: definition,
665
+ deploymentId,
666
+ deploymentDomain,
667
+ config,
668
+ deployContent,
669
+ operatorApprovals,
670
+ hubPublicKey: hexEncode(agentRepoStore.getSigningPublicKey()),
671
+ ...(params.toolPackagePins !== undefined
672
+ ? { toolPackagePins: params.toolPackagePins }
673
+ : {}),
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
+ });
691
+ // Seed a read grant on the deployment's workflow-run resource for the
692
+ // deploying principal so they can observe run events out of the box,
693
+ // mirroring the per-instance agent-state read grant the agent deploy
694
+ // path seeds for the creator. Without this a non-owner deployer would
695
+ // deploy a workflow they cannot read the runs of.
696
+ await tx.insert(grantTable).values({
697
+ id: generateId("grant"),
698
+ tenantId,
699
+ principalId: config.principalId,
700
+ resource: `workflow-run:${deploymentId}`,
701
+ action: "read",
702
+ effect: "allow",
703
+ origin: "creator",
704
+ createdAt: now,
705
+ updatedAt: now,
706
+ });
707
+ });
708
+ return {
709
+ deploymentId,
710
+ deploymentAddress: deriveDeploymentAddress({
711
+ deploymentId,
712
+ deploymentDomain,
713
+ }),
714
+ publicKey: result.publicKey,
715
+ };
716
+ }
717
+ async function rollbackCommittedAttachments(instanceId, committed) {
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)));
730
+ }
731
+ catch (err) {
732
+ logger.warn `session_asset rollback failed for earlier-committed instance=${instanceId} mountPath=${att.mountPath}: ${err instanceof Error ? err.message : String(err)}`;
733
+ }
734
+ }
735
+ }
736
+ async function sendAttachmentPack(instanceId, agentAddress, attachment) {
737
+ if (db === undefined) {
738
+ // Guarded at construction; reassert defensively so the
739
+ // narrowing is visible to readers and a future refactor cannot
740
+ // accidentally invoke this without a db.
741
+ throw new Error("sendAttachmentPack invoked without a db handle");
742
+ }
743
+ const { agentAssetId, source, mountPath, sourceCommitSha, repoId, pack, ref, } = attachment;
744
+ const assetPackSha = await createPackSha(pack);
745
+ // Insert manifest row before the pack send so we never end up in
746
+ // the materialized-without-manifest state. Both direct and
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,
753
+ mountPath,
754
+ assetPackSha,
755
+ sourceCommitSha,
756
+ source,
757
+ materializedAt: new Date(),
758
+ });
759
+ try {
760
+ await sidecarRouter.sendPack(agentAddress, pack, ref, sourceCommitSha, {
761
+ mountPath,
762
+ repoId,
763
+ });
764
+ }
765
+ catch (err) {
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)));
778
+ }
779
+ catch (rollbackErr) {
780
+ const msg = rollbackErr instanceof Error
781
+ ? rollbackErr.message
782
+ : String(rollbackErr);
783
+ logger.warn `session_asset rollback failed for instance=${instanceId} mountPath=${mountPath}: ${msg}`;
784
+ }
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
+ }
794
+ return resolved;
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
+ };
815
+ }
816
+ /**
817
+ * Build a per-agent `ClosureResolver` from the tenant's visible
818
+ * package-registry assets plus the statically-configured HTTP
819
+ * registries, then run the closure resolution against `pins`.
820
+ *
821
+ * Returns the resolved manifest and an asset-id-keyed index of the
822
+ * package-registry assets the resolver knew about, so the caller can
823
+ * derive mount paths from the asset name without a second DB hit.
824
+ */
825
+ async function buildAndResolve(args) {
826
+ if (db === undefined) {
827
+ // Guarded at construction; restate for the narrowing.
828
+ throw new Error("buildAndResolve invoked without a db handle");
829
+ }
830
+ const visibleAssets = await listAssetsForTenant(db, args.tenantId, "package-registry");
831
+ const registryMap = new Map();
832
+ // `assetIndex` carries only the assets the resolver might have
833
+ // read from — i.e. one row per registry name, the one that won
834
+ // its `(kind, name)` slot. Shadowed assets that lost the
835
+ // collision are deliberately excluded: the resolver can never
836
+ // reach them, so the fan-out path must never see them in the
837
+ // index either. The walker walks leaf-to-root inside
838
+ // `listAssetsForTenant`, so the first occurrence of any
839
+ // `(kind, name)` wins — we replay the same shadowing here.
840
+ // Shadowed assets — those that lose the `(kind, name)` collision
841
+ // contest at a lower tenancy level — are dropped entirely from
842
+ // the per-launch registry map. They never appear in `assetIndex`
843
+ // either, so the fan-out that translates `kind: "asset"` manifest
844
+ // entries back to asset rows cannot reach them. This matches the
845
+ // resolver's view: a closure built from this map sees exactly
846
+ // the assets the resolver would have read from, and shadowed
847
+ // tarballs are invisible to both layers.
848
+ const assetIndex = new Map();
849
+ for (const row of visibleAssets) {
850
+ if (registryMap.has(row.name))
851
+ continue;
852
+ const asset = {
853
+ id: row.id,
854
+ tenantId: row.tenantId,
855
+ kind: "package-registry",
856
+ name: row.name,
857
+ displayName: row.displayName,
858
+ creatorPrincipalId: row.creatorPrincipalId,
859
+ createdAt: row.createdAt,
860
+ updatedAt: row.updatedAt,
861
+ };
862
+ assetIndex.set(asset.id, asset);
863
+ registryMap.set(asset.name, new AssetRegistrySource({
864
+ name: asset.name,
865
+ assetId: asset.id,
866
+ readBlob: (path) => args.assetService.readAssetBlob({
867
+ assetId: asset.id,
868
+ path,
869
+ }),
870
+ listBlobs: (dir) => args.assetService.listAssetBlobs({
871
+ assetId: asset.id,
872
+ dir,
873
+ }),
874
+ }));
875
+ }
876
+ for (const [name, cfg] of args.registries.httpRegistries) {
877
+ // Asset wins on collision with an HTTP registry of the same
878
+ // name; symmetric with the inner-shadows-outer rule that
879
+ // governs the tenant walker.
880
+ if (registryMap.has(name))
881
+ continue;
882
+ registryMap.set(name, new HttpRegistrySource({ name, config: cfg }));
883
+ }
884
+ if (!registryMap.has(args.registries.defaultRegistry)) {
885
+ throw new Error(`agent ${args.agentId}: defaultRegistry "${args.registries.defaultRegistry}" is neither a tenant-visible package-registry asset nor a configured HTTP registry`);
886
+ }
887
+ const resolver = createClosureResolver({
888
+ registries: registryMap,
889
+ defaultRegistry: args.registries.defaultRegistry,
890
+ ...(args.registries.scopeRouting !== undefined
891
+ ? { scopeRouting: args.registries.scopeRouting }
892
+ : {}),
893
+ });
894
+ const manifest = await resolver.resolveClosure(args.pins);
895
+ return { manifest, assetIndex };
896
+ }
897
+ /**
898
+ * Build a `ResolvedAttachment` for an asset the resolver picked
899
+ * from but which has no per-agent attachment row. The pack is read
900
+ * from the asset's main ref (the same ref the resolver consumed
901
+ * tarballs from), and `agentAssetId` is `null` so the fan-out path
902
+ * knows to skip the `session_asset` insert.
903
+ */
904
+ async function resolveDirectAssetAttachment(args) {
905
+ const repoId = { kind: args.asset.kind, id: args.asset.id };
906
+ const sourceCommitSha = await agentRepoStore.repoStore.resolveRef(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
907
+ if (sourceCommitSha === null) {
908
+ throw new Error(`tool-package asset ${args.asset.kind}/${args.asset.id} has no commit on ${DEFAULT_ASSET_REF}`);
909
+ }
910
+ const { pack, ref: returnedRef } = await agentRepoStore.repoStore.createPack(HUB_PRINCIPAL, repoId, DEFAULT_ASSET_REF);
911
+ return {
912
+ agentAssetId: null,
913
+ source: "resolved",
914
+ assetName: args.asset.name,
915
+ assetKind: args.asset.kind,
916
+ mountPath: args.mountPath,
917
+ sourceCommitSha,
918
+ repoId,
919
+ pack,
920
+ ref: returnedRef,
921
+ };
922
+ }
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
+ async function endSession(agentAddress, reason) {
987
+ await sidecarRouter.sendAgentUndeploy(agentAddress, reason);
988
+ }
989
+ return {
990
+ stageWorkflowStep,
991
+ deployInstanceAtHead,
992
+ deploySingleStepAtHead,
993
+ deployWorkflowDefinition,
994
+ sendUserMessage,
995
+ endSession,
996
+ };
997
+ }