@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.
Files changed (87) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +23 -7
  3. package/dist/agent-repo.js +19 -6
  4. package/dist/agent-state-kind.js +18 -63
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +19 -97
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +52 -7
  10. package/dist/credential-push.js +178 -22
  11. package/dist/event-collector-registry.d.ts +3 -2
  12. package/dist/event-collector-registry.js +42 -13
  13. package/dist/event-collector.d.ts +12 -2
  14. package/dist/event-collector.js +45 -4
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +541 -86
  17. package/dist/hub-session-orchestrator.d.ts +2 -3
  18. package/dist/hub-session-orchestrator.js +22 -56
  19. package/dist/index.d.ts +19 -9
  20. package/dist/index.js +16 -7
  21. package/dist/reconciliation-scheduler.d.ts +14 -0
  22. package/dist/reconciliation-scheduler.js +55 -0
  23. package/dist/repo-store/index.d.ts +2 -1
  24. package/dist/repo-store/index.js +1 -0
  25. package/dist/repo-store/store.d.ts +1 -1
  26. package/dist/repo-store/store.js +138 -1
  27. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  28. package/dist/repo-store/subscribe-kind.js +42 -77
  29. package/dist/repo-store/types.d.ts +94 -6
  30. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  31. package/dist/repo-store/user-principal-gate.js +78 -0
  32. package/dist/session-service.d.ts +252 -126
  33. package/dist/session-service.js +851 -624
  34. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  35. package/dist/sidecar-allocation/capability-policy.js +124 -0
  36. package/dist/sidecar-allocation/contracts.d.ts +101 -0
  37. package/dist/sidecar-allocation/contracts.js +26 -0
  38. package/dist/sidecar-allocation/index.d.ts +5 -0
  39. package/dist/sidecar-allocation/index.js +4 -0
  40. package/dist/sidecar-allocation/operation.d.ts +10 -0
  41. package/dist/sidecar-allocation/operation.js +54 -0
  42. package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
  43. package/dist/sidecar-allocation/plugin-registry.js +61 -0
  44. package/dist/sidecar-allocation/reconciler.d.ts +54 -0
  45. package/dist/sidecar-allocation/reconciler.js +825 -0
  46. package/dist/skill-kind.js +12 -62
  47. package/dist/substrate.d.ts +3 -3
  48. package/dist/substrate.js +1 -1
  49. package/dist/workflow-allocation-service.d.ts +64 -0
  50. package/dist/workflow-allocation-service.js +554 -0
  51. package/dist/workflow-closure-resolution.d.ts +106 -0
  52. package/dist/workflow-closure-resolution.js +123 -0
  53. package/dist/workflow-definition-ensure.d.ts +24 -0
  54. package/dist/workflow-definition-ensure.js +75 -0
  55. package/dist/workflow-dispatch-service.d.ts +42 -0
  56. package/dist/workflow-dispatch-service.js +209 -0
  57. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  58. package/dist/workflow-dispatch-settlement.js +140 -0
  59. package/dist/workflow-kind.d.ts +29 -1
  60. package/dist/workflow-kind.js +143 -139
  61. package/dist/workflow-probe-gate.d.ts +286 -0
  62. package/dist/workflow-probe-gate.js +382 -0
  63. package/dist/workflow-run-kind.d.ts +239 -32
  64. package/dist/workflow-run-kind.js +874 -188
  65. package/dist/workflow-run-reader.d.ts +1 -1
  66. package/dist/workflow-run-reader.js +3 -7
  67. package/dist/workflow-run-restore.d.ts +16 -0
  68. package/dist/workflow-run-restore.js +30 -0
  69. package/dist/workflow-source-closure.d.ts +35 -0
  70. package/dist/workflow-source-closure.js +342 -0
  71. package/dist/workflow-source-pins.d.ts +8 -0
  72. package/dist/workflow-source-pins.js +14 -0
  73. package/dist/ws/index.d.ts +3 -3
  74. package/dist/ws/index.js +2 -2
  75. package/dist/ws/pending-tracker.d.ts +93 -0
  76. package/dist/ws/pending-tracker.js +132 -0
  77. package/dist/ws/sidecar-events.d.ts +139 -37
  78. package/dist/ws/sidecar-events.js +2 -2
  79. package/dist/ws/sidecar-handler.d.ts +218 -60
  80. package/dist/ws/sidecar-handler.js +1713 -732
  81. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  82. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  83. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  84. package/dist/ws/sidecar-token-authenticator.js +78 -7
  85. package/package.json +14 -13
  86. package/dist/available-skills-stanza.d.ts +0 -21
  87. package/dist/available-skills-stanza.js +0 -32
@@ -0,0 +1,382 @@
1
+ // Hub-side install/approve orchestration and gate for a code-sourced workflow
2
+ // definition.
3
+ //
4
+ // This is the production glue that turns a code-sourced workflow install into
5
+ // an approved, frozen definition WITHOUT executing any author code on the hub.
6
+ // Everything here operates over the inert projection and grant set the sidecar
7
+ // returns from a probe:
8
+ //
9
+ // 1. Resolve the frozen dependency closure for the definition's pin.
10
+ // 2. Probe the sidecar for the definition's inert needs-surface projection,
11
+ // its advisory grant set, and the content hash the sidecar shipped.
12
+ // 3. RECOMPUTE the wire hash over the RECEIVED projection as tamper-evidence:
13
+ // a shipped hash that differs from the hub recompute is rejected, fail
14
+ // closed, no coercion.
15
+ // 4. Reject a projection declaring a trigger type the runtime does not
16
+ // implement, so a deployment that could only ever sit inert never gets
17
+ // approved.
18
+ // 5. Reject a projection carrying a step the grant walk left no record for,
19
+ // so no deployment can schedule a step whose grants nobody approved.
20
+ // 6. Gate the advisory grant set AND the definition's declared grant
21
+ // requirements against the approval policy: an operator `ApprovalSet`
22
+ // requires every grant the probe surfaced and every requirement it
23
+ // declared to be approved or the gate fails, while `approve-probed`
24
+ // approves exactly what the probe surfaced.
25
+ // 7. Freeze the approved wire hash onto the definition version row, keyed by
26
+ // the definition's selector, and return the frozen approved grant set.
27
+ //
28
+ // The frozen approved set is the single source of truth for the definition's
29
+ // grants: the deploy path materializes deploy grants as a SUBSET of it (never a
30
+ // fresh walk), so a workflow can never acquire at deploy or run time a grant it
31
+ // did not have frozen at approval. The wire hash is that freeze's anchor -- the
32
+ // grant set is a deterministic projection of the exact content the hash
33
+ // addresses, so pinning the hash pins the set.
34
+ import { type } from "arktype";
35
+ import { and, eq } from "drizzle-orm";
36
+ import { workflowDefinitionVersion } from "@intx/db/schema";
37
+ import { computeWireDefinitionHash } from "@intx/types/wire-definition-hash";
38
+ import { EXECUTABLE_STEP_DESCENT, walkStepTree, } from "@intx/workflow/definition";
39
+ import { approvalSetFromItems, inertNestedBodies, isApprovedGrantRequirement, } from "@intx/workflow-deploy";
40
+ import { buildSourceAssetMounts, resolveWorkflowClosure, } from "./workflow-closure-resolution.js";
41
+ import { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
42
+ // The version `ensureWorkflowDefinitionForAsset` projects for a fresh
43
+ // definition, and therefore the row the approval freeze targets. Kept in step
44
+ // with the ensure helper: if that helper ever projects a different initial
45
+ // version, this must follow.
46
+ const FROZEN_VERSION = "1";
47
+ /**
48
+ * Build the production persistence step of the freeze. Records identity through
49
+ * the selector-keyed ensure helper (a definition keyed by `(assetId,
50
+ * wireHash)`) and writes the approved wire hash and the grant-walk snapshot onto
51
+ * that definition's version row in one transaction. The grant SET is not written
52
+ * to a version-row column -- the approved wire hash already pins the content the
53
+ * grants project from -- so it travels with the returned frozen approval; the
54
+ * snapshot is written because the run path reads it back to materialize grants.
55
+ */
56
+ export function createDbFrozenApprovalWriter(db) {
57
+ return async ({ assetId, approvedWireHash, grantSnapshot }) => {
58
+ // Ensure-then-stamp is one freeze: a crash between the two would persist a
59
+ // version row with a NULL `approvedWireHash`, which the schema treats as
60
+ // the legitimate "not yet approved" state -- indistinguishable from an
61
+ // un-approved definition. Wrap both writes in one transaction so the freeze
62
+ // is all-or-nothing.
63
+ return db.transaction(async (tx) => {
64
+ const { definitionId } = await ensureWorkflowDefinitionForAsset(tx, {
65
+ assetId,
66
+ wireHash: approvedWireHash,
67
+ });
68
+ // `FROZEN_VERSION` is hand-coupled to the version the ensure helper
69
+ // projects; if that coupling ever drifts, the update would silently stamp
70
+ // zero rows and no hash would persist. Assert exactly one row so a drift
71
+ // fails loud instead of open.
72
+ const stamped = await tx
73
+ .update(workflowDefinitionVersion)
74
+ .set({ approvedWireHash, grantSnapshot })
75
+ .where(and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, FROZEN_VERSION)))
76
+ .returning({ id: workflowDefinitionVersion.id });
77
+ if (stamped.length !== 1) {
78
+ throw new Error(`createDbFrozenApprovalWriter: expected to stamp exactly one ${FROZEN_VERSION} version row for definition ${definitionId}, but updated ${String(stamped.length)}`);
79
+ }
80
+ return { definitionId };
81
+ });
82
+ };
83
+ }
84
+ function isApproveProbed(policy) {
85
+ return policy.kind === "approve-probed";
86
+ }
87
+ /**
88
+ * Trigger types the definition vocabulary declares but the runtime does not
89
+ * implement. `schedule` is reserved: no cron parser and no scheduler exist, so
90
+ * a schedule-triggered deployment would hash, deploy, and then never fire --
91
+ * no error, no log, no failed run. Admitting one at the gate is the only way a
92
+ * deployment reaches that state, so the gate refuses it.
93
+ */
94
+ const UNIMPLEMENTED_TRIGGER_TYPES = new Set(["schedule"]);
95
+ // A projected trigger, typed only to its discriminant. `triggers` rides the
96
+ // wire projection as `unknown[]` on purpose (the wire envelope does not own
97
+ // the trigger vocabulary), so the discriminant is read through a validator
98
+ // rather than an assertion. An entry that carries no string `type` is not a
99
+ // trigger this gate has an opinion about and is left to the deploy path.
100
+ const ProjectedTriggerType = type({ type: "string" });
101
+ /**
102
+ * The distinct unimplemented trigger types a projection declares, in first-seen
103
+ * order. Empty when every declared trigger has an implementation behind it.
104
+ */
105
+ function collectUnimplementedTriggerTypes(triggers) {
106
+ const found = [];
107
+ for (const trigger of triggers) {
108
+ const parsed = ProjectedTriggerType(trigger);
109
+ if (parsed instanceof type.errors)
110
+ continue;
111
+ if (!UNIMPLEMENTED_TRIGGER_TYPES.has(parsed.type))
112
+ continue;
113
+ if (found.includes(parsed.type))
114
+ continue;
115
+ found.push(parsed.type);
116
+ }
117
+ return found;
118
+ }
119
+ const EXECUTABLE_CLOSURE_CONTEXT = "probe gate executable closure: ";
120
+ /**
121
+ * Every step the deployment can execute, walked over the frozen inert
122
+ * projection under `EXECUTABLE_STEP_DESCENT`, each carrying the chain it was
123
+ * reached through and the top-level step whose grant record accounts for it.
124
+ *
125
+ * The descent is the canonical one rather than a local re-derivation on
126
+ * purpose. Sharing `EXECUTABLE_STEP_DESCENT` with the capability walk makes the
127
+ * two sides agree on ONE thing: which primitive kinds are descended into, so a
128
+ * newly-added container kind cannot become reachable here while staying
129
+ * invisible to the walk. It does NOT make the two agree about grants. This
130
+ * function sees only step positions; whether the walk folded the right grants
131
+ * into a record is not observable from the inert projection and is not checked
132
+ * anywhere on this path.
133
+ *
134
+ * The position each step was reached at is the walk's own `path`, for the same
135
+ * reason. The head of that path is the top-level step the entry descends from,
136
+ * which is exactly the `perStep` key the capability walk folds its grants into.
137
+ */
138
+ function collectExecutableReaches(projection) {
139
+ const reaches = [];
140
+ walkStepTree({
141
+ tree: projection,
142
+ context: EXECUTABLE_CLOSURE_CONTEXT,
143
+ nestedTrees: (step) => inertNestedBodies(step, EXECUTABLE_STEP_DESCENT),
144
+ visit: ({ stepId, path }) => {
145
+ reaches.push({ stepId, recordStepId: path[0], reachedThrough: path });
146
+ },
147
+ });
148
+ return reaches;
149
+ }
150
+ /**
151
+ * Every executable step the grant-walk snapshot carries no record for.
152
+ *
153
+ * A step's approved-grant record is the snapshot entry keyed by the top-level
154
+ * step it descends from: the capability walk collects one record per top-level
155
+ * step and folds into it the grants of every step that step can run.
156
+ *
157
+ * This is a PRESENCE check and nothing more. An absent record is decisive --
158
+ * it leaves every step of that subtree with no approved grants at all, which
159
+ * is the hole this catches. A present record is not evidence the other way:
160
+ * nothing here opens the record to confirm it actually carries the grants the
161
+ * steps beneath it need. A record present but under-filled passes this check
162
+ * and still refuses those tool calls at run time.
163
+ */
164
+ function collectStepsWithoutGrantRecord(projection, snapshot) {
165
+ const recordedStepIds = new Set(snapshot.perStep.map((record) => record.stepId));
166
+ return collectExecutableReaches(projection).filter((reach) => !recordedStepIds.has(reach.recordStepId));
167
+ }
168
+ function describeStepsWithoutGrantRecord(missing) {
169
+ const positions = missing
170
+ .map((missed) => `${missed.stepId} (reached through ${missed.reachedThrough.join(" > ")}; expected a grant-walk record keyed by top-level step ${missed.recordStepId})`)
171
+ .join("; ");
172
+ return `the probe's grant-walk snapshot carries no approved-grant record covering ${String(missing.length)} executable step(s): ${positions}. A step outside every record deploys with no approved grants, and its tool calls are refused at run time with nothing on the deploy path reporting it.`;
173
+ }
174
+ /**
175
+ * Gate a probe result and, on approval, freeze it. Operates purely over the
176
+ * inert projection and grant set -- no author code runs here and the capability
177
+ * walk is never re-run.
178
+ *
179
+ * Fails closed on the three security-load-bearing checks before it writes
180
+ * anything: the recomputed wire hash must match the hash the sidecar shipped
181
+ * (tamper-evidence), every advisory grant must be operator-approved, and every
182
+ * declared grant requirement must be operator-approved. It also refuses a
183
+ * projection whose triggers include a reserved-but-unimplemented type -- not a
184
+ * security check, but the layer a pinned closure cannot carry a stale copy of,
185
+ * so it is where a workflow that could only sit inert is caught -- and one
186
+ * whose executable closure reaches a step the grant walk left no record for --
187
+ * the layer holding both halves of the probe answer at once, and a presence
188
+ * check on those records rather than a check that any record's contents are
189
+ * sufficient.
190
+ * Only then does it freeze the recomputed hash onto the version row and return
191
+ * the approved grant set.
192
+ */
193
+ export async function gateAndFreezeProbeResult(args) {
194
+ const { assetId, probeResult, approvals, persist } = args;
195
+ // Tamper-evidence: recompute over the RECEIVED projection and compare to the
196
+ // shipped hash. A mismatch means the projection the hub is approving is not
197
+ // the one the sidecar hashed, so reject rather than freeze a hash that does
198
+ // not describe the approved content.
199
+ const recomputedWireHash = await computeWireDefinitionHash(probeResult.projection);
200
+ if (recomputedWireHash !== probeResult.wireHash) {
201
+ return {
202
+ ok: false,
203
+ reason: "wire_hash_mismatch",
204
+ shippedWireHash: probeResult.wireHash,
205
+ recomputedWireHash,
206
+ };
207
+ }
208
+ // Reject a trigger type nothing implements. This runs on the projection
209
+ // rather than on the author's definition because `defineWorkflow` is bundled
210
+ // INTO the pinned workflow closure: a closure published before the authoring
211
+ // check carries its own frozen copy and never sees it. The projection's
212
+ // `triggers` are produced by the hub's live->inert projector, so this is the
213
+ // one trigger surface a stale closure cannot carry past. Placed after the
214
+ // wire-hash check so tamper-evidence still decides first -- the projection
215
+ // must be the one the sidecar hashed before its content is reasoned about.
216
+ const unimplementedTriggerTypes = collectUnimplementedTriggerTypes(probeResult.projection.triggers);
217
+ if (unimplementedTriggerTypes.length > 0) {
218
+ return {
219
+ ok: false,
220
+ reason: "unimplemented_trigger",
221
+ unimplementedTriggerTypes,
222
+ };
223
+ }
224
+ // Totality: every step the deployment can execute must have an approved-grant
225
+ // record behind it. The two halves of a probe answer are produced
226
+ // independently -- the projection by the hub's live->inert projector, the
227
+ // grant-walk snapshot by the sidecar's capability walk over the live
228
+ // definition -- and nothing until now compared them. A step the walk skipped
229
+ // still projects, still deploys, and is still scheduled; its tool calls are
230
+ // then refused for lack of any grant, and the tool runner turns that refusal
231
+ // into an error tool result rather than a failure, so the run completes
232
+ // having done none of the work. This is the check that makes that
233
+ // unreachable: it is total over the closure and it runs on every deploy,
234
+ // rather than depending on some test happening to invoke a tool from the
235
+ // affected step.
236
+ //
237
+ // Placed after the trigger check and before the operator-policy checks
238
+ // below. A deploy that trips this is a defect in the deploy path, not a
239
+ // decision the operator can make differently, so it must not be reported
240
+ // behind an unapproved-grant message an operator would act on instead.
241
+ //
242
+ // DO NOT move this assertion earlier in this package's history. The record it
243
+ // requires is a claim that the approval covers everything the step can run,
244
+ // and that claim was not kept for a step inside a loop body or a section body
245
+ // until the deploy and runtime producers were made total over the executable
246
+ // closure. Asserted before those producers, the gate would have been
247
+ // enforcing a guarantee the rest of the system did not honour.
248
+ const stepsWithoutGrantRecord = collectStepsWithoutGrantRecord(probeResult.projection, probeResult.grantWalkSnapshot);
249
+ if (stepsWithoutGrantRecord.length > 0) {
250
+ return {
251
+ ok: false,
252
+ reason: "steps_without_grant_record",
253
+ stepsWithoutGrantRecord,
254
+ message: describeStepsWithoutGrantRecord(stepsWithoutGrantRecord),
255
+ };
256
+ }
257
+ // Gate the advisory grant set. Under an `ApprovalSet` every grant the probe
258
+ // surfaced must appear in the operator's approved set; any miss fails the
259
+ // gate closed. Under `approve-probed` there is no set to gate against -- the
260
+ // probe's surface IS the approved set -- so nothing is ever unapproved.
261
+ const unapprovedGrants = isApproveProbed(approvals)
262
+ ? []
263
+ : probeResult.grants.filter((grant) => !approvals.grants.has(grant));
264
+ if (unapprovedGrants.length > 0) {
265
+ return { ok: false, reason: "grants_not_approved", unapprovedGrants };
266
+ }
267
+ // Gate the DECLARED grant requirements. These ride the walk snapshot rather
268
+ // than the flattened `grants`, and the walk never surfaces them, so the
269
+ // filter above cannot see them -- yet the run path materializes each one into
270
+ // a real grant row on the run principal, and a wildcard row reaches gates no
271
+ // walk-derived row can address. They therefore need the operator's decision
272
+ // on exactly the same terms the advertised grants do. Under `approve-probed`
273
+ // the probe's surface IS the approved surface, so there is nothing to gate
274
+ // against and the requirements ride into the approval below.
275
+ const declaredRequirements = probeResult.grantWalkSnapshot.grantRequirements;
276
+ const unapprovedGrantRequirements = isApproveProbed(approvals)
277
+ ? []
278
+ : declaredRequirements.filter((requirement) => !isApprovedGrantRequirement(approvals, requirement));
279
+ if (unapprovedGrantRequirements.length > 0) {
280
+ return {
281
+ ok: false,
282
+ reason: "grant_requirements_not_approved",
283
+ unapprovedGrantRequirements,
284
+ };
285
+ }
286
+ // Freeze: the approved surface is exactly what the workflow advertised (all
287
+ // of it now operator-approved), pinned to the recomputed hash. Persisting the
288
+ // hash is the freeze; the approved surface is returned for the deploy
289
+ // hand-off. The declared requirements join the walk's grant strings in that
290
+ // surface under both policies, so the record the hand-off and any audit read
291
+ // describes every kind of authority the freeze will mint.
292
+ const approvedGrants = [
293
+ ...probeResult.grants,
294
+ ...declaredRequirements,
295
+ ];
296
+ const { definitionId } = await persist({
297
+ assetId,
298
+ approvedWireHash: recomputedWireHash,
299
+ approvedGrants,
300
+ grantSnapshot: probeResult.grantWalkSnapshot,
301
+ });
302
+ return {
303
+ ok: true,
304
+ definitionId,
305
+ approvedWireHash: recomputedWireHash,
306
+ approvedSurface: approvalSetFromItems(approvedGrants),
307
+ projection: probeResult.projection,
308
+ };
309
+ }
310
+ // Both asset arms carry an identical `source` field type, so narrow on the
311
+ // source's own `package.format` discriminant rather than adding a redundant
312
+ // discriminant to the args.
313
+ function isAssetSourceInstallArgs(args) {
314
+ return (args.source.kind === "asset" && args.source.package.format === "source");
315
+ }
316
+ function isAssetTarballInstallArgs(args) {
317
+ return (args.source.kind === "asset" && args.source.package.format === "tarball");
318
+ }
319
+ /**
320
+ * The install/approve orchestration entrypoint the end-to-end flow drives:
321
+ * resolve the frozen closure, probe the sidecar, then gate and freeze the
322
+ * result. This is production glue, not test-only wiring.
323
+ *
324
+ * The operator-approval decision is an input (`approvals`): the caller supplies
325
+ * either the `ApprovalSet` the operator approved, which the gate holds the
326
+ * advisory set to, or `approve-probed` to approve exactly the surface the probe
327
+ * reports.
328
+ *
329
+ * Returns the gate outcome alongside the inert projection and the frozen
330
+ * closure so the deploy hand-off consumes them verbatim rather than re-probing
331
+ * or re-resolving.
332
+ */
333
+ export async function installAndApproveWorkflowDefinition(args) {
334
+ let closure;
335
+ let assets;
336
+ if (isAssetSourceInstallArgs(args)) {
337
+ closure = await resolveWorkflowClosure({
338
+ source: args.source,
339
+ reads: args.reads,
340
+ registryName: args.registryName,
341
+ registryConfig: args.registryConfig,
342
+ ...(args.fetchPackument !== undefined
343
+ ? { fetchPackument: args.fetchPackument }
344
+ : {}),
345
+ });
346
+ assets = await buildSourceAssetMounts(closure, args.resolveAttachment);
347
+ }
348
+ else if (isAssetTarballInstallArgs(args)) {
349
+ closure = await resolveWorkflowClosure({
350
+ source: args.source,
351
+ pin: args.pin,
352
+ readBlob: args.readBlob,
353
+ listBlobs: args.listBlobs,
354
+ });
355
+ assets = await buildSourceAssetMounts(closure, args.resolveAttachment);
356
+ }
357
+ else {
358
+ closure = await resolveWorkflowClosure({
359
+ source: args.source,
360
+ pin: args.pin,
361
+ registryConfig: args.registryConfig,
362
+ ...(args.fetchPackument !== undefined
363
+ ? { fetchPackument: args.fetchPackument }
364
+ : {}),
365
+ });
366
+ assets = [];
367
+ }
368
+ const probeResult = await args.router.sendProbe({
369
+ source: args.source,
370
+ closure,
371
+ entry: args.entry,
372
+ ...(assets.length > 0 ? { assets } : {}),
373
+ });
374
+ await args.onProbeResult?.(probeResult);
375
+ const approval = await gateAndFreezeProbeResult({
376
+ assetId: args.assetId,
377
+ probeResult,
378
+ approvals: args.approvals,
379
+ persist: createDbFrozenApprovalWriter(args.db),
380
+ });
381
+ return { approval, projection: probeResult.projection, closure };
382
+ }