@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
|
@@ -26,15 +26,17 @@ export type WorkflowDispatchServiceDeps = {
|
|
|
26
26
|
/** Resolve the deployment anchor's durable routing address. */
|
|
27
27
|
readonly resolveAnchorAddress: (anchorRunId: string) => Promise<string | null>;
|
|
28
28
|
readonly leaseDurationMs?: number;
|
|
29
|
+
/** Bounds pending claims and unfinished deliveries across all drains. */
|
|
30
|
+
readonly maxConcurrentDispatches?: number;
|
|
29
31
|
readonly retryDelayMs?: (attempt: number) => number;
|
|
30
32
|
readonly now?: () => Date;
|
|
31
33
|
readonly createLeaseId?: () => string;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
|
-
* Drives Hub-owned workflow triggers onto
|
|
36
|
+
* Drives Hub-owned workflow triggers onto provisioned sidecars. The database
|
|
35
37
|
* row is the delivery authority: websocket acceptance never deletes the raw
|
|
36
38
|
* payload, and a generation replacement requeues every row that has not been
|
|
37
39
|
* settled by the workflow-run Git claim-check.
|
|
38
40
|
*/
|
|
39
|
-
export declare function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs, retryDelayMs, now, createLeaseId, }: WorkflowDispatchServiceDeps): WorkflowDispatchService;
|
|
41
|
+
export declare function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs, maxConcurrentDispatches, retryDelayMs, now, createLeaseId, }: WorkflowDispatchServiceDeps): WorkflowDispatchService;
|
|
40
42
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getLogger } from "@intx/log";
|
|
2
|
-
import { base64Encode, hexEncode } from "@intx/types";
|
|
2
|
+
import { base64Encode, deriveWorkflowRunId, hexEncode } from "@intx/types";
|
|
3
3
|
import { SignalDeliverFrame } from "@intx/types/sidecar";
|
|
4
|
+
import { runSidecarOperation, SidecarOperationTimeoutError, } from "./sidecar-allocation/operation.js";
|
|
4
5
|
const logger = getLogger(["hub", "workflow-dispatch"]);
|
|
5
6
|
const DEFAULT_LEASE_DURATION_MS = 30_000;
|
|
7
|
+
const DEFAULT_MAX_CONCURRENT_DISPATCHES = 8;
|
|
6
8
|
function defaultRetryDelay(attempt) {
|
|
7
9
|
return Math.min(500 * 2 ** Math.min(attempt, 6), 30_000);
|
|
8
10
|
}
|
|
@@ -12,6 +14,7 @@ function randomLeaseId() {
|
|
|
12
14
|
function targetForReadyAllocation(allocation) {
|
|
13
15
|
if (allocation.status !== "allocated" ||
|
|
14
16
|
allocation.ensureAcceptedGeneration !== allocation.generation ||
|
|
17
|
+
allocation.initializationLeaseId !== undefined ||
|
|
15
18
|
allocation.connectDeadline !== undefined) {
|
|
16
19
|
return null;
|
|
17
20
|
}
|
|
@@ -21,16 +24,22 @@ function targetForReadyAllocation(allocation) {
|
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
|
-
* Drives Hub-owned workflow triggers onto
|
|
27
|
+
* Drives Hub-owned workflow triggers onto provisioned sidecars. The database
|
|
25
28
|
* row is the delivery authority: websocket acceptance never deletes the raw
|
|
26
29
|
* payload, and a generation replacement requeues every row that has not been
|
|
27
30
|
* settled by the workflow-run Git claim-check.
|
|
28
31
|
*/
|
|
29
|
-
export function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs = DEFAULT_LEASE_DURATION_MS, retryDelayMs = defaultRetryDelay, now = () => new Date(), createLeaseId = randomLeaseId, }) {
|
|
30
|
-
if (leaseDurationMs <= 0) {
|
|
31
|
-
throw new Error("leaseDurationMs must be positive");
|
|
32
|
+
export function createWorkflowDispatchService({ dispatchStore, allocationStore, router, resolveAnchorAddress, leaseDurationMs = DEFAULT_LEASE_DURATION_MS, maxConcurrentDispatches = DEFAULT_MAX_CONCURRENT_DISPATCHES, retryDelayMs = defaultRetryDelay, now = () => new Date(), createLeaseId = randomLeaseId, }) {
|
|
33
|
+
if (!Number.isSafeInteger(leaseDurationMs) || leaseDurationMs <= 0) {
|
|
34
|
+
throw new Error("leaseDurationMs must be a positive integer");
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isSafeInteger(maxConcurrentDispatches) ||
|
|
37
|
+
maxConcurrentDispatches <= 0) {
|
|
38
|
+
throw new Error("maxConcurrentDispatches must be a positive integer");
|
|
32
39
|
}
|
|
33
40
|
let drainPromise = null;
|
|
41
|
+
let admittedDispatches = 0;
|
|
42
|
+
const activeDispatches = new Set();
|
|
34
43
|
function retryAt(attempt) {
|
|
35
44
|
return new Date(now().getTime() + retryDelayMs(attempt));
|
|
36
45
|
}
|
|
@@ -45,53 +54,107 @@ export function createWorkflowDispatchService({ dispatchStore, allocationStore,
|
|
|
45
54
|
});
|
|
46
55
|
}
|
|
47
56
|
async function reconcileNext() {
|
|
48
|
-
|
|
49
|
-
const dispatch = await dispatchStore.claimNextPending({
|
|
50
|
-
leaseId,
|
|
51
|
-
leaseDurationMs,
|
|
52
|
-
});
|
|
53
|
-
if (dispatch === null)
|
|
57
|
+
if (admittedDispatches >= maxConcurrentDispatches)
|
|
54
58
|
return false;
|
|
59
|
+
admittedDispatches += 1;
|
|
60
|
+
let delivery;
|
|
61
|
+
let activeDispatchId;
|
|
62
|
+
try {
|
|
63
|
+
const leaseId = createLeaseId();
|
|
64
|
+
const claimStartedAt = performance.now();
|
|
65
|
+
const dispatch = await dispatchStore.claimNextPending({
|
|
66
|
+
leaseId,
|
|
67
|
+
leaseDurationMs,
|
|
68
|
+
excludedDispatchIds: [...activeDispatches],
|
|
69
|
+
});
|
|
70
|
+
if (dispatch === null)
|
|
71
|
+
return false;
|
|
72
|
+
// A concurrent claim can outlive both its exclusion snapshot and the old
|
|
73
|
+
// lease. Do not start another delivery while its original I/O is pending.
|
|
74
|
+
if (activeDispatches.has(dispatch.id))
|
|
75
|
+
return true;
|
|
76
|
+
const remaining = leaseDurationMs - (performance.now() - claimStartedAt);
|
|
77
|
+
if (remaining <= 0)
|
|
78
|
+
return true;
|
|
79
|
+
activeDispatchId = dispatch.id;
|
|
80
|
+
activeDispatches.add(dispatch.id);
|
|
81
|
+
try {
|
|
82
|
+
await runSidecarOperation("Workflow dispatch", Math.ceil(remaining), (signal) => {
|
|
83
|
+
delivery = deliver(dispatch, leaseId, signal);
|
|
84
|
+
return delivery;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!(error instanceof SidecarOperationTimeoutError))
|
|
89
|
+
throw error;
|
|
90
|
+
// The durable lease expires independently. Do not start another database
|
|
91
|
+
// write here: a stuck retry write must not occupy the freed drain either.
|
|
92
|
+
logger.warn `Dispatch ${dispatch.id} stopped at its delivery deadline`;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
const release = () => {
|
|
98
|
+
admittedDispatches -= 1;
|
|
99
|
+
if (activeDispatchId !== undefined)
|
|
100
|
+
activeDispatches.delete(activeDispatchId);
|
|
101
|
+
};
|
|
102
|
+
// Timeouts free the drain, but the underlying I/O retains its admission
|
|
103
|
+
// reservation and dispatch exclusion until it actually settles.
|
|
104
|
+
if (delivery === undefined)
|
|
105
|
+
release();
|
|
106
|
+
else
|
|
107
|
+
void delivery.then(release, release);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function deliver(dispatch, leaseId, signal) {
|
|
55
111
|
const allocation = await allocationStore.findByAnchorRunId(dispatch.anchorRunId);
|
|
112
|
+
signal.throwIfAborted();
|
|
56
113
|
if (allocation === null) {
|
|
57
114
|
await retry(dispatch, leaseId, "allocation_missing", `No sidecar allocation exists for workflow anchor ${dispatch.anchorRunId}`);
|
|
58
|
-
return
|
|
115
|
+
return;
|
|
59
116
|
}
|
|
60
117
|
const target = targetForReadyAllocation(allocation);
|
|
61
118
|
if (target === null) {
|
|
62
119
|
await retry(dispatch, leaseId, "allocation_not_ready", `Sidecar allocation ${allocation.id} is not ready for delivery`);
|
|
63
|
-
return
|
|
120
|
+
return;
|
|
64
121
|
}
|
|
65
122
|
const agentAddress = await resolveAnchorAddress(dispatch.anchorRunId);
|
|
123
|
+
signal.throwIfAborted();
|
|
66
124
|
if (agentAddress === null) {
|
|
67
125
|
await retry(dispatch, leaseId, "anchor_address_missing", `Workflow anchor ${dispatch.anchorRunId} has no routing address`);
|
|
68
|
-
return
|
|
126
|
+
return;
|
|
69
127
|
}
|
|
70
128
|
try {
|
|
71
129
|
if (dispatch.kind === "signal") {
|
|
72
|
-
const
|
|
130
|
+
const frame = SignalDeliverFrame.assert(JSON.parse(new TextDecoder().decode(dispatch.rawMessage)));
|
|
73
131
|
await router.sendSignalDeliverToAllocation(target, {
|
|
74
|
-
agentAddress:
|
|
75
|
-
runId:
|
|
76
|
-
signalName:
|
|
77
|
-
signalId:
|
|
78
|
-
payload:
|
|
79
|
-
});
|
|
132
|
+
agentAddress: frame.agentAddress,
|
|
133
|
+
runId: frame.runId,
|
|
134
|
+
signalName: frame.signalName,
|
|
135
|
+
signalId: frame.signalId,
|
|
136
|
+
payload: frame.payload,
|
|
137
|
+
}, signal);
|
|
80
138
|
}
|
|
81
139
|
else {
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
140
|
+
// A deliverable mail dispatch always carries the sender persisted at
|
|
141
|
+
// enqueue (the workflow_run_dispatch mail-sender check enforces it).
|
|
142
|
+
// A null here means the row bypassed that invariant, so fail loudly
|
|
143
|
+
// rather than deliver with no authenticated sender or fall back to the
|
|
144
|
+
// MIME From.
|
|
145
|
+
if (dispatch.senderAddress === null) {
|
|
146
|
+
throw new Error(`mail dispatch ${dispatch.id} has no persisted authenticated sender`);
|
|
147
|
+
}
|
|
148
|
+
await router.sendWorkflowRunDispatchToAllocation(target, agentAddress, deriveWorkflowRunId(agentAddress), dispatch.stepGrants, base64Encode(dispatch.rawMessage), dispatch.senderAddress, dispatch.messageId, signal);
|
|
86
149
|
}
|
|
87
150
|
// Keep the delivery lease until the sidecar acknowledges its durable
|
|
88
151
|
// inbox write. If that ack never arrives, lease expiry makes the same
|
|
89
152
|
// immutable payload claimable again.
|
|
90
153
|
}
|
|
91
154
|
catch (error) {
|
|
155
|
+
signal.throwIfAborted();
|
|
92
156
|
await retry(dispatch, leaseId, "dispatch_unroutable", error instanceof Error ? error.message : String(error));
|
|
93
157
|
}
|
|
94
|
-
return true;
|
|
95
158
|
}
|
|
96
159
|
async function reconcileUntilIdle(maxIterations = 100) {
|
|
97
160
|
let reconciled = 0;
|
package/dist/workflow-kind.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export declare const workflowDefinitionEnvelopeSchema: import("arktype/internal/
|
|
|
32
32
|
locator: "tenant";
|
|
33
33
|
name?: string;
|
|
34
34
|
}[];
|
|
35
|
+
sidecarPlacement?: {
|
|
36
|
+
capabilities?: {
|
|
37
|
+
capability: string;
|
|
38
|
+
effect: "require" | "block";
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
inboundMailPolicy?: {
|
|
42
|
+
untrustedFrom?: "reject" | "admit";
|
|
43
|
+
invalid?: "reject" | "admit";
|
|
44
|
+
missing?: "reject" | "admit";
|
|
45
|
+
unknown?: "reject" | "admit";
|
|
46
|
+
};
|
|
35
47
|
}, {}>;
|
|
36
48
|
export declare const workflowKindHandler: KindHandler;
|
|
37
49
|
export declare const workflowAuthorize: AuthorizeFn;
|
package/dist/workflow-kind.js
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
// used by skill assets.
|
|
24
24
|
import { type } from "arktype";
|
|
25
25
|
import { getLogger } from "@intx/log";
|
|
26
|
-
import { CredentialBinding, GrantRequirement } from "@intx/types";
|
|
26
|
+
import { CredentialBinding, GrantRequirement, SidecarCapabilityPolicy, } from "@intx/types";
|
|
27
|
+
import { InboundMailPolicy } from "@intx/types/runtime";
|
|
27
28
|
import { PackageJSON, isContainedEntryPath } from "@intx/types/package-json";
|
|
28
|
-
import {
|
|
29
|
-
import { UserPrincipal, } from "./repo-store/index.js";
|
|
29
|
+
import { authorizeUserPrincipal, } from "./repo-store/index.js";
|
|
30
30
|
const logger = getLogger(["hub-sessions", "workflow-kind"]);
|
|
31
31
|
export const WORKFLOW_JSON_PATH = "workflow.json";
|
|
32
32
|
export const CAPABILITY_DECLARATIONS_JSON_PATH = "capability-declarations.json";
|
|
@@ -78,6 +78,13 @@ export const workflowDefinitionEnvelopeSchema = type({
|
|
|
78
78
|
// authority, or handle) is rejected at the deploy boundary rather than
|
|
79
79
|
// passed through to launch-time resolution unchecked.
|
|
80
80
|
"credentialBindings?": CredentialBinding.array(),
|
|
81
|
+
"sidecarPlacement?": SidecarCapabilityPolicy,
|
|
82
|
+
// `inboundMailPolicy` is validated here too -- same defense-in-depth
|
|
83
|
+
// rationale as credentialBindings above: a malformed policy (an unknown
|
|
84
|
+
// outcome key or a value that is not reject/admit) is rejected at the deploy
|
|
85
|
+
// boundary rather than passed through to later admission resolution
|
|
86
|
+
// unchecked.
|
|
87
|
+
"inboundMailPolicy?": InboundMailPolicy,
|
|
81
88
|
}).onUndeclaredKey("ignore");
|
|
82
89
|
const SidecarPrincipal = type({
|
|
83
90
|
kind: "'sidecar'",
|
|
@@ -241,63 +248,13 @@ export const workflowAuthorize = (principal, repoId, ref, action) => {
|
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
if (principal.kind === "user") {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const parsed = UserPrincipal(principal);
|
|
252
|
-
if (parsed instanceof type.errors) {
|
|
253
|
-
return {
|
|
254
|
-
allowed: false,
|
|
255
|
-
reason: `user principal is malformed: ${parsed.summary}`,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (!parsed.tokenClaims.actions.includes(action)) {
|
|
259
|
-
return {
|
|
260
|
-
allowed: false,
|
|
261
|
-
reason: `token does not grant action ${action}`,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
// `ref === "*"` is the substrate's sentinel for the bulk read
|
|
265
|
-
// performed by `listRefs`. Per-ref filtering is the advertise-refs
|
|
266
|
-
// layer's responsibility, so the bulk read is gated on action and
|
|
267
|
-
// expiry alone.
|
|
268
|
-
if (ref !== "*" && !glob.match(parsed.tokenClaims.refPattern, ref)) {
|
|
269
|
-
return {
|
|
270
|
-
allowed: false,
|
|
271
|
-
reason: `token refPattern ${parsed.tokenClaims.refPattern} does not match ${ref}`,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
if (Date.now() >= parsed.tokenClaims.expiresAt) {
|
|
275
|
-
return {
|
|
276
|
-
allowed: false,
|
|
277
|
-
reason: `token expired at ${parsed.tokenClaims.expiresAt}`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
const expectedResource = `asset:${repoId.id}`;
|
|
281
|
-
if (parsed.authz.resource !== expectedResource) {
|
|
282
|
-
return {
|
|
283
|
-
allowed: false,
|
|
284
|
-
reason: `authz verdict resource ${parsed.authz.resource} does not match ${expectedResource}`,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
const expectedGrantVerb = repoActionToGrantVerb(action);
|
|
288
|
-
if (parsed.authz.grantVerb !== expectedGrantVerb) {
|
|
289
|
-
return {
|
|
290
|
-
allowed: false,
|
|
291
|
-
reason: `authz verdict grantVerb ${parsed.authz.grantVerb} does not match ${expectedGrantVerb}`,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
if (parsed.authz.effect === "allow") {
|
|
295
|
-
return { allowed: true };
|
|
296
|
-
}
|
|
297
|
-
return {
|
|
298
|
-
allowed: false,
|
|
299
|
-
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
300
|
-
};
|
|
251
|
+
return authorizeUserPrincipal({
|
|
252
|
+
principal,
|
|
253
|
+
repoId,
|
|
254
|
+
ref,
|
|
255
|
+
action,
|
|
256
|
+
resourcePrefix: "asset",
|
|
257
|
+
});
|
|
301
258
|
}
|
|
302
259
|
// Fail closed on any kind not handled above. The tenant-level
|
|
303
260
|
// `workflow` principal kind (`@intx/types` principalKinds) is a
|
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
import type { DBExecutor } from "@intx/db";
|
|
2
|
-
import type { GrantWalkSnapshot } from "@intx/types";
|
|
2
|
+
import type { ApprovalItem, GrantRequirement, GrantWalkSnapshot } from "@intx/types";
|
|
3
3
|
import type { PackumentFetcher, RegistryConfig } from "@intx/tool-packaging";
|
|
4
4
|
import type { WorkflowProjectionDefinition } from "@intx/types/sidecar";
|
|
5
5
|
import type { ToolPackageManifest } from "@intx/types/tool-packages";
|
|
6
6
|
import type { WorkflowDefinitionAssetSource, WorkflowDefinitionRegistrySource } from "@intx/types/workflow-sources";
|
|
7
|
-
import type
|
|
7
|
+
import { type ApprovalSet } from "@intx/workflow-deploy";
|
|
8
8
|
import { type ResolveAssetAttachmentFn } from "./workflow-closure-resolution.js";
|
|
9
9
|
import type { SourceTreeReads } from "./workflow-source-closure.js";
|
|
10
|
-
import type {
|
|
10
|
+
import type { SendProbeArgs, WorkflowProbeResult } from "./ws/sidecar-handler.js";
|
|
11
|
+
type WorkflowProbeRouter = {
|
|
12
|
+
sendProbe(args: SendProbeArgs): Promise<WorkflowProbeResult>;
|
|
13
|
+
};
|
|
11
14
|
/**
|
|
12
15
|
* The frozen record an approval writes: the definition's asset selector, the
|
|
13
|
-
* approved wire hash (the freeze anchor), the approved
|
|
14
|
-
* grant-walk snapshot the run path materializes grants from. The
|
|
15
|
-
* deterministic projection of the content the hash addresses and
|
|
16
|
-
* deploy hand-off in memory; the snapshot is persisted onto the
|
|
17
|
-
* run derives its grants from the frozen walk without
|
|
18
|
-
* the workflow's `workflow.json`.
|
|
16
|
+
* approved wire hash (the freeze anchor), the approved surface, and the
|
|
17
|
+
* grant-walk snapshot the run path materializes grants from. The approved
|
|
18
|
+
* surface is a deterministic projection of the content the hash addresses and
|
|
19
|
+
* rides the deploy hand-off in memory; the snapshot is persisted onto the
|
|
20
|
+
* version row so a run derives its grants from the frozen walk without
|
|
21
|
+
* re-reading and re-walking the workflow's `workflow.json`.
|
|
22
|
+
*
|
|
23
|
+
* `approvedGrants` carries both kinds of approved item: the walk's grant-shape
|
|
24
|
+
* strings and the definition's declared grant requirements. The requirements
|
|
25
|
+
* belong in the same record because the run path mints real grant rows from
|
|
26
|
+
* them, so an account of the approval that listed only the walk strings would
|
|
27
|
+
* under-report the authority the definition will actually carry. It is the flat
|
|
28
|
+
* `ApprovalItem` list rather than the gate's partitioned `ApprovalSet` because
|
|
29
|
+
* this record is the input to persistence, and the persisted form is flat.
|
|
19
30
|
*/
|
|
20
31
|
export type FrozenApproval = {
|
|
21
32
|
readonly assetId: string;
|
|
22
33
|
readonly approvedWireHash: string;
|
|
23
|
-
readonly approvedGrants: readonly
|
|
34
|
+
readonly approvedGrants: readonly ApprovalItem[];
|
|
24
35
|
readonly grantSnapshot: GrantWalkSnapshot;
|
|
25
36
|
};
|
|
26
37
|
/**
|
|
@@ -33,15 +44,17 @@ export type PersistFrozenApprovalFn = (approval: FrozenApproval) => Promise<{
|
|
|
33
44
|
}>;
|
|
34
45
|
/**
|
|
35
46
|
* 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
|
|
47
|
+
* approval the deploy hand-off consumes. The `ok: false` arms name the five
|
|
37
48
|
* fail-closed paths: a shipped hash that does not match the hub recompute
|
|
38
|
-
* (tamper-evidence),
|
|
49
|
+
* (tamper-evidence), advisory grants the operator did not approve, declared
|
|
50
|
+
* grant requirements the operator did not approve, a trigger type the runtime
|
|
51
|
+
* does not implement, and an executable step the grant walk left no record for.
|
|
39
52
|
*/
|
|
40
53
|
export type ProbeGateResult = {
|
|
41
54
|
readonly ok: true;
|
|
42
55
|
readonly definitionId: string;
|
|
43
56
|
readonly approvedWireHash: string;
|
|
44
|
-
readonly
|
|
57
|
+
readonly approvedSurface: ApprovalSet;
|
|
45
58
|
/**
|
|
46
59
|
* The inert wire projection the freeze hashed. Rides the ok-arm so the
|
|
47
60
|
* deploy hand-off carries the exact content the frozen hash addresses,
|
|
@@ -57,6 +70,54 @@ export type ProbeGateResult = {
|
|
|
57
70
|
readonly ok: false;
|
|
58
71
|
readonly reason: "grants_not_approved";
|
|
59
72
|
readonly unapprovedGrants: readonly string[];
|
|
73
|
+
} | {
|
|
74
|
+
readonly ok: false;
|
|
75
|
+
readonly reason: "grant_requirements_not_approved";
|
|
76
|
+
/**
|
|
77
|
+
* The declared requirements the operator's approval does not cover, in
|
|
78
|
+
* the order the probe declared them. Named in full so the operator can
|
|
79
|
+
* see exactly which authority the definition asked to delegate.
|
|
80
|
+
*/
|
|
81
|
+
readonly unapprovedGrantRequirements: readonly GrantRequirement[];
|
|
82
|
+
} | {
|
|
83
|
+
readonly ok: false;
|
|
84
|
+
readonly reason: "unimplemented_trigger";
|
|
85
|
+
/** The distinct reserved-but-unimplemented trigger types the projection declared. */
|
|
86
|
+
readonly unimplementedTriggerTypes: readonly string[];
|
|
87
|
+
} | {
|
|
88
|
+
readonly ok: false;
|
|
89
|
+
readonly reason: "steps_without_grant_record";
|
|
90
|
+
/** Every executable step the grant-walk snapshot accounts for nothing at. */
|
|
91
|
+
readonly stepsWithoutGrantRecord: readonly StepWithoutGrantRecord[];
|
|
92
|
+
/**
|
|
93
|
+
* One sentence naming every miss. A deploy that trips this is a defect in
|
|
94
|
+
* the deploy path rather than in the author's workflow, so the sentence
|
|
95
|
+
* carries what whoever maintains that path needs: the step, the position,
|
|
96
|
+
* and the record that was absent.
|
|
97
|
+
*/
|
|
98
|
+
readonly message: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* One step the deployment can execute that the probe's grant-walk snapshot
|
|
102
|
+
* carries no record for.
|
|
103
|
+
*/
|
|
104
|
+
export type StepWithoutGrantRecord = {
|
|
105
|
+
/** The executable step id with no approved grants behind it. */
|
|
106
|
+
readonly stepId: string;
|
|
107
|
+
/**
|
|
108
|
+
* The chain of step ids the executable walk reached `stepId` through,
|
|
109
|
+
* outermost first and `stepId` itself last. Two nested bodies may
|
|
110
|
+
* legitimately carry the same step id, so the chain -- not the id alone --
|
|
111
|
+
* is what names the position in the closure.
|
|
112
|
+
*/
|
|
113
|
+
readonly reachedThrough: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* The top-level step whose snapshot record was supposed to account for
|
|
116
|
+
* `stepId`. The capability walk folds every nested body's grants into the
|
|
117
|
+
* record of the top-level step that carries the body, so this names the
|
|
118
|
+
* `perStep` key the absent record would have had.
|
|
119
|
+
*/
|
|
120
|
+
readonly recordStepId: string;
|
|
60
121
|
};
|
|
61
122
|
/**
|
|
62
123
|
* Build the production persistence step of the freeze. Records identity through
|
|
@@ -71,7 +132,8 @@ export declare function createDbFrozenApprovalWriter(db: DBExecutor): PersistFro
|
|
|
71
132
|
/**
|
|
72
133
|
* Approve exactly the grant surface the probe reports, without a pre-walked
|
|
73
134
|
* operator `ApprovalSet` to gate against. Under this mode the gate skips the
|
|
74
|
-
* per-grant membership
|
|
135
|
+
* per-grant and per-requirement membership checks and freezes exactly what the
|
|
136
|
+
* probe advertised.
|
|
75
137
|
*
|
|
76
138
|
* This is the code-sourced analogue of the live-authored self-approve: the hub
|
|
77
139
|
* has no live definition to pre-walk, so the probe's advertised grants ARE the
|
|
@@ -79,13 +141,13 @@ export declare function createDbFrozenApprovalWriter(db: DBExecutor): PersistFro
|
|
|
79
141
|
* still runs and can still fail closed.
|
|
80
142
|
*/
|
|
81
143
|
export type ApproveProbedGrants = {
|
|
82
|
-
readonly
|
|
144
|
+
readonly kind: "approve-probed";
|
|
83
145
|
};
|
|
84
146
|
/**
|
|
85
|
-
* How the gate turns the probe's
|
|
86
|
-
* Either an explicit operator `ApprovalSet` -- every advertised grant
|
|
87
|
-
* appear in it or the gate fails closed -- or
|
|
88
|
-
* exactly the surface the probe reported.
|
|
147
|
+
* How the gate turns the probe's advertised surface into an approved one.
|
|
148
|
+
* Either an explicit operator `ApprovalSet` -- every advertised grant and every
|
|
149
|
+
* declared grant requirement must appear in it or the gate fails closed -- or
|
|
150
|
+
* `approve-probed`, which approves exactly the surface the probe reported.
|
|
89
151
|
*/
|
|
90
152
|
export type ProbeApprovalPolicy = ApprovalSet | ApproveProbedGrants;
|
|
91
153
|
export type GateAndFreezeArgs = {
|
|
@@ -94,9 +156,9 @@ export type GateAndFreezeArgs = {
|
|
|
94
156
|
/** The sidecar's inert probe answer: projection, advisory grants, shipped hash. */
|
|
95
157
|
readonly probeResult: WorkflowProbeResult;
|
|
96
158
|
/**
|
|
97
|
-
* The approval policy. An `ApprovalSet` gates the advisory set
|
|
98
|
-
*
|
|
99
|
-
* the surface the probe reported.
|
|
159
|
+
* The approval policy. An `ApprovalSet` gates the advisory set and the
|
|
160
|
+
* declared grant requirements against the operator-approved items;
|
|
161
|
+
* `approve-probed` approves exactly the surface the probe reported.
|
|
100
162
|
*/
|
|
101
163
|
readonly approvals: ProbeApprovalPolicy;
|
|
102
164
|
/** Persistence step for the freeze; `createDbFrozenApprovalWriter` in production. */
|
|
@@ -107,11 +169,19 @@ export type GateAndFreezeArgs = {
|
|
|
107
169
|
* inert projection and grant set -- no author code runs here and the capability
|
|
108
170
|
* walk is never re-run.
|
|
109
171
|
*
|
|
110
|
-
* Fails closed on the
|
|
172
|
+
* Fails closed on the three security-load-bearing checks before it writes
|
|
111
173
|
* anything: the recomputed wire hash must match the hash the sidecar shipped
|
|
112
|
-
* (tamper-evidence),
|
|
113
|
-
*
|
|
114
|
-
*
|
|
174
|
+
* (tamper-evidence), every advisory grant must be operator-approved, and every
|
|
175
|
+
* declared grant requirement must be operator-approved. It also refuses a
|
|
176
|
+
* projection whose triggers include a reserved-but-unimplemented type -- not a
|
|
177
|
+
* security check, but the layer a pinned closure cannot carry a stale copy of,
|
|
178
|
+
* so it is where a workflow that could only sit inert is caught -- and one
|
|
179
|
+
* whose executable closure reaches a step the grant walk left no record for --
|
|
180
|
+
* the layer holding both halves of the probe answer at once, and a presence
|
|
181
|
+
* check on those records rather than a check that any record's contents are
|
|
182
|
+
* sufficient.
|
|
183
|
+
* Only then does it freeze the recomputed hash onto the version row and return
|
|
184
|
+
* the approved grant set.
|
|
115
185
|
*/
|
|
116
186
|
export declare function gateAndFreezeProbeResult(args: GateAndFreezeArgs): Promise<ProbeGateResult>;
|
|
117
187
|
type InstallAndApproveCommonArgs = {
|
|
@@ -126,9 +196,11 @@ type InstallAndApproveCommonArgs = {
|
|
|
126
196
|
*/
|
|
127
197
|
readonly approvals: ProbeApprovalPolicy;
|
|
128
198
|
/** The sidecar router carrying the probe transport. */
|
|
129
|
-
readonly router:
|
|
199
|
+
readonly router: WorkflowProbeRouter;
|
|
130
200
|
/** Executor the freeze writes through. */
|
|
131
201
|
readonly db: DBExecutor;
|
|
202
|
+
/** Optional durable handoff invoked before the approval gate writes. */
|
|
203
|
+
readonly onProbeResult?: (result: WorkflowProbeResult) => Promise<void>;
|
|
132
204
|
};
|
|
133
205
|
/** Install a definition published to an npm registry. */
|
|
134
206
|
export type InstallAndApproveRegistryArgs = InstallAndApproveCommonArgs & {
|