@intx/hub-sessions 0.2.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/README.md +3 -5
- package/dist/agent-repo.d.ts +9 -5
- package/dist/agent-repo.js +2 -2
- package/dist/agent-state-kind.js +4 -0
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +9 -91
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +7 -6
- package/dist/credential-push.js +42 -18
- package/dist/event-collector-registry.d.ts +1 -1
- package/dist/event-collector-registry.js +4 -4
- package/dist/event-collector.d.ts +1 -1
- package/dist/event-collector.js +10 -2
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +539 -80
- package/dist/hub-session-orchestrator.js +14 -49
- package/dist/index.d.ts +17 -8
- package/dist/index.js +14 -6
- package/dist/repo-store/index.d.ts +1 -1
- 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/session-service.d.ts +277 -96
- package/dist/session-service.js +741 -547
- 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.js +4 -0
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- 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 +17 -1
- package/dist/workflow-kind.js +127 -80
- package/dist/workflow-probe-gate.d.ts +214 -0
- package/dist/workflow-probe-gate.js +207 -0
- package/dist/workflow-run-kind.d.ts +128 -14
- package/dist/workflow-run-kind.js +353 -83
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- 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 -3
- package/dist/ws/index.js +1 -1
- package/dist/ws/sidecar-events.d.ts +100 -12
- package/dist/ws/sidecar-events.js +2 -0
- package/dist/ws/sidecar-handler.d.ts +128 -7
- package/dist/ws/sidecar-handler.js +1069 -135
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +64 -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,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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AuthorizeFn, type KindHandler, type Principal, type RepoId, type RepoStore } from "./repo-store/index.js";
|
|
1
|
+
import { type AuthorizeFn, type CommittedReads, type KindHandler, type Principal, type RepoId, type RepoStore } from "./repo-store/index.js";
|
|
2
2
|
export type WorkflowRunHubPrincipal = {
|
|
3
3
|
readonly kind: "hub";
|
|
4
4
|
};
|
|
@@ -8,18 +8,19 @@ export type WorkflowRunSidecarPrincipal = {
|
|
|
8
8
|
};
|
|
9
9
|
export type WorkflowRunWorkflowProcessPrincipal = {
|
|
10
10
|
readonly kind: "workflow-process";
|
|
11
|
-
readonly
|
|
11
|
+
readonly anchorRunId: string;
|
|
12
12
|
readonly runId?: string;
|
|
13
13
|
};
|
|
14
14
|
export type WorkflowRunSupervisorPrincipal = {
|
|
15
15
|
readonly kind: "supervisor";
|
|
16
|
-
readonly
|
|
16
|
+
readonly anchorRunId: string;
|
|
17
17
|
};
|
|
18
18
|
export type WorkflowRunPrincipal = WorkflowRunHubPrincipal | WorkflowRunSidecarPrincipal | WorkflowRunWorkflowProcessPrincipal | WorkflowRunSupervisorPrincipal;
|
|
19
19
|
export declare const WORKFLOW_RUN_GITIGNORE_PATH = ".gitignore";
|
|
20
20
|
export declare const WORKFLOW_RUN_RUNS_PREFIX = "runs";
|
|
21
21
|
export declare const WORKFLOW_RUN_EVENTS_DIR = "events";
|
|
22
22
|
export declare const WORKFLOW_RUN_BLOBS_DIR = "blobs";
|
|
23
|
+
export declare const WORKFLOW_RUN_GRANTS_FILE = "grants.json";
|
|
23
24
|
export declare const WORKFLOW_RUN_ADDRESSES_PREFIX = "addresses";
|
|
24
25
|
export declare const WORKFLOW_RUN_CONTROL_PREFIX = "control";
|
|
25
26
|
export declare const WORKFLOW_RUN_INBOX_DIR = "inbox";
|
|
@@ -42,15 +43,28 @@ export declare const WORKFLOW_RUN_WATERMARK_FILE = "watermark.json";
|
|
|
42
43
|
* deduped by a retained consumed entry, short enough that `consumed/`
|
|
43
44
|
* reaches a bounded steady state of one day's message volume.
|
|
44
45
|
*
|
|
45
|
-
* INVARIANT (operator-owned): the horizon must be >= the longest
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* re-
|
|
53
|
-
*
|
|
46
|
+
* INVARIANT (operator-owned): the horizon must be >= the longest window in
|
|
47
|
+
* which the same `messageId` could legitimately be re-submitted and still must
|
|
48
|
+
* be caught as a duplicate. The hub now redelivers un-acked inbound mail
|
|
49
|
+
* (connected-window retry and reconnect-redelivery), so an at-least-once
|
|
50
|
+
* internal source DOES exist -- but the dedup guarantee against it does not
|
|
51
|
+
* rest on window arithmetic. It rests on a STRUCTURAL fact: `enqueueInbox` is
|
|
52
|
+
* only ever called with a freshly stamped `receivedAt` (a redelivery
|
|
53
|
+
* re-enters `onMailMessage` and re-stamps `Date.now()`, never carrying the
|
|
54
|
+
* original), and the watermark only ever advances to at most
|
|
55
|
+
* `consumedAt - retentionHorizonMs`, which is <= now, so a fresh `receivedAt`
|
|
56
|
+
* always sits a full horizon above the watermark and can never be stale-
|
|
57
|
+
* refused. A redelivery instead hits the `consumed/`/`processing/`/`inbox/`
|
|
58
|
+
* dedup index and is deduped there. The sole path that carries an original
|
|
59
|
+
* (old) `receivedAt` back into the queue is `replayProcessingToInbox`, which
|
|
60
|
+
* writes straight to `inbox/` and bypasses the stale gate entirely. So
|
|
61
|
+
* `claim_check_stale_enqueue` is unreachable via redelivery today. If any
|
|
62
|
+
* redelivery source is ever changed to carry the ORIGINAL `receivedAt` into
|
|
63
|
+
* `enqueueInbox`, stale becomes reachable, the horizon must then be >= that
|
|
64
|
+
* source's maximum redelivery window, and `StaleInboxEnqueueError`'s
|
|
65
|
+
* withhold-not-ack handling becomes load-bearing; a breach surfaces LOUDLY (an
|
|
66
|
+
* old-`receivedAt` re-submission is refused at enqueue) rather than as silent
|
|
67
|
+
* double-processing.
|
|
54
68
|
*/
|
|
55
69
|
export declare const DEFAULT_CONSUMED_RETENTION_MS: number;
|
|
56
70
|
/**
|
|
@@ -68,6 +82,28 @@ export declare const DEFAULT_CONSUMED_RETENTION_MS: number;
|
|
|
68
82
|
* `<agentKey>` directory layer below the prefix).
|
|
69
83
|
*/
|
|
70
84
|
export declare const WORKFLOW_RUN_AGENT_STATE_PREFIX = "agent-state";
|
|
85
|
+
/**
|
|
86
|
+
* Parse the seq from a per-event log filename `<seq>.json` under
|
|
87
|
+
* `runs/<runId>/events/`. Returns the non-negative integer seq, or
|
|
88
|
+
* `null` when the name is not a legal per-event filename. This is the
|
|
89
|
+
* one place the filename shape is defined; every reader of the event log
|
|
90
|
+
* narrows names through it rather than re-encoding the regex. Callers
|
|
91
|
+
* decide what an illegal name means -- a foreign entry to skip, or a
|
|
92
|
+
* substrate-invariant violation to surface -- since `validatePush` is
|
|
93
|
+
* the authority that keeps illegal names from landing in the first place.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseEventSeq(filename: string): number | null;
|
|
96
|
+
/**
|
|
97
|
+
* Narrow a per-event filename to its seq, throwing when it is illegal.
|
|
98
|
+
* A reader that enumerates the committed event log to act on its entries
|
|
99
|
+
* uses this rather than `parseEventSeq`: `validatePush` is the authority
|
|
100
|
+
* that keeps an illegal name from ever landing under
|
|
101
|
+
* `runs/<runId>/events/`, so a name that reaches a reader is corruption,
|
|
102
|
+
* and silently skipping it would drop an event from processing. `context`
|
|
103
|
+
* is the repo-root-relative blob path, surfaced in the error so the
|
|
104
|
+
* offending entry is identifiable.
|
|
105
|
+
*/
|
|
106
|
+
export declare function requireEventSeq(filename: string, context: string): number;
|
|
71
107
|
/**
|
|
72
108
|
* JSON envelope carried by inbox and processing entries. Keys:
|
|
73
109
|
* - `messageId`: dedup key for the inbound message.
|
|
@@ -115,6 +151,10 @@ declare const ConsumedEnvelope: import("arktype/internal/variants/object.ts").Ob
|
|
|
115
151
|
store: string;
|
|
116
152
|
path: string;
|
|
117
153
|
};
|
|
154
|
+
rejection?: {
|
|
155
|
+
code: string;
|
|
156
|
+
message: string;
|
|
157
|
+
};
|
|
118
158
|
}, {}>;
|
|
119
159
|
/**
|
|
120
160
|
* JSON envelope carried by the per-address `watermark.json` blob. The
|
|
@@ -153,6 +193,54 @@ export type EnqueueInboxResult = {
|
|
|
153
193
|
inboxKey: string;
|
|
154
194
|
envelope: ClaimCheckEnvelope;
|
|
155
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* Which already-present state an `enqueueInbox` call found the messageId
|
|
198
|
+
* in. Every value is POSITIVE evidence the message's bytes are durably on
|
|
199
|
+
* disk (inbox/processing) or were already consumed -- so a caller gating a
|
|
200
|
+
* receipt on the enqueue may safely acknowledge on any of them.
|
|
201
|
+
*/
|
|
202
|
+
export type EnqueueAlreadyPresentReason = "duplicate" | "already_inbox" | "processing" | "consumed";
|
|
203
|
+
/**
|
|
204
|
+
* Outcome of an `enqueueInbox` call. Modeled as a value (not an exception)
|
|
205
|
+
* precisely because the return/throw boundary is the ack/withhold boundary
|
|
206
|
+
* for a caller gating a durable-receipt ack: a returned outcome is safe to
|
|
207
|
+
* acknowledge (the bytes are on disk -- freshly written or already present),
|
|
208
|
+
* a throw is not (the write could not complete or its disposition cannot be
|
|
209
|
+
* decided). `enqueued` is the only outcome that added a new inbox entry, so
|
|
210
|
+
* it is the only one a dispatch-driving caller wakes its loop on.
|
|
211
|
+
*/
|
|
212
|
+
export type EnqueueInboxOutcome = ({
|
|
213
|
+
outcome: "enqueued";
|
|
214
|
+
} & EnqueueInboxResult) | {
|
|
215
|
+
outcome: "already-present";
|
|
216
|
+
reason: EnqueueAlreadyPresentReason;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Thrown by `enqueueInbox` when the inbound's `receivedAt` is strictly below
|
|
220
|
+
* the address's retention watermark. This is refusal under UNCERTAINTY, not
|
|
221
|
+
* proof of prior receipt: the consumed dedup entry that would rule out a
|
|
222
|
+
* duplicate may have been pruned, so the substrate can no longer tell a
|
|
223
|
+
* duplicate from a never-processed message and refuses rather than risk
|
|
224
|
+
* reprocessing. A caller gating a durable-receipt ack MUST NOT acknowledge on
|
|
225
|
+
* this -- acking an "I cannot tell" would terminally drop a message that was
|
|
226
|
+
* never written. It is its own type (not a generic Error) so that a caller,
|
|
227
|
+
* and monitoring, can surface it as a distinct loud signal rather than
|
|
228
|
+
* blending it into ordinary I/O-failure noise.
|
|
229
|
+
*
|
|
230
|
+
* Structurally unreachable on the mail-inbound path today: `enqueueInbox` is
|
|
231
|
+
* only ever called with a freshly stamped `receivedAt` (a redelivery
|
|
232
|
+
* re-stamps `Date.now()` rather than carrying the original), and the watermark
|
|
233
|
+
* only ever advances to at most `consumedAt - retentionHorizonMs <= now`, so a
|
|
234
|
+
* fresh `receivedAt` sits a full horizon above it. The sole path that carries
|
|
235
|
+
* an original (old) `receivedAt` back into the queue is
|
|
236
|
+
* `replayProcessingToInbox`, which writes straight to `inbox/` and bypasses
|
|
237
|
+
* this gate entirely. If any redelivery source is ever changed to carry the
|
|
238
|
+
* original `receivedAt` into `enqueueInbox`, this becomes reachable and its
|
|
239
|
+
* withhold-not-ack handling becomes load-bearing.
|
|
240
|
+
*/
|
|
241
|
+
export declare class StaleInboxEnqueueError extends Error {
|
|
242
|
+
constructor(message: string);
|
|
243
|
+
}
|
|
156
244
|
/**
|
|
157
245
|
* Append a new inbox entry for `address`. The merge callback reads
|
|
158
246
|
* the address subtree under the per-repo lock, augments the inbox
|
|
@@ -166,7 +254,7 @@ export type EnqueueInboxResult = {
|
|
|
166
254
|
* invariant here also catches the concurrent-enqueue race that the
|
|
167
255
|
* per-repo lock alone cannot surface.
|
|
168
256
|
*/
|
|
169
|
-
export declare function enqueueInbox(store: RepoStore, principal: Principal, repoId: RepoId, args: EnqueueInboxArgs): Promise<
|
|
257
|
+
export declare function enqueueInbox(store: RepoStore, principal: Principal, repoId: RepoId, args: EnqueueInboxArgs): Promise<EnqueueInboxOutcome>;
|
|
170
258
|
export type DequeueToProcessingResult = {
|
|
171
259
|
commitSha: string;
|
|
172
260
|
key: string;
|
|
@@ -208,7 +296,7 @@ export type ReadProcessingEntryResult = {
|
|
|
208
296
|
* committed -- which is exactly when the supervisor forwards
|
|
209
297
|
* `trigger.fired` -- observes the processing entry. Reading the working tree (rather than walking the
|
|
210
298
|
* committed git tree) matches the workflow-process child's sibling
|
|
211
|
-
*
|
|
299
|
+
* read of `runs/<runId>/events/`. Because the
|
|
212
300
|
* read issues no commit it cannot race the supervisor's `markConsumed`
|
|
213
301
|
* write; it returns a point-in-time snapshot of the directory.
|
|
214
302
|
*/
|
|
@@ -218,6 +306,16 @@ export type MarkConsumedArgs = {
|
|
|
218
306
|
messageId: string;
|
|
219
307
|
runId: string;
|
|
220
308
|
consumedAt: number;
|
|
309
|
+
/**
|
|
310
|
+
* Present when the supervisor deliberately refused the message instead of
|
|
311
|
+
* delivering it to the run. The consumed entry remains the durable dedup
|
|
312
|
+
* record, while Hub projection uses this detail to fail (rather than settle)
|
|
313
|
+
* an exclusive-dispatch row.
|
|
314
|
+
*/
|
|
315
|
+
rejection?: {
|
|
316
|
+
code: string;
|
|
317
|
+
message: string;
|
|
318
|
+
};
|
|
221
319
|
/**
|
|
222
320
|
* Retention horizon for the consumed dedup index, in milliseconds.
|
|
223
321
|
* The commit advances the per-address watermark to
|
|
@@ -284,6 +382,22 @@ export type ReplayProcessingToInboxResult = {
|
|
|
284
382
|
* yields an empty set.
|
|
285
383
|
*/
|
|
286
384
|
export declare function readOwnedMessageIds(store: RepoStore, repoId: RepoId): Promise<Set<string>>;
|
|
385
|
+
export type WorkflowRunLifecycle = "absent" | "live" | "terminal";
|
|
386
|
+
/** Read one run's lifecycle from a committed workflow-run tree. */
|
|
387
|
+
export declare function readCommittedWorkflowRunLifecycle(reads: CommittedReads | null, runId: string): Promise<WorkflowRunLifecycle>;
|
|
388
|
+
/**
|
|
389
|
+
* Read the durable lifecycle of one run from the workflow-run working tree.
|
|
390
|
+
* `grants.json` alone is still an absent run: grants are staged before the
|
|
391
|
+
* first trigger, while the first event is the durable proof that the run was
|
|
392
|
+
* fired. A sealed event log is terminal by the kind handler's compaction
|
|
393
|
+
* invariant.
|
|
394
|
+
*
|
|
395
|
+
* The supervisor uses this when in-memory cohort membership is empty. That
|
|
396
|
+
* happens both for a genuinely new deployment and briefly during recovery,
|
|
397
|
+
* so treating both states as "fire" would start a second driver for a live
|
|
398
|
+
* log or reuse a terminal run.
|
|
399
|
+
*/
|
|
400
|
+
export declare function readWorkflowRunLifecycle(store: RepoStore, repoId: RepoId, runId: string): Promise<WorkflowRunLifecycle>;
|
|
287
401
|
export type ReplayProcessingToInboxOpts = {
|
|
288
402
|
/**
|
|
289
403
|
* MessageIds whose run is still LIVE (non-terminal) and therefore owns
|