@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.
Files changed (73) hide show
  1. package/README.md +3 -5
  2. package/dist/agent-repo.d.ts +9 -5
  3. package/dist/agent-repo.js +2 -2
  4. package/dist/agent-state-kind.js +4 -0
  5. package/dist/asset-service.d.ts +1 -20
  6. package/dist/asset-service.js +9 -91
  7. package/dist/committed-source-tree.d.ts +10 -0
  8. package/dist/committed-source-tree.js +35 -0
  9. package/dist/credential-push.d.ts +7 -6
  10. package/dist/credential-push.js +42 -18
  11. package/dist/event-collector-registry.d.ts +1 -1
  12. package/dist/event-collector-registry.js +4 -4
  13. package/dist/event-collector.d.ts +1 -1
  14. package/dist/event-collector.js +10 -2
  15. package/dist/hub-session-lookups.d.ts +125 -7
  16. package/dist/hub-session-lookups.js +539 -80
  17. package/dist/hub-session-orchestrator.js +14 -49
  18. package/dist/index.d.ts +17 -8
  19. package/dist/index.js +14 -6
  20. package/dist/repo-store/index.d.ts +1 -1
  21. package/dist/repo-store/store.d.ts +1 -1
  22. package/dist/repo-store/store.js +138 -1
  23. package/dist/repo-store/subscribe-kind.d.ts +6 -3
  24. package/dist/repo-store/subscribe-kind.js +42 -77
  25. package/dist/repo-store/types.d.ts +94 -6
  26. package/dist/session-service.d.ts +277 -96
  27. package/dist/session-service.js +741 -547
  28. package/dist/sidecar-allocation/contracts.d.ts +78 -0
  29. package/dist/sidecar-allocation/contracts.js +21 -0
  30. package/dist/sidecar-allocation/index.d.ts +4 -0
  31. package/dist/sidecar-allocation/index.js +3 -0
  32. package/dist/sidecar-allocation/placement-policy.d.ts +11 -0
  33. package/dist/sidecar-allocation/placement-policy.js +21 -0
  34. package/dist/sidecar-allocation/plugin-registry.d.ts +11 -0
  35. package/dist/sidecar-allocation/plugin-registry.js +37 -0
  36. package/dist/sidecar-allocation/reconciler.d.ts +42 -0
  37. package/dist/sidecar-allocation/reconciler.js +431 -0
  38. package/dist/skill-kind.js +4 -0
  39. package/dist/substrate.d.ts +3 -3
  40. package/dist/substrate.js +1 -1
  41. package/dist/workflow-allocation-service.d.ts +58 -0
  42. package/dist/workflow-allocation-service.js +239 -0
  43. package/dist/workflow-closure-resolution.d.ts +106 -0
  44. package/dist/workflow-closure-resolution.js +123 -0
  45. package/dist/workflow-definition-ensure.d.ts +24 -0
  46. package/dist/workflow-definition-ensure.js +75 -0
  47. package/dist/workflow-dispatch-service.d.ts +40 -0
  48. package/dist/workflow-dispatch-service.js +146 -0
  49. package/dist/workflow-dispatch-settlement.d.ts +29 -0
  50. package/dist/workflow-dispatch-settlement.js +140 -0
  51. package/dist/workflow-kind.d.ts +17 -1
  52. package/dist/workflow-kind.js +127 -80
  53. package/dist/workflow-probe-gate.d.ts +214 -0
  54. package/dist/workflow-probe-gate.js +207 -0
  55. package/dist/workflow-run-kind.d.ts +128 -14
  56. package/dist/workflow-run-kind.js +353 -83
  57. package/dist/workflow-run-reader.d.ts +1 -1
  58. package/dist/workflow-run-reader.js +3 -7
  59. package/dist/workflow-run-restore.d.ts +15 -0
  60. package/dist/workflow-run-restore.js +26 -0
  61. package/dist/workflow-source-closure.d.ts +35 -0
  62. package/dist/workflow-source-closure.js +342 -0
  63. package/dist/ws/index.d.ts +3 -3
  64. package/dist/ws/index.js +1 -1
  65. package/dist/ws/sidecar-events.d.ts +100 -12
  66. package/dist/ws/sidecar-events.js +2 -0
  67. package/dist/ws/sidecar-handler.d.ts +128 -7
  68. package/dist/ws/sidecar-handler.js +1069 -135
  69. package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
  70. package/dist/ws/sidecar-token-authenticator.js +64 -7
  71. package/package.json +14 -13
  72. package/dist/available-skills-stanza.d.ts +0 -21
  73. 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 deploymentId: string;
11
+ readonly anchorRunId: string;
12
12
  readonly runId?: string;
13
13
  };
14
14
  export type WorkflowRunSupervisorPrincipal = {
15
15
  readonly kind: "supervisor";
16
- readonly deploymentId: string;
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
- * window in which the same `messageId` could legitimately be
47
- * re-submitted and still must be caught as a duplicate. There is no
48
- * automatic internal mail redelivery in the system today, so this is
49
- * the external re-submission window. If an at-least-once redelivery
50
- * source is ever added, the horizon must be >= its maximum redelivery
51
- * window or dedup breaks; a breach surfaces LOUDLY (a too-late
52
- * re-submission carrying an old `receivedAt` is refused at enqueue,
53
- * not silently reprocessed) rather than as silent double-processing.
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<EnqueueInboxResult>;
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
- * reads of `workflow.json` and `runs/<runId>/events/`. Because the
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