@intx/hub-sessions 0.1.2 → 0.3.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/LICENSE +176 -0
- package/README.md +82 -1
- package/dist/agent-repo.d.ts +93 -0
- package/dist/agent-repo.js +109 -0
- package/dist/agent-state-kind.d.ts +12 -0
- package/dist/agent-state-kind.js +189 -0
- package/dist/asset-service.d.ts +104 -0
- package/dist/asset-service.js +267 -0
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +33 -0
- package/dist/credential-push.js +109 -0
- package/dist/event-collector-registry.d.ts +20 -0
- package/dist/event-collector-registry.js +115 -0
- package/dist/event-collector.d.ts +39 -0
- package/dist/event-collector.js +365 -0
- package/dist/hub-session-lookups.d.ts +135 -0
- package/dist/hub-session-lookups.js +663 -0
- package/dist/hub-session-orchestrator.d.ts +25 -0
- package/dist/hub-session-orchestrator.js +87 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/package-registry-kind.d.ts +70 -0
- package/dist/package-registry-kind.js +260 -0
- package/dist/repo-store/index.d.ts +4 -0
- package/dist/repo-store/index.js +3 -0
- package/dist/repo-store/store.d.ts +41 -0
- package/dist/repo-store/store.js +1829 -0
- package/dist/repo-store/subscribe-kind.d.ts +56 -0
- package/dist/repo-store/subscribe-kind.js +144 -0
- package/dist/repo-store/types.d.ts +571 -0
- package/dist/repo-store/types.js +42 -0
- package/dist/session-service.d.ts +416 -0
- package/dist/session-service.js +1191 -0
- package/dist/sidecar-allocation/contracts.d.ts +78 -0
- package/dist/sidecar-allocation/contracts.js +21 -0
- package/dist/sidecar-allocation/index.d.ts +4 -0
- package/dist/sidecar-allocation/index.js +3 -0
- package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
- package/dist/sidecar-allocation/placement-policy.js +21 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
- package/dist/sidecar-allocation/plugin-registry.js +37 -0
- package/dist/sidecar-allocation/reconciler.d.ts +42 -0
- package/dist/sidecar-allocation/reconciler.js +431 -0
- package/dist/skill-kind.d.ts +41 -0
- package/dist/skill-kind.js +292 -0
- package/dist/substrate.d.ts +8 -0
- package/dist/substrate.js +21 -0
- package/dist/workflow-allocation-service.d.ts +58 -0
- package/dist/workflow-allocation-service.js +239 -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 +40 -0
- package/dist/workflow-dispatch-service.js +146 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +37 -0
- package/dist/workflow-kind.js +310 -0
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-event-log.d.ts +21 -0
- package/dist/workflow-run-event-log.js +51 -0
- package/dist/workflow-run-kind.d.ts +440 -0
- package/dist/workflow-run-kind.js +2916 -0
- package/dist/workflow-run-reader.d.ts +47 -0
- package/dist/workflow-run-reader.js +153 -0
- package/dist/workflow-run-restore.d.ts +15 -0
- package/dist/workflow-run-restore.js +26 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/ws/index.d.ts +3 -0
- package/dist/ws/index.js +3 -0
- package/dist/ws/sidecar-events.d.ts +222 -0
- package/dist/ws/sidecar-events.js +72 -0
- package/dist/ws/sidecar-handler.d.ts +305 -0
- package/dist/ws/sidecar-handler.js +2537 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +17 -0
- package/dist/ws/sidecar-token-authenticator.js +81 -0
- package/package.json +35 -12
- package/src/agent-repo.test.ts +0 -310
- package/src/agent-repo.ts +0 -165
- package/src/agent-state-kind.test.ts +0 -247
- package/src/agent-state-kind.ts +0 -204
- package/src/asset-service.test.ts +0 -540
- package/src/asset-service.ts +0 -378
- package/src/available-skills-stanza.test.ts +0 -87
- package/src/available-skills-stanza.ts +0 -47
- package/src/credential-push.ts +0 -65
- package/src/event-collector-registry.test.ts +0 -73
- package/src/event-collector-registry.ts +0 -171
- package/src/event-collector.test.ts +0 -1387
- package/src/event-collector.ts +0 -424
- package/src/hub-session-lookups.ts +0 -206
- package/src/hub-session-orchestrator.test.ts +0 -510
- package/src/hub-session-orchestrator.ts +0 -213
- package/src/index.ts +0 -78
- package/src/repo-store/index.ts +0 -15
- package/src/repo-store/store.test.ts +0 -1169
- package/src/repo-store/store.ts +0 -428
- package/src/repo-store/types.ts +0 -253
- package/src/session-service.test.ts +0 -895
- package/src/session-service.ts +0 -464
- package/src/skill-kind.test.ts +0 -599
- package/src/skill-kind.ts +0 -350
- package/src/ws/index.ts +0 -18
- package/src/ws/sidecar-events.test.ts +0 -96
- package/src/ws/sidecar-events.ts +0 -231
- package/src/ws/sidecar-handler.test.ts +0 -2217
- package/src/ws/sidecar-handler.ts +0 -1574
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { DBExecutor } from "@intx/db";
|
|
2
|
+
import type { GrantWalkSnapshot } from "@intx/types";
|
|
3
|
+
import type { PackumentFetcher, RegistryConfig } from "@intx/tool-packaging";
|
|
4
|
+
import type { WorkflowProjectionDefinition } from "@intx/types/sidecar";
|
|
5
|
+
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
6
|
+
import type { WorkflowDefinitionAssetSource, WorkflowDefinitionRegistrySource } from "@intx/types/workflow-sources";
|
|
7
|
+
import type { ApprovalSet } from "@intx/workflow-deploy";
|
|
8
|
+
import { type ResolveAssetAttachmentFn } from "./workflow-closure-resolution.js";
|
|
9
|
+
import type { SourceTreeReads } from "./workflow-source-closure.js";
|
|
10
|
+
import type { SidecarRouter, WorkflowProbeResult } from "./ws/sidecar-handler.js";
|
|
11
|
+
/**
|
|
12
|
+
* The frozen record an approval writes: the definition's asset selector, the
|
|
13
|
+
* approved wire hash (the freeze anchor), the approved grant set, and the
|
|
14
|
+
* grant-walk snapshot the run path materializes grants from. The grant set is a
|
|
15
|
+
* deterministic projection of the content the hash addresses and rides the
|
|
16
|
+
* deploy hand-off in memory; the snapshot is persisted onto the version row so a
|
|
17
|
+
* run derives its grants from the frozen walk without re-reading and re-walking
|
|
18
|
+
* the workflow's `workflow.json`.
|
|
19
|
+
*/
|
|
20
|
+
export type FrozenApproval = {
|
|
21
|
+
readonly assetId: string;
|
|
22
|
+
readonly approvedWireHash: string;
|
|
23
|
+
readonly approvedGrants: readonly string[];
|
|
24
|
+
readonly grantSnapshot: GrantWalkSnapshot;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Persists a frozen approval and returns the definition it was recorded
|
|
28
|
+
* against. Bound to a `DBExecutor` in production via
|
|
29
|
+
* `createDbFrozenApprovalWriter`; a test double records the call.
|
|
30
|
+
*/
|
|
31
|
+
export type PersistFrozenApprovalFn = (approval: FrozenApproval) => Promise<{
|
|
32
|
+
definitionId: string;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* The outcome of gating and freezing a probe result. `ok: true` is the frozen
|
|
36
|
+
* approval the deploy hand-off consumes. The `ok: false` arms name the two
|
|
37
|
+
* fail-closed paths: a shipped hash that does not match the hub recompute
|
|
38
|
+
* (tamper-evidence), and advisory grants the operator did not approve.
|
|
39
|
+
*/
|
|
40
|
+
export type ProbeGateResult = {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly definitionId: string;
|
|
43
|
+
readonly approvedWireHash: string;
|
|
44
|
+
readonly approvedGrants: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* The inert wire projection the freeze hashed. Rides the ok-arm so the
|
|
47
|
+
* deploy hand-off carries the exact content the frozen hash addresses,
|
|
48
|
+
* never a re-projection of a registry that may have moved since approval.
|
|
49
|
+
*/
|
|
50
|
+
readonly projection: WorkflowProjectionDefinition;
|
|
51
|
+
} | {
|
|
52
|
+
readonly ok: false;
|
|
53
|
+
readonly reason: "wire_hash_mismatch";
|
|
54
|
+
readonly shippedWireHash: string;
|
|
55
|
+
readonly recomputedWireHash: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly ok: false;
|
|
58
|
+
readonly reason: "grants_not_approved";
|
|
59
|
+
readonly unapprovedGrants: readonly string[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Build the production persistence step of the freeze. Records identity through
|
|
63
|
+
* the selector-keyed ensure helper (a definition keyed by `(assetId,
|
|
64
|
+
* wireHash)`) and writes the approved wire hash and the grant-walk snapshot onto
|
|
65
|
+
* that definition's version row in one transaction. The grant SET is not written
|
|
66
|
+
* to a version-row column -- the approved wire hash already pins the content the
|
|
67
|
+
* grants project from -- so it travels with the returned frozen approval; the
|
|
68
|
+
* snapshot is written because the run path reads it back to materialize grants.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createDbFrozenApprovalWriter(db: DBExecutor): PersistFrozenApprovalFn;
|
|
71
|
+
/**
|
|
72
|
+
* Approve exactly the grant surface the probe reports, without a pre-walked
|
|
73
|
+
* operator `ApprovalSet` to gate against. Under this mode the gate skips the
|
|
74
|
+
* per-grant membership check and freezes exactly what the probe advertised.
|
|
75
|
+
*
|
|
76
|
+
* This is the code-sourced analogue of the live-authored self-approve: the hub
|
|
77
|
+
* has no live definition to pre-walk, so the probe's advertised grants ARE the
|
|
78
|
+
* declared surface. It does NOT relax tamper-evidence -- the wire-hash check
|
|
79
|
+
* still runs and can still fail closed.
|
|
80
|
+
*/
|
|
81
|
+
export type ApproveProbedGrants = {
|
|
82
|
+
readonly mode: "approve-probed";
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* How the gate turns the probe's advisory grant set into an approved set.
|
|
86
|
+
* Either an explicit operator `ApprovalSet` -- every advertised grant must
|
|
87
|
+
* appear in it or the gate fails closed -- or `approve-probed`, which approves
|
|
88
|
+
* exactly the surface the probe reported.
|
|
89
|
+
*/
|
|
90
|
+
export type ProbeApprovalPolicy = ApprovalSet | ApproveProbedGrants;
|
|
91
|
+
export type GateAndFreezeArgs = {
|
|
92
|
+
/** The `workflow`-kind asset the frozen definition projects over. */
|
|
93
|
+
readonly assetId: string;
|
|
94
|
+
/** The sidecar's inert probe answer: projection, advisory grants, shipped hash. */
|
|
95
|
+
readonly probeResult: WorkflowProbeResult;
|
|
96
|
+
/**
|
|
97
|
+
* The approval policy. An `ApprovalSet` gates the advisory set against the
|
|
98
|
+
* operator-approved grant-shape strings; `approve-probed` approves exactly
|
|
99
|
+
* the surface the probe reported.
|
|
100
|
+
*/
|
|
101
|
+
readonly approvals: ProbeApprovalPolicy;
|
|
102
|
+
/** Persistence step for the freeze; `createDbFrozenApprovalWriter` in production. */
|
|
103
|
+
readonly persist: PersistFrozenApprovalFn;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Gate a probe result and, on approval, freeze it. Operates purely over the
|
|
107
|
+
* inert projection and grant set -- no author code runs here and the capability
|
|
108
|
+
* walk is never re-run.
|
|
109
|
+
*
|
|
110
|
+
* Fails closed on the two security-load-bearing checks before it writes
|
|
111
|
+
* anything: the recomputed wire hash must match the hash the sidecar shipped
|
|
112
|
+
* (tamper-evidence), and every advisory grant must be operator-approved. Only
|
|
113
|
+
* then does it freeze the recomputed hash onto the version row and return the
|
|
114
|
+
* approved grant set.
|
|
115
|
+
*/
|
|
116
|
+
export declare function gateAndFreezeProbeResult(args: GateAndFreezeArgs): Promise<ProbeGateResult>;
|
|
117
|
+
type InstallAndApproveCommonArgs = {
|
|
118
|
+
/** The `interchange.workflow` entry-module path the sidecar evaluates to project the definition. */
|
|
119
|
+
readonly entry: string;
|
|
120
|
+
/** The `workflow`-kind asset the frozen definition projects over. */
|
|
121
|
+
readonly assetId: string;
|
|
122
|
+
/**
|
|
123
|
+
* The approval policy threaded to the gate: an operator `ApprovalSet` to gate
|
|
124
|
+
* the advisory set against, or `approve-probed` to approve exactly the
|
|
125
|
+
* surface the probe reports.
|
|
126
|
+
*/
|
|
127
|
+
readonly approvals: ProbeApprovalPolicy;
|
|
128
|
+
/** The sidecar router carrying the probe transport. */
|
|
129
|
+
readonly router: Pick<SidecarRouter, "sendProbe">;
|
|
130
|
+
/** Executor the freeze writes through. */
|
|
131
|
+
readonly db: DBExecutor;
|
|
132
|
+
};
|
|
133
|
+
/** Install a definition published to an npm registry. */
|
|
134
|
+
export type InstallAndApproveRegistryArgs = InstallAndApproveCommonArgs & {
|
|
135
|
+
readonly source: WorkflowDefinitionRegistrySource;
|
|
136
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
137
|
+
readonly pin: string;
|
|
138
|
+
/** URL and credentials for the registry `source` names. */
|
|
139
|
+
readonly registryConfig: RegistryConfig;
|
|
140
|
+
/** Test seam for packument fetches, threaded to closure resolution. Omitted in production. */
|
|
141
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Install a definition published as a tarball inside a hub `package-registry`
|
|
145
|
+
* asset. The caller mints the asset-read closures (`readBlob`/`listBlobs`) and
|
|
146
|
+
* `resolveAttachment`; this glue never imports the asset service, so hub-service
|
|
147
|
+
* ownership stays at the caller.
|
|
148
|
+
*/
|
|
149
|
+
export type InstallAndApproveAssetTarballArgs = InstallAndApproveCommonArgs & {
|
|
150
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
151
|
+
/** A `name@range` spec for the workflow definition package. */
|
|
152
|
+
readonly pin: string;
|
|
153
|
+
/** Reads a blob at `path` from the asset the definition is sourced from. */
|
|
154
|
+
readonly readBlob: (path: string) => Promise<Uint8Array>;
|
|
155
|
+
/** Lists the blob names directly under `dir` in that asset. */
|
|
156
|
+
readonly listBlobs: (dir: string) => Promise<string[]>;
|
|
157
|
+
/** Resolves each asset the closure references to the pack the probe delivers. */
|
|
158
|
+
readonly resolveAttachment: ResolveAssetAttachmentFn;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Install a definition whose package lives as a git subtree of a hub asset at a
|
|
162
|
+
* pinned commit. The caller binds a `SourceTreeReads` to that commit and
|
|
163
|
+
* supplies the npm `registryConfig` for the external deps; there is no
|
|
164
|
+
* `name@range` pin (the member is selected from `source.package.packageName`).
|
|
165
|
+
* `resolveAttachment` delivers the same git pack the tarball arm does, so the
|
|
166
|
+
* sidecar checks the subtree out of it.
|
|
167
|
+
*/
|
|
168
|
+
export type InstallAndApproveAssetSourceArgs = InstallAndApproveCommonArgs & {
|
|
169
|
+
readonly source: WorkflowDefinitionAssetSource;
|
|
170
|
+
/** Git-tree reads pinned to `source.package.commitSha`. */
|
|
171
|
+
readonly reads: SourceTreeReads;
|
|
172
|
+
/**
|
|
173
|
+
* The registry name external deps are stamped with in the frozen closure.
|
|
174
|
+
* Must be a name the sidecar's registry map is keyed by (its npm registry).
|
|
175
|
+
*/
|
|
176
|
+
readonly registryName: string;
|
|
177
|
+
/** URL and credentials for the npm registry external deps resolve against. */
|
|
178
|
+
readonly registryConfig: RegistryConfig;
|
|
179
|
+
/** Test seam for packument fetches, threaded to closure resolution. Omitted in production. */
|
|
180
|
+
readonly fetchPackument?: PackumentFetcher;
|
|
181
|
+
/** Resolves each asset the closure references to the pack the probe delivers. */
|
|
182
|
+
readonly resolveAttachment: ResolveAssetAttachmentFn;
|
|
183
|
+
};
|
|
184
|
+
export type InstallAndApproveArgs = InstallAndApproveRegistryArgs | InstallAndApproveAssetTarballArgs | InstallAndApproveAssetSourceArgs;
|
|
185
|
+
/**
|
|
186
|
+
* The frozen hand-off `installAndApproveWorkflowDefinition` produces. It carries
|
|
187
|
+
* the gate outcome plus the two values the source-ref deploy frame needs and
|
|
188
|
+
* must NOT recompute at deploy: the inert `projection` the freeze hashed and the
|
|
189
|
+
* frozen dependency `closure` the pin resolved to. Re-resolving either at deploy
|
|
190
|
+
* would reintroduce the non-determinism the freeze eliminates -- a registry that
|
|
191
|
+
* moved between approve and deploy would pin different bytes and project
|
|
192
|
+
* differently, failing the child re-verify -- so both ride from approve verbatim.
|
|
193
|
+
*/
|
|
194
|
+
export type InstallAndApproveResult = {
|
|
195
|
+
readonly approval: ProbeGateResult;
|
|
196
|
+
readonly projection: WorkflowProjectionDefinition;
|
|
197
|
+
readonly closure: ToolPackageManifest;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The install/approve orchestration entrypoint the end-to-end flow drives:
|
|
201
|
+
* resolve the frozen closure, probe the sidecar, then gate and freeze the
|
|
202
|
+
* result. This is production glue, not test-only wiring.
|
|
203
|
+
*
|
|
204
|
+
* The operator-approval decision is an input (`approvals`): the caller supplies
|
|
205
|
+
* either the `ApprovalSet` the operator approved, which the gate holds the
|
|
206
|
+
* advisory set to, or `approve-probed` to approve exactly the surface the probe
|
|
207
|
+
* reports.
|
|
208
|
+
*
|
|
209
|
+
* Returns the gate outcome alongside the inert projection and the frozen
|
|
210
|
+
* closure so the deploy hand-off consumes them verbatim rather than re-probing
|
|
211
|
+
* or re-resolving.
|
|
212
|
+
*/
|
|
213
|
+
export declare function installAndApproveWorkflowDefinition(args: InstallAndApproveArgs): Promise<InstallAndApproveResult>;
|
|
214
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
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. Gate the advisory grant set against the approval policy: an operator
|
|
16
|
+
// `ApprovalSet` requires every grant the probe surfaced to be approved or
|
|
17
|
+
// the gate fails, while `approve-probed` approves exactly what the probe
|
|
18
|
+
// surfaced.
|
|
19
|
+
// 5. Freeze the approved wire hash onto the definition version row, keyed by
|
|
20
|
+
// the definition's selector, and return the frozen approved grant set.
|
|
21
|
+
//
|
|
22
|
+
// The frozen approved set is the single source of truth for the definition's
|
|
23
|
+
// grants: the deploy path materializes deploy grants as a SUBSET of it (never a
|
|
24
|
+
// fresh walk), so a workflow can never acquire at deploy or run time a grant it
|
|
25
|
+
// did not have frozen at approval. The wire hash is that freeze's anchor -- the
|
|
26
|
+
// grant set is a deterministic projection of the exact content the hash
|
|
27
|
+
// addresses, so pinning the hash pins the set.
|
|
28
|
+
import { and, eq } from "drizzle-orm";
|
|
29
|
+
import { workflowDefinitionVersion } from "@intx/db/schema";
|
|
30
|
+
import { computeWireDefinitionHash } from "@intx/types/wire-definition-hash";
|
|
31
|
+
import { buildSourceAssetMounts, resolveWorkflowClosure, } from "./workflow-closure-resolution.js";
|
|
32
|
+
import { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
|
|
33
|
+
// The version `ensureWorkflowDefinitionForAsset` projects for a fresh
|
|
34
|
+
// definition, and therefore the row the approval freeze targets. Kept in step
|
|
35
|
+
// with the ensure helper: if that helper ever projects a different initial
|
|
36
|
+
// version, this must follow.
|
|
37
|
+
const FROZEN_VERSION = "1";
|
|
38
|
+
/**
|
|
39
|
+
* Build the production persistence step of the freeze. Records identity through
|
|
40
|
+
* the selector-keyed ensure helper (a definition keyed by `(assetId,
|
|
41
|
+
* wireHash)`) and writes the approved wire hash and the grant-walk snapshot onto
|
|
42
|
+
* that definition's version row in one transaction. The grant SET is not written
|
|
43
|
+
* to a version-row column -- the approved wire hash already pins the content the
|
|
44
|
+
* grants project from -- so it travels with the returned frozen approval; the
|
|
45
|
+
* snapshot is written because the run path reads it back to materialize grants.
|
|
46
|
+
*/
|
|
47
|
+
export function createDbFrozenApprovalWriter(db) {
|
|
48
|
+
return async ({ assetId, approvedWireHash, grantSnapshot }) => {
|
|
49
|
+
// Ensure-then-stamp is one freeze: a crash between the two would persist a
|
|
50
|
+
// version row with a NULL `approvedWireHash`, which the schema treats as
|
|
51
|
+
// the legitimate "not yet approved" state -- indistinguishable from an
|
|
52
|
+
// un-approved definition. Wrap both writes in one transaction so the freeze
|
|
53
|
+
// is all-or-nothing.
|
|
54
|
+
return db.transaction(async (tx) => {
|
|
55
|
+
const { definitionId } = await ensureWorkflowDefinitionForAsset(tx, {
|
|
56
|
+
assetId,
|
|
57
|
+
wireHash: approvedWireHash,
|
|
58
|
+
});
|
|
59
|
+
// `FROZEN_VERSION` is hand-coupled to the version the ensure helper
|
|
60
|
+
// projects; if that coupling ever drifts, the update would silently stamp
|
|
61
|
+
// zero rows and no hash would persist. Assert exactly one row so a drift
|
|
62
|
+
// fails loud instead of open.
|
|
63
|
+
const stamped = await tx
|
|
64
|
+
.update(workflowDefinitionVersion)
|
|
65
|
+
.set({ approvedWireHash, grantSnapshot })
|
|
66
|
+
.where(and(eq(workflowDefinitionVersion.definitionId, definitionId), eq(workflowDefinitionVersion.version, FROZEN_VERSION)))
|
|
67
|
+
.returning({ id: workflowDefinitionVersion.id });
|
|
68
|
+
if (stamped.length !== 1) {
|
|
69
|
+
throw new Error(`createDbFrozenApprovalWriter: expected to stamp exactly one ${FROZEN_VERSION} version row for definition ${definitionId}, but updated ${String(stamped.length)}`);
|
|
70
|
+
}
|
|
71
|
+
return { definitionId };
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isApproveProbed(policy) {
|
|
76
|
+
return "mode" in policy;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Gate a probe result and, on approval, freeze it. Operates purely over the
|
|
80
|
+
* inert projection and grant set -- no author code runs here and the capability
|
|
81
|
+
* walk is never re-run.
|
|
82
|
+
*
|
|
83
|
+
* Fails closed on the two security-load-bearing checks before it writes
|
|
84
|
+
* anything: the recomputed wire hash must match the hash the sidecar shipped
|
|
85
|
+
* (tamper-evidence), and every advisory grant must be operator-approved. Only
|
|
86
|
+
* then does it freeze the recomputed hash onto the version row and return the
|
|
87
|
+
* approved grant set.
|
|
88
|
+
*/
|
|
89
|
+
export async function gateAndFreezeProbeResult(args) {
|
|
90
|
+
const { assetId, probeResult, approvals, persist } = args;
|
|
91
|
+
// Tamper-evidence: recompute over the RECEIVED projection and compare to the
|
|
92
|
+
// shipped hash. A mismatch means the projection the hub is approving is not
|
|
93
|
+
// the one the sidecar hashed, so reject rather than freeze a hash that does
|
|
94
|
+
// not describe the approved content.
|
|
95
|
+
const recomputedWireHash = await computeWireDefinitionHash(probeResult.projection);
|
|
96
|
+
if (recomputedWireHash !== probeResult.wireHash) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: "wire_hash_mismatch",
|
|
100
|
+
shippedWireHash: probeResult.wireHash,
|
|
101
|
+
recomputedWireHash,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Gate the advisory grant set. Under an `ApprovalSet` every grant the probe
|
|
105
|
+
// surfaced must appear in the operator's approved set; any miss fails the
|
|
106
|
+
// gate closed. Under `approve-probed` there is no set to gate against -- the
|
|
107
|
+
// probe's surface IS the approved set -- so nothing is ever unapproved.
|
|
108
|
+
const unapprovedGrants = isApproveProbed(approvals)
|
|
109
|
+
? []
|
|
110
|
+
: probeResult.grants.filter((grant) => !approvals.has(grant));
|
|
111
|
+
if (unapprovedGrants.length > 0) {
|
|
112
|
+
return { ok: false, reason: "grants_not_approved", unapprovedGrants };
|
|
113
|
+
}
|
|
114
|
+
// Freeze: the approved set is exactly what the workflow advertised (all of it
|
|
115
|
+
// now operator-approved), pinned to the recomputed hash. Persisting the hash
|
|
116
|
+
// is the freeze; the grant set is returned for the deploy hand-off.
|
|
117
|
+
const approvedGrants = [...probeResult.grants];
|
|
118
|
+
const { definitionId } = await persist({
|
|
119
|
+
assetId,
|
|
120
|
+
approvedWireHash: recomputedWireHash,
|
|
121
|
+
approvedGrants,
|
|
122
|
+
grantSnapshot: probeResult.grantWalkSnapshot,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
definitionId,
|
|
127
|
+
approvedWireHash: recomputedWireHash,
|
|
128
|
+
approvedGrants: new Set(approvedGrants),
|
|
129
|
+
projection: probeResult.projection,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Both asset arms carry an identical `source` field type, so narrow on the
|
|
133
|
+
// source's own `package.format` discriminant rather than adding a redundant
|
|
134
|
+
// discriminant to the args.
|
|
135
|
+
function isAssetSourceInstallArgs(args) {
|
|
136
|
+
return (args.source.kind === "asset" && args.source.package.format === "source");
|
|
137
|
+
}
|
|
138
|
+
function isAssetTarballInstallArgs(args) {
|
|
139
|
+
return (args.source.kind === "asset" && args.source.package.format === "tarball");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The install/approve orchestration entrypoint the end-to-end flow drives:
|
|
143
|
+
* resolve the frozen closure, probe the sidecar, then gate and freeze the
|
|
144
|
+
* result. This is production glue, not test-only wiring.
|
|
145
|
+
*
|
|
146
|
+
* The operator-approval decision is an input (`approvals`): the caller supplies
|
|
147
|
+
* either the `ApprovalSet` the operator approved, which the gate holds the
|
|
148
|
+
* advisory set to, or `approve-probed` to approve exactly the surface the probe
|
|
149
|
+
* reports.
|
|
150
|
+
*
|
|
151
|
+
* Returns the gate outcome alongside the inert projection and the frozen
|
|
152
|
+
* closure so the deploy hand-off consumes them verbatim rather than re-probing
|
|
153
|
+
* or re-resolving.
|
|
154
|
+
*/
|
|
155
|
+
export async function installAndApproveWorkflowDefinition(args) {
|
|
156
|
+
let closure;
|
|
157
|
+
let assets;
|
|
158
|
+
if (isAssetSourceInstallArgs(args)) {
|
|
159
|
+
closure = await resolveWorkflowClosure({
|
|
160
|
+
source: args.source,
|
|
161
|
+
reads: args.reads,
|
|
162
|
+
registryName: args.registryName,
|
|
163
|
+
registryConfig: args.registryConfig,
|
|
164
|
+
...(args.fetchPackument !== undefined
|
|
165
|
+
? { fetchPackument: args.fetchPackument }
|
|
166
|
+
: {}),
|
|
167
|
+
});
|
|
168
|
+
assets = await buildSourceAssetMounts(closure, args.resolveAttachment);
|
|
169
|
+
}
|
|
170
|
+
else if (isAssetTarballInstallArgs(args)) {
|
|
171
|
+
closure = await resolveWorkflowClosure({
|
|
172
|
+
source: args.source,
|
|
173
|
+
pin: args.pin,
|
|
174
|
+
readBlob: args.readBlob,
|
|
175
|
+
listBlobs: args.listBlobs,
|
|
176
|
+
});
|
|
177
|
+
assets = await buildSourceAssetMounts(closure, args.resolveAttachment);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
closure = await resolveWorkflowClosure({
|
|
181
|
+
source: args.source,
|
|
182
|
+
pin: args.pin,
|
|
183
|
+
registryConfig: args.registryConfig,
|
|
184
|
+
...(args.fetchPackument !== undefined
|
|
185
|
+
? { fetchPackument: args.fetchPackument }
|
|
186
|
+
: {}),
|
|
187
|
+
});
|
|
188
|
+
assets = [];
|
|
189
|
+
}
|
|
190
|
+
const { sendProbe } = args.router;
|
|
191
|
+
if (sendProbe === undefined) {
|
|
192
|
+
throw new Error("installAndApproveWorkflowDefinition: router does not support sendProbe");
|
|
193
|
+
}
|
|
194
|
+
const probeResult = await sendProbe({
|
|
195
|
+
source: args.source,
|
|
196
|
+
closure,
|
|
197
|
+
entry: args.entry,
|
|
198
|
+
...(assets.length > 0 ? { assets } : {}),
|
|
199
|
+
});
|
|
200
|
+
const approval = await gateAndFreezeProbeResult({
|
|
201
|
+
assetId: args.assetId,
|
|
202
|
+
probeResult,
|
|
203
|
+
approvals: args.approvals,
|
|
204
|
+
persist: createDbFrozenApprovalWriter(args.db),
|
|
205
|
+
});
|
|
206
|
+
return { approval, projection: probeResult.projection, closure };
|
|
207
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Filename of a run's combined event log, a sibling of its `events/` dir. */
|
|
2
|
+
export declare const WORKFLOW_RUN_EVENTS_FILE = "events.jsonl";
|
|
3
|
+
/**
|
|
4
|
+
* Split a combined event-log file's content into the per-event JSON texts it
|
|
5
|
+
* holds, in file order. Each non-empty line is the verbatim text of what was
|
|
6
|
+
* an `events/<seq>.json` blob; the trailing newline yields no extra entry.
|
|
7
|
+
* Event JSON never contains a literal newline (JSON escapes them), so a line
|
|
8
|
+
* split is a faithful inverse of the encode side.
|
|
9
|
+
*/
|
|
10
|
+
export declare function splitCombinedEventLog(content: string): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Join per-event blobs (in seq order) into a combined event-log file: each
|
|
13
|
+
* blob's exact bytes followed by a newline. Operating on bytes -- not
|
|
14
|
+
* decoded strings -- keeps the sealed file a *verbatim* concatenation of
|
|
15
|
+
* the per-event blobs, which matters because each event is signed over its
|
|
16
|
+
* own bytes; a decode/re-encode round-trip could alter them. This is the
|
|
17
|
+
* single source of the combined-file byte layout shared by the compaction
|
|
18
|
+
* writer and the validator's byte-equality bridge, so the two cannot
|
|
19
|
+
* drift. An empty input yields an empty file.
|
|
20
|
+
*/
|
|
21
|
+
export declare function encodeCombinedEventLog(perEventBlobs: readonly Uint8Array[]): Uint8Array;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The sealed-form event log for a terminated run. Compaction folds a run's
|
|
2
|
+
// per-event `runs/<runId>/events/<seq>.json` blobs into a single combined
|
|
3
|
+
// file, `runs/<runId>/events.jsonl`, once the run reaches a terminal event.
|
|
4
|
+
// Each line of the combined file is the verbatim text of the per-event blob
|
|
5
|
+
// it replaced, in seq order, so the fold is a byte-for-byte transition that
|
|
6
|
+
// the workflow-run kind handler can validate against the prior per-event
|
|
7
|
+
// tree. Readers handle both shapes: per-event files for in-flight runs, the
|
|
8
|
+
// combined file for sealed (terminal) runs.
|
|
9
|
+
/** Filename of a run's combined event log, a sibling of its `events/` dir. */
|
|
10
|
+
export const WORKFLOW_RUN_EVENTS_FILE = "events.jsonl";
|
|
11
|
+
/**
|
|
12
|
+
* Split a combined event-log file's content into the per-event JSON texts it
|
|
13
|
+
* holds, in file order. Each non-empty line is the verbatim text of what was
|
|
14
|
+
* an `events/<seq>.json` blob; the trailing newline yields no extra entry.
|
|
15
|
+
* Event JSON never contains a literal newline (JSON escapes them), so a line
|
|
16
|
+
* split is a faithful inverse of the encode side.
|
|
17
|
+
*/
|
|
18
|
+
export function splitCombinedEventLog(content) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const line of content.split("\n")) {
|
|
21
|
+
if (line.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
out.push(line);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Join per-event blobs (in seq order) into a combined event-log file: each
|
|
29
|
+
* blob's exact bytes followed by a newline. Operating on bytes -- not
|
|
30
|
+
* decoded strings -- keeps the sealed file a *verbatim* concatenation of
|
|
31
|
+
* the per-event blobs, which matters because each event is signed over its
|
|
32
|
+
* own bytes; a decode/re-encode round-trip could alter them. This is the
|
|
33
|
+
* single source of the combined-file byte layout shared by the compaction
|
|
34
|
+
* writer and the validator's byte-equality bridge, so the two cannot
|
|
35
|
+
* drift. An empty input yields an empty file.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeCombinedEventLog(perEventBlobs) {
|
|
38
|
+
const NEWLINE = 0x0a;
|
|
39
|
+
let total = 0;
|
|
40
|
+
for (const blob of perEventBlobs)
|
|
41
|
+
total += blob.byteLength + 1;
|
|
42
|
+
const out = new Uint8Array(total);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
for (const blob of perEventBlobs) {
|
|
45
|
+
out.set(blob, offset);
|
|
46
|
+
offset += blob.byteLength;
|
|
47
|
+
out[offset] = NEWLINE;
|
|
48
|
+
offset += 1;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|