@intx/hub-sessions 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -5
- package/dist/agent-repo.d.ts +23 -7
- package/dist/agent-repo.js +19 -6
- package/dist/agent-state-kind.js +18 -63
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +19 -97
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +52 -7
- package/dist/credential-push.js +178 -22
- package/dist/event-collector-registry.d.ts +3 -2
- package/dist/event-collector-registry.js +42 -13
- package/dist/event-collector.d.ts +12 -2
- package/dist/event-collector.js +45 -4
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +541 -86
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +22 -56
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +2 -1
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +252 -126
- package/dist/session-service.js +851 -624
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +101 -0
- package/dist/sidecar-allocation/contracts.js +26 -0
- package/dist/sidecar-allocation/index.d.ts +5 -0
- package/dist/sidecar-allocation/index.js +4 -0
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
- package/dist/sidecar-allocation/plugin-registry.js +61 -0
- package/dist/sidecar-allocation/reconciler.d.ts +54 -0
- package/dist/sidecar-allocation/reconciler.js +825 -0
- package/dist/skill-kind.js +12 -62
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +64 -0
- package/dist/workflow-allocation-service.js +554 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +42 -0
- package/dist/workflow-dispatch-service.js +209 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +29 -1
- package/dist/workflow-kind.js +143 -139
- package/dist/workflow-probe-gate.d.ts +286 -0
- package/dist/workflow-probe-gate.js +382 -0
- package/dist/workflow-run-kind.d.ts +239 -32
- package/dist/workflow-run-kind.js +874 -188
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +16 -0
- package/dist/workflow-run-restore.js +30 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +2 -2
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +139 -37
- package/dist/ws/sidecar-events.js +2 -2
- package/dist/ws/sidecar-handler.d.ts +218 -60
- package/dist/ws/sidecar-handler.js +1713 -732
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +78 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
|
@@ -0,0 +1,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
|
+
}
|