@intx/hub-sessions 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-repo.d.ts +14 -2
- package/dist/agent-repo.js +17 -4
- package/dist/agent-state-kind.js +14 -63
- package/dist/asset-service.js +14 -10
- package/dist/credential-push.d.ts +48 -4
- package/dist/credential-push.js +138 -6
- package/dist/event-collector-registry.d.ts +2 -1
- package/dist/event-collector-registry.js +38 -9
- package/dist/event-collector.d.ts +11 -1
- package/dist/event-collector.js +36 -3
- package/dist/hub-session-lookups.d.ts +1 -1
- package/dist/hub-session-lookups.js +68 -72
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +13 -12
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +1 -0
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +66 -121
- package/dist/session-service.js +444 -411
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +29 -6
- package/dist/sidecar-allocation/contracts.js +7 -2
- package/dist/sidecar-allocation/index.d.ts +4 -3
- package/dist/sidecar-allocation/index.js +3 -2
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +16 -3
- package/dist/sidecar-allocation/plugin-registry.js +36 -12
- package/dist/sidecar-allocation/reconciler.d.ts +16 -4
- package/dist/sidecar-allocation/reconciler.js +486 -92
- package/dist/skill-kind.js +8 -62
- package/dist/substrate.d.ts +1 -1
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +21 -15
- package/dist/workflow-allocation-service.js +440 -125
- package/dist/workflow-dispatch-service.d.ts +4 -2
- package/dist/workflow-dispatch-service.js +89 -26
- package/dist/workflow-kind.d.ts +12 -0
- package/dist/workflow-kind.js +17 -60
- package/dist/workflow-probe-gate.d.ts +99 -27
- package/dist/workflow-probe-gate.js +196 -21
- package/dist/workflow-run-kind.d.ts +112 -19
- package/dist/workflow-run-kind.js +626 -210
- package/dist/workflow-run-restore.d.ts +1 -0
- package/dist/workflow-run-restore.js +5 -1
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +1 -1
- package/dist/ws/index.js +1 -1
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +43 -29
- package/dist/ws/sidecar-events.js +0 -2
- package/dist/ws/sidecar-handler.d.ts +122 -85
- package/dist/ws/sidecar-handler.js +925 -878
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.js +37 -23
- package/package.json +13 -13
- package/dist/sidecar-allocation/placement-policy.d.ts +0 -11
- package/dist/sidecar-allocation/placement-policy.js +0 -21
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
// 3. RECOMPUTE the wire hash over the RECEIVED projection as tamper-evidence:
|
|
13
13
|
// a shipped hash that differs from the hub recompute is rejected, fail
|
|
14
14
|
// closed, no coercion.
|
|
15
|
-
// 4.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// 4. Reject a projection declaring a trigger type the runtime does not
|
|
16
|
+
// implement, so a deployment that could only ever sit inert never gets
|
|
17
|
+
// approved.
|
|
18
|
+
// 5. Reject a projection carrying a step the grant walk left no record for,
|
|
19
|
+
// so no deployment can schedule a step whose grants nobody approved.
|
|
20
|
+
// 6. Gate the advisory grant set AND the definition's declared grant
|
|
21
|
+
// requirements against the approval policy: an operator `ApprovalSet`
|
|
22
|
+
// requires every grant the probe surfaced and every requirement it
|
|
23
|
+
// declared to be approved or the gate fails, while `approve-probed`
|
|
24
|
+
// approves exactly what the probe surfaced.
|
|
25
|
+
// 7. Freeze the approved wire hash onto the definition version row, keyed by
|
|
20
26
|
// the definition's selector, and return the frozen approved grant set.
|
|
21
27
|
//
|
|
22
28
|
// The frozen approved set is the single source of truth for the definition's
|
|
@@ -25,9 +31,12 @@
|
|
|
25
31
|
// did not have frozen at approval. The wire hash is that freeze's anchor -- the
|
|
26
32
|
// grant set is a deterministic projection of the exact content the hash
|
|
27
33
|
// addresses, so pinning the hash pins the set.
|
|
34
|
+
import { type } from "arktype";
|
|
28
35
|
import { and, eq } from "drizzle-orm";
|
|
29
36
|
import { workflowDefinitionVersion } from "@intx/db/schema";
|
|
30
37
|
import { computeWireDefinitionHash } from "@intx/types/wire-definition-hash";
|
|
38
|
+
import { EXECUTABLE_STEP_DESCENT, walkStepTree, } from "@intx/workflow/definition";
|
|
39
|
+
import { approvalSetFromItems, inertNestedBodies, isApprovedGrantRequirement, } from "@intx/workflow-deploy";
|
|
31
40
|
import { buildSourceAssetMounts, resolveWorkflowClosure, } from "./workflow-closure-resolution.js";
|
|
32
41
|
import { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
|
|
33
42
|
// The version `ensureWorkflowDefinitionForAsset` projects for a fresh
|
|
@@ -73,18 +82,113 @@ export function createDbFrozenApprovalWriter(db) {
|
|
|
73
82
|
};
|
|
74
83
|
}
|
|
75
84
|
function isApproveProbed(policy) {
|
|
76
|
-
return "
|
|
85
|
+
return policy.kind === "approve-probed";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Trigger types the definition vocabulary declares but the runtime does not
|
|
89
|
+
* implement. `schedule` is reserved: no cron parser and no scheduler exist, so
|
|
90
|
+
* a schedule-triggered deployment would hash, deploy, and then never fire --
|
|
91
|
+
* no error, no log, no failed run. Admitting one at the gate is the only way a
|
|
92
|
+
* deployment reaches that state, so the gate refuses it.
|
|
93
|
+
*/
|
|
94
|
+
const UNIMPLEMENTED_TRIGGER_TYPES = new Set(["schedule"]);
|
|
95
|
+
// A projected trigger, typed only to its discriminant. `triggers` rides the
|
|
96
|
+
// wire projection as `unknown[]` on purpose (the wire envelope does not own
|
|
97
|
+
// the trigger vocabulary), so the discriminant is read through a validator
|
|
98
|
+
// rather than an assertion. An entry that carries no string `type` is not a
|
|
99
|
+
// trigger this gate has an opinion about and is left to the deploy path.
|
|
100
|
+
const ProjectedTriggerType = type({ type: "string" });
|
|
101
|
+
/**
|
|
102
|
+
* The distinct unimplemented trigger types a projection declares, in first-seen
|
|
103
|
+
* order. Empty when every declared trigger has an implementation behind it.
|
|
104
|
+
*/
|
|
105
|
+
function collectUnimplementedTriggerTypes(triggers) {
|
|
106
|
+
const found = [];
|
|
107
|
+
for (const trigger of triggers) {
|
|
108
|
+
const parsed = ProjectedTriggerType(trigger);
|
|
109
|
+
if (parsed instanceof type.errors)
|
|
110
|
+
continue;
|
|
111
|
+
if (!UNIMPLEMENTED_TRIGGER_TYPES.has(parsed.type))
|
|
112
|
+
continue;
|
|
113
|
+
if (found.includes(parsed.type))
|
|
114
|
+
continue;
|
|
115
|
+
found.push(parsed.type);
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
const EXECUTABLE_CLOSURE_CONTEXT = "probe gate executable closure: ";
|
|
120
|
+
/**
|
|
121
|
+
* Every step the deployment can execute, walked over the frozen inert
|
|
122
|
+
* projection under `EXECUTABLE_STEP_DESCENT`, each carrying the chain it was
|
|
123
|
+
* reached through and the top-level step whose grant record accounts for it.
|
|
124
|
+
*
|
|
125
|
+
* The descent is the canonical one rather than a local re-derivation on
|
|
126
|
+
* purpose. Sharing `EXECUTABLE_STEP_DESCENT` with the capability walk makes the
|
|
127
|
+
* two sides agree on ONE thing: which primitive kinds are descended into, so a
|
|
128
|
+
* newly-added container kind cannot become reachable here while staying
|
|
129
|
+
* invisible to the walk. It does NOT make the two agree about grants. This
|
|
130
|
+
* function sees only step positions; whether the walk folded the right grants
|
|
131
|
+
* into a record is not observable from the inert projection and is not checked
|
|
132
|
+
* anywhere on this path.
|
|
133
|
+
*
|
|
134
|
+
* The position each step was reached at is the walk's own `path`, for the same
|
|
135
|
+
* reason. The head of that path is the top-level step the entry descends from,
|
|
136
|
+
* which is exactly the `perStep` key the capability walk folds its grants into.
|
|
137
|
+
*/
|
|
138
|
+
function collectExecutableReaches(projection) {
|
|
139
|
+
const reaches = [];
|
|
140
|
+
walkStepTree({
|
|
141
|
+
tree: projection,
|
|
142
|
+
context: EXECUTABLE_CLOSURE_CONTEXT,
|
|
143
|
+
nestedTrees: (step) => inertNestedBodies(step, EXECUTABLE_STEP_DESCENT),
|
|
144
|
+
visit: ({ stepId, path }) => {
|
|
145
|
+
reaches.push({ stepId, recordStepId: path[0], reachedThrough: path });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
return reaches;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Every executable step the grant-walk snapshot carries no record for.
|
|
152
|
+
*
|
|
153
|
+
* A step's approved-grant record is the snapshot entry keyed by the top-level
|
|
154
|
+
* step it descends from: the capability walk collects one record per top-level
|
|
155
|
+
* step and folds into it the grants of every step that step can run.
|
|
156
|
+
*
|
|
157
|
+
* This is a PRESENCE check and nothing more. An absent record is decisive --
|
|
158
|
+
* it leaves every step of that subtree with no approved grants at all, which
|
|
159
|
+
* is the hole this catches. A present record is not evidence the other way:
|
|
160
|
+
* nothing here opens the record to confirm it actually carries the grants the
|
|
161
|
+
* steps beneath it need. A record present but under-filled passes this check
|
|
162
|
+
* and still refuses those tool calls at run time.
|
|
163
|
+
*/
|
|
164
|
+
function collectStepsWithoutGrantRecord(projection, snapshot) {
|
|
165
|
+
const recordedStepIds = new Set(snapshot.perStep.map((record) => record.stepId));
|
|
166
|
+
return collectExecutableReaches(projection).filter((reach) => !recordedStepIds.has(reach.recordStepId));
|
|
167
|
+
}
|
|
168
|
+
function describeStepsWithoutGrantRecord(missing) {
|
|
169
|
+
const positions = missing
|
|
170
|
+
.map((missed) => `${missed.stepId} (reached through ${missed.reachedThrough.join(" > ")}; expected a grant-walk record keyed by top-level step ${missed.recordStepId})`)
|
|
171
|
+
.join("; ");
|
|
172
|
+
return `the probe's grant-walk snapshot carries no approved-grant record covering ${String(missing.length)} executable step(s): ${positions}. A step outside every record deploys with no approved grants, and its tool calls are refused at run time with nothing on the deploy path reporting it.`;
|
|
77
173
|
}
|
|
78
174
|
/**
|
|
79
175
|
* Gate a probe result and, on approval, freeze it. Operates purely over the
|
|
80
176
|
* inert projection and grant set -- no author code runs here and the capability
|
|
81
177
|
* walk is never re-run.
|
|
82
178
|
*
|
|
83
|
-
* Fails closed on the
|
|
179
|
+
* Fails closed on the three security-load-bearing checks before it writes
|
|
84
180
|
* anything: the recomputed wire hash must match the hash the sidecar shipped
|
|
85
|
-
* (tamper-evidence),
|
|
86
|
-
*
|
|
87
|
-
*
|
|
181
|
+
* (tamper-evidence), every advisory grant must be operator-approved, and every
|
|
182
|
+
* declared grant requirement must be operator-approved. It also refuses a
|
|
183
|
+
* projection whose triggers include a reserved-but-unimplemented type -- not a
|
|
184
|
+
* security check, but the layer a pinned closure cannot carry a stale copy of,
|
|
185
|
+
* so it is where a workflow that could only sit inert is caught -- and one
|
|
186
|
+
* whose executable closure reaches a step the grant walk left no record for --
|
|
187
|
+
* the layer holding both halves of the probe answer at once, and a presence
|
|
188
|
+
* check on those records rather than a check that any record's contents are
|
|
189
|
+
* sufficient.
|
|
190
|
+
* Only then does it freeze the recomputed hash onto the version row and return
|
|
191
|
+
* the approved grant set.
|
|
88
192
|
*/
|
|
89
193
|
export async function gateAndFreezeProbeResult(args) {
|
|
90
194
|
const { assetId, probeResult, approvals, persist } = args;
|
|
@@ -101,20 +205,94 @@ export async function gateAndFreezeProbeResult(args) {
|
|
|
101
205
|
recomputedWireHash,
|
|
102
206
|
};
|
|
103
207
|
}
|
|
208
|
+
// Reject a trigger type nothing implements. This runs on the projection
|
|
209
|
+
// rather than on the author's definition because `defineWorkflow` is bundled
|
|
210
|
+
// INTO the pinned workflow closure: a closure published before the authoring
|
|
211
|
+
// check carries its own frozen copy and never sees it. The projection's
|
|
212
|
+
// `triggers` are produced by the hub's live->inert projector, so this is the
|
|
213
|
+
// one trigger surface a stale closure cannot carry past. Placed after the
|
|
214
|
+
// wire-hash check so tamper-evidence still decides first -- the projection
|
|
215
|
+
// must be the one the sidecar hashed before its content is reasoned about.
|
|
216
|
+
const unimplementedTriggerTypes = collectUnimplementedTriggerTypes(probeResult.projection.triggers);
|
|
217
|
+
if (unimplementedTriggerTypes.length > 0) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
reason: "unimplemented_trigger",
|
|
221
|
+
unimplementedTriggerTypes,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// Totality: every step the deployment can execute must have an approved-grant
|
|
225
|
+
// record behind it. The two halves of a probe answer are produced
|
|
226
|
+
// independently -- the projection by the hub's live->inert projector, the
|
|
227
|
+
// grant-walk snapshot by the sidecar's capability walk over the live
|
|
228
|
+
// definition -- and nothing until now compared them. A step the walk skipped
|
|
229
|
+
// still projects, still deploys, and is still scheduled; its tool calls are
|
|
230
|
+
// then refused for lack of any grant, and the tool runner turns that refusal
|
|
231
|
+
// into an error tool result rather than a failure, so the run completes
|
|
232
|
+
// having done none of the work. This is the check that makes that
|
|
233
|
+
// unreachable: it is total over the closure and it runs on every deploy,
|
|
234
|
+
// rather than depending on some test happening to invoke a tool from the
|
|
235
|
+
// affected step.
|
|
236
|
+
//
|
|
237
|
+
// Placed after the trigger check and before the operator-policy checks
|
|
238
|
+
// below. A deploy that trips this is a defect in the deploy path, not a
|
|
239
|
+
// decision the operator can make differently, so it must not be reported
|
|
240
|
+
// behind an unapproved-grant message an operator would act on instead.
|
|
241
|
+
//
|
|
242
|
+
// DO NOT move this assertion earlier in this package's history. The record it
|
|
243
|
+
// requires is a claim that the approval covers everything the step can run,
|
|
244
|
+
// and that claim was not kept for a step inside a loop body or a section body
|
|
245
|
+
// until the deploy and runtime producers were made total over the executable
|
|
246
|
+
// closure. Asserted before those producers, the gate would have been
|
|
247
|
+
// enforcing a guarantee the rest of the system did not honour.
|
|
248
|
+
const stepsWithoutGrantRecord = collectStepsWithoutGrantRecord(probeResult.projection, probeResult.grantWalkSnapshot);
|
|
249
|
+
if (stepsWithoutGrantRecord.length > 0) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
reason: "steps_without_grant_record",
|
|
253
|
+
stepsWithoutGrantRecord,
|
|
254
|
+
message: describeStepsWithoutGrantRecord(stepsWithoutGrantRecord),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
104
257
|
// Gate the advisory grant set. Under an `ApprovalSet` every grant the probe
|
|
105
258
|
// surfaced must appear in the operator's approved set; any miss fails the
|
|
106
259
|
// gate closed. Under `approve-probed` there is no set to gate against -- the
|
|
107
260
|
// probe's surface IS the approved set -- so nothing is ever unapproved.
|
|
108
261
|
const unapprovedGrants = isApproveProbed(approvals)
|
|
109
262
|
? []
|
|
110
|
-
: probeResult.grants.filter((grant) => !approvals.has(grant));
|
|
263
|
+
: probeResult.grants.filter((grant) => !approvals.grants.has(grant));
|
|
111
264
|
if (unapprovedGrants.length > 0) {
|
|
112
265
|
return { ok: false, reason: "grants_not_approved", unapprovedGrants };
|
|
113
266
|
}
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
267
|
+
// Gate the DECLARED grant requirements. These ride the walk snapshot rather
|
|
268
|
+
// than the flattened `grants`, and the walk never surfaces them, so the
|
|
269
|
+
// filter above cannot see them -- yet the run path materializes each one into
|
|
270
|
+
// a real grant row on the run principal, and a wildcard row reaches gates no
|
|
271
|
+
// walk-derived row can address. They therefore need the operator's decision
|
|
272
|
+
// on exactly the same terms the advertised grants do. Under `approve-probed`
|
|
273
|
+
// the probe's surface IS the approved surface, so there is nothing to gate
|
|
274
|
+
// against and the requirements ride into the approval below.
|
|
275
|
+
const declaredRequirements = probeResult.grantWalkSnapshot.grantRequirements;
|
|
276
|
+
const unapprovedGrantRequirements = isApproveProbed(approvals)
|
|
277
|
+
? []
|
|
278
|
+
: declaredRequirements.filter((requirement) => !isApprovedGrantRequirement(approvals, requirement));
|
|
279
|
+
if (unapprovedGrantRequirements.length > 0) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: "grant_requirements_not_approved",
|
|
283
|
+
unapprovedGrantRequirements,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Freeze: the approved surface is exactly what the workflow advertised (all
|
|
287
|
+
// of it now operator-approved), pinned to the recomputed hash. Persisting the
|
|
288
|
+
// hash is the freeze; the approved surface is returned for the deploy
|
|
289
|
+
// hand-off. The declared requirements join the walk's grant strings in that
|
|
290
|
+
// surface under both policies, so the record the hand-off and any audit read
|
|
291
|
+
// describes every kind of authority the freeze will mint.
|
|
292
|
+
const approvedGrants = [
|
|
293
|
+
...probeResult.grants,
|
|
294
|
+
...declaredRequirements,
|
|
295
|
+
];
|
|
118
296
|
const { definitionId } = await persist({
|
|
119
297
|
assetId,
|
|
120
298
|
approvedWireHash: recomputedWireHash,
|
|
@@ -125,7 +303,7 @@ export async function gateAndFreezeProbeResult(args) {
|
|
|
125
303
|
ok: true,
|
|
126
304
|
definitionId,
|
|
127
305
|
approvedWireHash: recomputedWireHash,
|
|
128
|
-
|
|
306
|
+
approvedSurface: approvalSetFromItems(approvedGrants),
|
|
129
307
|
projection: probeResult.projection,
|
|
130
308
|
};
|
|
131
309
|
}
|
|
@@ -187,16 +365,13 @@ export async function installAndApproveWorkflowDefinition(args) {
|
|
|
187
365
|
});
|
|
188
366
|
assets = [];
|
|
189
367
|
}
|
|
190
|
-
const
|
|
191
|
-
if (sendProbe === undefined) {
|
|
192
|
-
throw new Error("installAndApproveWorkflowDefinition: router does not support sendProbe");
|
|
193
|
-
}
|
|
194
|
-
const probeResult = await sendProbe({
|
|
368
|
+
const probeResult = await args.router.sendProbe({
|
|
195
369
|
source: args.source,
|
|
196
370
|
closure,
|
|
197
371
|
entry: args.entry,
|
|
198
372
|
...(assets.length > 0 ? { assets } : {}),
|
|
199
373
|
});
|
|
374
|
+
await args.onProbeResult?.(probeResult);
|
|
200
375
|
const approval = await gateAndFreezeProbeResult({
|
|
201
376
|
assetId: args.assetId,
|
|
202
377
|
probeResult,
|
|
@@ -26,6 +26,20 @@ export declare const WORKFLOW_RUN_CONTROL_PREFIX = "control";
|
|
|
26
26
|
export declare const WORKFLOW_RUN_INBOX_DIR = "inbox";
|
|
27
27
|
export declare const WORKFLOW_RUN_PROCESSING_DIR = "processing";
|
|
28
28
|
export declare const WORKFLOW_RUN_CONSUMED_DIR = "consumed";
|
|
29
|
+
/**
|
|
30
|
+
* Per-run inbound mail-part subtree. Non-text inbound mail content
|
|
31
|
+
* (image/audio/video/document mail parts) is committed here as real
|
|
32
|
+
* files rather than inlined into the JSON event log, whose serialization
|
|
33
|
+
* boundary would corrupt binary bytes. The layout is
|
|
34
|
+
* `runs/<runId>/parts/<urlEncoded(messageId)>/<index>-<name>`: one
|
|
35
|
+
* directory per inbound message (so a long-lived run's successive turns
|
|
36
|
+
* never collide), and one file per mail part carrying its verbatim
|
|
37
|
+
* bytes. The workflow-host ingest writes the bytes and records a
|
|
38
|
+
* lightweight `{ name, contentType, ref }` reference into the run's
|
|
39
|
+
* trigger / signal payload; the step invoker reads the bytes back at
|
|
40
|
+
* `agent.send` time. Files are immutable once written, like `blobs/`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const WORKFLOW_RUN_PARTS_DIR = "parts";
|
|
29
43
|
/**
|
|
30
44
|
* Filename of the per-address retention watermark blob, a direct child
|
|
31
45
|
* of `addresses/<urlEncoded>/` (a file, not a directory). Carries the
|
|
@@ -82,6 +96,38 @@ export declare const DEFAULT_CONSUMED_RETENTION_MS: number;
|
|
|
82
96
|
* `<agentKey>` directory layer below the prefix).
|
|
83
97
|
*/
|
|
84
98
|
export declare const WORKFLOW_RUN_AGENT_STATE_PREFIX = "agent-state";
|
|
99
|
+
/**
|
|
100
|
+
* Conversational-mailbox subtree for the warm single-step agent. The
|
|
101
|
+
* substrate mailbox backing commits the agent's durable inbox under
|
|
102
|
+
* `mailbox/INBOX/` so the full message history replicates to the hub
|
|
103
|
+
* alongside the run state. The layout is:
|
|
104
|
+
*
|
|
105
|
+
* - `mailbox/INBOX/index.json` — the mailbox index. MUTABLE: the
|
|
106
|
+
* backing rewrites it on every flush, so it is exempt from the
|
|
107
|
+
* retained-blob byte-equality walk the `<uid>.eml` blobs are subject
|
|
108
|
+
* to. It must nonetheless PERSIST once it existed: dropping it drops
|
|
109
|
+
* the whole mailbox and resets uidValidity/uidNext on the next open.
|
|
110
|
+
* - `mailbox/INBOX/<uid>.eml` — one message per file, carrying the
|
|
111
|
+
* raw signed message bytes. `<uid>` is a decimal integer >= 1. A
|
|
112
|
+
* RETAINED `<uid>.eml` (present in both prior and prospective) is
|
|
113
|
+
* opaque and IMMUTABLE: it must reappear byte-identically, exactly
|
|
114
|
+
* like `runs/<runId>/blobs/`. A prior `<uid>.eml` may be ABSENT from
|
|
115
|
+
* the prospective tree — that is the warm agent expunging a message.
|
|
116
|
+
* The raw bytes are not lost: they stay reachable through the parent
|
|
117
|
+
* commit, and a `workflow-run` repo's objects are never GC'd (its
|
|
118
|
+
* kind is off the GC allow-list), so an expunged message survives in
|
|
119
|
+
* history for the life of the run repo. The audit trail rests on
|
|
120
|
+
* "these objects are never pruned", not on the live tree being
|
|
121
|
+
* monotonic.
|
|
122
|
+
*
|
|
123
|
+
* The only entries permitted under `mailbox/` are the `INBOX/`
|
|
124
|
+
* directory; the only entries permitted under `mailbox/INBOX/` are
|
|
125
|
+
* `index.json` and `<uid>.eml` message files. Anything else fails the
|
|
126
|
+
* push.
|
|
127
|
+
*/
|
|
128
|
+
export declare const WORKFLOW_RUN_MAILBOX_PREFIX = "mailbox";
|
|
129
|
+
export declare const WORKFLOW_RUN_MAILBOX_INBOX_DIR = "INBOX";
|
|
130
|
+
export declare const WORKFLOW_RUN_MAILBOX_INDEX_FILE = "index.json";
|
|
85
131
|
/**
|
|
86
132
|
* Parse the seq from a per-event log filename `<seq>.json` under
|
|
87
133
|
* `runs/<runId>/events/`. Returns the non-negative integer seq, or
|
|
@@ -104,6 +150,15 @@ export declare function parseEventSeq(filename: string): number | null;
|
|
|
104
150
|
* offending entry is identifiable.
|
|
105
151
|
*/
|
|
106
152
|
export declare function requireEventSeq(filename: string, context: string): number;
|
|
153
|
+
/**
|
|
154
|
+
* Maximum byte length of a single mail part path component (the
|
|
155
|
+
* URL-encoded message segment, and each `<index>-<name>` filename).
|
|
156
|
+
* messageIds and mail part names arrive from untrusted inbound mail;
|
|
157
|
+
* an over-long RFC 5322 message-id URL-encodes past the filesystem's
|
|
158
|
+
* 255-byte component limit and would otherwise fail at disk-write time,
|
|
159
|
+
* downstream of validation. Reject it at the boundary instead.
|
|
160
|
+
*/
|
|
161
|
+
export declare const MAX_MAIL_PART_PATH_COMPONENT_BYTES = 255;
|
|
107
162
|
/**
|
|
108
163
|
* JSON envelope carried by inbox and processing entries. Keys:
|
|
109
164
|
* - `messageId`: dedup key for the inbound message.
|
|
@@ -169,6 +224,35 @@ declare const WatermarkEnvelope: import("arktype/internal/variants/object.ts").O
|
|
|
169
224
|
export type ClaimCheckEnvelope = typeof ClaimCheckEnvelope.infer;
|
|
170
225
|
export type ConsumedEnvelope = typeof ConsumedEnvelope.infer;
|
|
171
226
|
export type WatermarkEnvelope = typeof WatermarkEnvelope.infer;
|
|
227
|
+
/**
|
|
228
|
+
* Terminal event discriminators mapped to the `workflow_run.status` value
|
|
229
|
+
* each settles the run into. A run whose log contains an entry with one of
|
|
230
|
+
* these `type` values must not receive any event with a strictly greater
|
|
231
|
+
* seq.
|
|
232
|
+
*
|
|
233
|
+
* This map is a hand-rolled copy of the runtime's terminal-run vocabulary
|
|
234
|
+
* (`isTerminalRunPhase` in `@intx/workflow` state-machine `state.ts`,
|
|
235
|
+
* re-exported from the state-machine index and consumed by `transition.ts`),
|
|
236
|
+
* duplicated here because `@intx/hub-sessions` must not depend on
|
|
237
|
+
* `@intx/workflow`. It is the sole authority for that vocabulary and MUST
|
|
238
|
+
* stay in sync with the canonical runtime definition:
|
|
239
|
+
* if the runtime adds or removes a terminal run phase, update this map too.
|
|
240
|
+
* Drift silently reopens the restore-time double-driver collision that
|
|
241
|
+
* `scanRunsForBoot` (below) exists to prevent.
|
|
242
|
+
*/
|
|
243
|
+
type TerminalRunStatus = "completed" | "failed" | "cancelled";
|
|
244
|
+
/**
|
|
245
|
+
* Classify a workflow-run event type against the terminal-status vocabulary.
|
|
246
|
+
* `TERMINAL_EVENT_STATUS` is the sole authority (see above), so a type absent
|
|
247
|
+
* from it is by definition not terminal: no separate membership set is
|
|
248
|
+
* consulted and no "unmapped terminal type" case can arise.
|
|
249
|
+
*/
|
|
250
|
+
export declare function classifyTerminalEvent(eventType: string): {
|
|
251
|
+
terminal: true;
|
|
252
|
+
status: TerminalRunStatus;
|
|
253
|
+
} | {
|
|
254
|
+
terminal: false;
|
|
255
|
+
};
|
|
172
256
|
export declare const workflowRunKindHandler: KindHandler;
|
|
173
257
|
export declare const workflowRunAuthorize: AuthorizeFn;
|
|
174
258
|
export type EnqueueInboxArgs = {
|
|
@@ -310,7 +394,7 @@ export type MarkConsumedArgs = {
|
|
|
310
394
|
* Present when the supervisor deliberately refused the message instead of
|
|
311
395
|
* delivering it to the run. The consumed entry remains the durable dedup
|
|
312
396
|
* record, while Hub projection uses this detail to fail (rather than settle)
|
|
313
|
-
* an
|
|
397
|
+
* an allocation-dispatch row.
|
|
314
398
|
*/
|
|
315
399
|
rejection?: {
|
|
316
400
|
code: string;
|
|
@@ -361,27 +445,36 @@ export type ReplayProcessingToInboxResult = {
|
|
|
361
445
|
commitSha: string;
|
|
362
446
|
replayedKeys: string[];
|
|
363
447
|
};
|
|
448
|
+
export type ScanRunsForBootResult = {
|
|
449
|
+
ownedMessageIds: Set<string>;
|
|
450
|
+
pendingSealRunIds: string[];
|
|
451
|
+
};
|
|
364
452
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
453
|
+
* Walk `runs/` once and return the two boot-recovery inputs the supervisor's
|
|
454
|
+
* spawn needs, from a single traversal of the working tree via `getRepoDir`:
|
|
455
|
+
*
|
|
456
|
+
* - `ownedMessageIds`: the `consumedMessageId`s of NON-terminal runs -- the
|
|
457
|
+
* messages a live run still owns. Spawn feeds this into
|
|
458
|
+
* `replayProcessingToInbox`'s `ownedMessageIds` so a parked run's message is
|
|
459
|
+
* not re-admitted to inbox and dispatched a second time while the run is
|
|
460
|
+
* recovered by re-driving its durable log. Without this, the re-drive AND the
|
|
461
|
+
* re-triggered fresh run both re-park the same awaitSignal gate on the same
|
|
462
|
+
* runId, and the two concurrent runtime bodies race to a corrupt terminal.
|
|
463
|
+
* - `pendingSealRunIds`: runs that are terminal but still in per-event form --
|
|
464
|
+
* an interrupted fold left them unsealed. Spawn hands these to the recovery
|
|
465
|
+
* sweep, which re-runs the idempotent fold. A terminal event is a *proposal*:
|
|
466
|
+
* the authoritative decision is `compactRunEvents`, which independently
|
|
467
|
+
* re-checks the run's max-seq event and no-ops a run that is not actually
|
|
468
|
+
* terminal, so this scan may be loose.
|
|
374
469
|
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* carries a terminal event is excluded; an absent `runs/` directory
|
|
382
|
-
* yields an empty set.
|
|
470
|
+
* The working tree tracks the run-event ref (`refs/heads/main`); the
|
|
471
|
+
* claim-check ref (`refs/heads/events`) cannot see it, which is why this lives
|
|
472
|
+
* at the caller rather than inside `replayProcessingToInbox`'s single-ref
|
|
473
|
+
* delta. A run whose log is sealed (combined `events.jsonl`, only permitted for
|
|
474
|
+
* a terminated run) contributes to neither set; an absent `runs/` directory
|
|
475
|
+
* yields empty results.
|
|
383
476
|
*/
|
|
384
|
-
export declare function
|
|
477
|
+
export declare function scanRunsForBoot(store: RepoStore, repoId: RepoId): Promise<ScanRunsForBootResult>;
|
|
385
478
|
export type WorkflowRunLifecycle = "absent" | "live" | "terminal";
|
|
386
479
|
/** Read one run's lifecycle from a committed workflow-run tree. */
|
|
387
480
|
export declare function readCommittedWorkflowRunLifecycle(reads: CommittedReads | null, runId: string): Promise<WorkflowRunLifecycle>;
|