@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
package/dist/event-collector.js
CHANGED
|
@@ -10,7 +10,7 @@ import { parseTurnPartType } from "@intx/db";
|
|
|
10
10
|
import { generateId } from "@intx/hub-common";
|
|
11
11
|
const log = getLogger(["hub", "event-collector"]);
|
|
12
12
|
export function createEventCollector(config) {
|
|
13
|
-
const { db, sessionId, runId, tenantId, onTurnFinalized } = config;
|
|
13
|
+
const { db, sessionId, runId, tenantId, onTurnFinalized, onUsage } = config;
|
|
14
14
|
// Current inference turn being accumulated. A new turn is created on each
|
|
15
15
|
// inference.start. Finalized on connector.reply, reactor.done,
|
|
16
16
|
// reactor.error (fatal), or abandon. Null when no turn is active.
|
|
@@ -47,6 +47,12 @@ export function createEventCollector(config) {
|
|
|
47
47
|
let accumulatedToolCalls = [];
|
|
48
48
|
// Tool results that reported isError, accumulated for TurnFinalized.
|
|
49
49
|
let accumulatedToolErrors = [];
|
|
50
|
+
// Final cumulative token usage for the current turn. `inference.usage` events
|
|
51
|
+
// carry a running cumulative total and fire multiple times per step, so these
|
|
52
|
+
// are OVERWRITTEN (not summed); the last value before finalize is the
|
|
53
|
+
// authoritative per-turn total. Both reset on each new turn.
|
|
54
|
+
let turnUsage = null;
|
|
55
|
+
let turnSource = null;
|
|
50
56
|
async function onEvent(event) {
|
|
51
57
|
switch (event.type) {
|
|
52
58
|
case "inference.start":
|
|
@@ -59,6 +65,8 @@ export function createEventCollector(config) {
|
|
|
59
65
|
case "inference.done":
|
|
60
66
|
await handleInferenceDone(event.data.turn.content);
|
|
61
67
|
streamingText = "";
|
|
68
|
+
turnUsage = event.data.usage;
|
|
69
|
+
turnSource = event.data.source;
|
|
62
70
|
break;
|
|
63
71
|
case "tool.done": {
|
|
64
72
|
const callId = event.data.result.callId;
|
|
@@ -145,9 +153,15 @@ export function createEventCollector(config) {
|
|
|
145
153
|
});
|
|
146
154
|
}
|
|
147
155
|
break;
|
|
156
|
+
case "inference.usage":
|
|
157
|
+
// Cumulative running total that fires several times per step; overwrite
|
|
158
|
+
// so the last value before finalize is the authoritative per-turn
|
|
159
|
+
// usage, emitted once via onUsage from finalizeTurn.
|
|
160
|
+
turnUsage = event.data.usage;
|
|
161
|
+
turnSource = event.data.source;
|
|
162
|
+
break;
|
|
148
163
|
default:
|
|
149
|
-
// reactor.start, streaming deltas,
|
|
150
|
-
// not persisted.
|
|
164
|
+
// reactor.start, streaming deltas, and other events are not persisted.
|
|
151
165
|
break;
|
|
152
166
|
}
|
|
153
167
|
}
|
|
@@ -170,6 +184,8 @@ export function createEventCollector(config) {
|
|
|
170
184
|
callArgs.clear();
|
|
171
185
|
accumulatedToolCalls = [];
|
|
172
186
|
accumulatedToolErrors = [];
|
|
187
|
+
turnUsage = null;
|
|
188
|
+
turnSource = null;
|
|
173
189
|
await db.insert(inferenceTurn).values({
|
|
174
190
|
id: currentTurnId,
|
|
175
191
|
sessionId,
|
|
@@ -318,6 +334,23 @@ export function createEventCollector(config) {
|
|
|
318
334
|
toolErrors: [...accumulatedToolErrors],
|
|
319
335
|
});
|
|
320
336
|
}
|
|
337
|
+
// Report per-turn usage once, alongside the finalize notify. Gated on
|
|
338
|
+
// turnUsage being present so a turn that ran no inference emits nothing.
|
|
339
|
+
// NOTE: abandon() finalizes with notify=false, so a turn abandoned
|
|
340
|
+
// mid-step (e.g. a sidecar disconnect) reports no usage even if the
|
|
341
|
+
// provider already billed input tokens -- an accepted gap until durable
|
|
342
|
+
// usage persistence lands on the turn row.
|
|
343
|
+
if (notify && onUsage && turnUsage !== null && turnSource !== null) {
|
|
344
|
+
onUsage({
|
|
345
|
+
tenantId,
|
|
346
|
+
sessionId,
|
|
347
|
+
runId,
|
|
348
|
+
turnId,
|
|
349
|
+
provider: turnSource.provider,
|
|
350
|
+
model: turnSource.model,
|
|
351
|
+
usage: turnUsage,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
321
354
|
currentTurnId = null;
|
|
322
355
|
}
|
|
323
356
|
async function abandon() {
|
|
@@ -5,7 +5,7 @@ export type HubSessionLookupsDeps = {
|
|
|
5
5
|
db: DB["db"];
|
|
6
6
|
agentRepoStore: AgentRepoStore;
|
|
7
7
|
};
|
|
8
|
-
export declare function createHubSessionLookups(deps: HubSessionLookupsDeps): Required<Omit<SidecarLookups, "materializeMailTriggeredRunGrants">>;
|
|
8
|
+
export declare function createHubSessionLookups(deps: HubSessionLookupsDeps): Required<Omit<SidecarLookups, "materializeMailTriggeredRunGrants" | "resyncCredentials" | "resolveSenderKey" | "resolveSenderKeyStrict">>;
|
|
9
9
|
/**
|
|
10
10
|
* Extract the run id from an `<runId>@<domain>` run address.
|
|
11
11
|
* Throws on any input the `@intx/types`-owned `parseRunAddress`
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Each lookup is a stateless DB or repo call. They are gathered into a
|
|
6
6
|
// single struct that the hub app passes to `createSidecarRouter` as
|
|
7
7
|
// `lookups`.
|
|
8
|
-
import { eq, and, asc, inArray, isNull } from "drizzle-orm";
|
|
8
|
+
import { eq, and, asc, inArray, isNotNull, isNull } from "drizzle-orm";
|
|
9
9
|
import { createApprovalStore, createSignalCorrelationStore, createWorkflowRunDispatchStore, createWorkflowRunStore, } from "@intx/db";
|
|
10
10
|
import { agentSession, liveWorkflowRunStatuses, principal, sessionMail, sidecarAllocation, workflowRun, } from "@intx/db/schema";
|
|
11
11
|
import { getLogger } from "@intx/log";
|
|
@@ -23,25 +23,6 @@ export function createHubSessionLookups(deps) {
|
|
|
23
23
|
const workflowRunStore = createWorkflowRunStore(db);
|
|
24
24
|
const workflowRunDispatchStore = createWorkflowRunDispatchStore(db);
|
|
25
25
|
return {
|
|
26
|
-
async lookupPublicKey(agentAddress) {
|
|
27
|
-
// Every routable address names one workflow run, whose key lives on its
|
|
28
|
-
// single self-anchored workflow_run row, keyed by address. Read the key
|
|
29
|
-
// off that row, gated on a live run (born "deployed", "running" after its
|
|
30
|
-
// first trigger) so a decommissioned deployment's key can no longer
|
|
31
|
-
// satisfy a challenge. The "deployed" arm is load-bearing: the reconnect
|
|
32
|
-
// ownership challenge fires in the deploy->first-trigger window, so a
|
|
33
|
-
// "running"-only gate would fail every such challenge closed. A missing
|
|
34
|
-
// row or a null publicKey (live but not yet acked) returns null so the
|
|
35
|
-
// reconnect challenge fails closed and the address stays unrouted rather
|
|
36
|
-
// than routing without ownership proof.
|
|
37
|
-
const row = await db
|
|
38
|
-
.select({ publicKey: workflowRun.publicKey })
|
|
39
|
-
.from(workflowRun)
|
|
40
|
-
.where(and(eq(workflowRun.address, agentAddress), inArray(workflowRun.status, [...liveWorkflowRunStatuses])))
|
|
41
|
-
.limit(1)
|
|
42
|
-
.then((rows) => rows[0]);
|
|
43
|
-
return row?.publicKey ?? null;
|
|
44
|
-
},
|
|
45
26
|
async lookupDeployRef() {
|
|
46
27
|
// A workflow run is a supervised workflow-process pinned forever like a
|
|
47
28
|
// native deployment: it keeps its deploy-time definition and never
|
|
@@ -269,9 +250,11 @@ export function createHubSessionLookups(deps) {
|
|
|
269
250
|
id: workflowRun.id,
|
|
270
251
|
address: workflowRun.address,
|
|
271
252
|
anchorRunId: workflowRun.anchorRunId,
|
|
253
|
+
tenantId: workflowRun.tenantId,
|
|
254
|
+
definitionId: workflowRun.definitionId,
|
|
272
255
|
})
|
|
273
256
|
.from(workflowRun)
|
|
274
|
-
.where(and(eq(workflowRun.address, source.agentAddress), inArray(workflowRun.status, [...liveWorkflowRunStatuses])))
|
|
257
|
+
.where(and(eq(workflowRun.address, source.agentAddress), inArray(workflowRun.status, [...liveWorkflowRunStatuses]), isNotNull(workflowRun.definitionId)))
|
|
275
258
|
.limit(1);
|
|
276
259
|
if (anchor === undefined ||
|
|
277
260
|
anchor.anchorRunId !== anchor.id ||
|
|
@@ -282,43 +265,30 @@ export function createHubSessionLookups(deps) {
|
|
|
282
265
|
const anchorAddress = anchor.address;
|
|
283
266
|
let newlyTerminalRuns;
|
|
284
267
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
// Replacement advances this same row. Keep its lock until the
|
|
303
|
-
// repository ref has advanced so ownership cannot change after
|
|
304
|
-
// validation but before the old worker's pack becomes
|
|
305
|
-
// authoritative.
|
|
306
|
-
return agentRepoStore.receiveWorkflowRunPack({ kind: "workflow-run", id: workflowRunRepoId }, pack, ref, commitSha);
|
|
307
|
-
});
|
|
308
|
-
if (newlyTerminalRuns === null) {
|
|
309
|
-
logger.warn `Workflow-run pack rejected for ${workflowRunRepoId}: source connection does not own the deployment's current allocation`;
|
|
310
|
-
return { accepted: false, reason: "path_violation" };
|
|
268
|
+
newlyTerminalRuns = await db.transaction(async (tx) => {
|
|
269
|
+
const [allocation] = await tx
|
|
270
|
+
.select()
|
|
271
|
+
.from(sidecarAllocation)
|
|
272
|
+
.where(eq(sidecarAllocation.anchorRunId, anchor.id))
|
|
273
|
+
.limit(1)
|
|
274
|
+
.for("update");
|
|
275
|
+
if (allocation === undefined ||
|
|
276
|
+
allocation.id !== source.allocationId ||
|
|
277
|
+
allocation.anchorRunId !== source.anchorRunId ||
|
|
278
|
+
source.anchorRunId !== anchor.id ||
|
|
279
|
+
allocation.status !== "allocated" ||
|
|
280
|
+
allocation.generation !== source.generation ||
|
|
281
|
+
allocation.ensureAcceptedGeneration !== source.generation) {
|
|
282
|
+
return null;
|
|
311
283
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
newlyTerminalRuns = await agentRepoStore.receiveWorkflowRunPack({ kind: "workflow-run", id: workflowRunRepoId }, pack, ref, commitSha);
|
|
284
|
+
// Replacement advances this same row. Keep its lock until the
|
|
285
|
+
// repository ref has advanced so ownership cannot change after
|
|
286
|
+
// validation but before the old worker's pack becomes authoritative.
|
|
287
|
+
return agentRepoStore.receiveWorkflowRunPack({ kind: "workflow-run", id: workflowRunRepoId }, pack, ref, commitSha);
|
|
288
|
+
});
|
|
289
|
+
if (newlyTerminalRuns === null) {
|
|
290
|
+
logger.warn `Workflow-run pack rejected for ${workflowRunRepoId}: source connection does not own the deployment's current allocation`;
|
|
291
|
+
return { accepted: false, reason: "path_violation" };
|
|
322
292
|
}
|
|
323
293
|
}
|
|
324
294
|
catch (err) {
|
|
@@ -352,6 +322,39 @@ export function createHubSessionLookups(deps) {
|
|
|
352
322
|
for (const { runId, status } of newlyTerminalRuns) {
|
|
353
323
|
try {
|
|
354
324
|
await db.transaction(async (tx) => {
|
|
325
|
+
// Lazily anchor the run before settling it. An internal run that
|
|
326
|
+
// parks only on a plain signal gate never reaches
|
|
327
|
+
// `registerSignalCorrelation`, the sole other path that mints an
|
|
328
|
+
// internal run row, so its terminal event can be the first the hub
|
|
329
|
+
// sees of the run. A never-minted row is ordinary bookkeeping, not
|
|
330
|
+
// a deployment-boundary violation, so mint it here against this
|
|
331
|
+
// deployment's anchor rather than letting the ownership guard below
|
|
332
|
+
// mistake absence for foreignness. The insert no-ops when any row
|
|
333
|
+
// already exists, which keeps that guard authoritative for a row
|
|
334
|
+
// that exists and anchors elsewhere. The principal is null: an
|
|
335
|
+
// internal run inherits its deployment's grants and has none of its
|
|
336
|
+
// own.
|
|
337
|
+
//
|
|
338
|
+
// The mint necessarily precedes the ownership guard, so an id the
|
|
339
|
+
// hub has never seen is claimed under THIS anchor before anything
|
|
340
|
+
// establishes it belongs here. That ordering is required -- the
|
|
341
|
+
// guard reads the row the mint may have to create -- and it is
|
|
342
|
+
// bounded rather than unbounded: internal run ids are supplied by
|
|
343
|
+
// the sidecar and accepted verbatim, so the value is
|
|
344
|
+
// caller-influenced, but it is a different population from the
|
|
345
|
+
// anchor ids the hub mints itself, and nothing resolves an
|
|
346
|
+
// internal id without also constraining the anchor or the tenant.
|
|
347
|
+
// The insert cannot take a row away from another deployment; the
|
|
348
|
+
// worst it does is create one for an id that deployment would
|
|
349
|
+
// otherwise have created later.
|
|
350
|
+
await workflowRunStore.createIfAbsent({
|
|
351
|
+
id: runId,
|
|
352
|
+
anchorRunId: anchor.id,
|
|
353
|
+
definitionId: anchor.definitionId,
|
|
354
|
+
tenantId: anchor.tenantId,
|
|
355
|
+
principalId: null,
|
|
356
|
+
status: "running",
|
|
357
|
+
}, tx);
|
|
355
358
|
const [ownedRun] = await tx
|
|
356
359
|
.select({ anchorRunId: workflowRun.anchorRunId })
|
|
357
360
|
.from(workflowRun)
|
|
@@ -363,19 +366,11 @@ export function createHubSessionLookups(deps) {
|
|
|
363
366
|
}
|
|
364
367
|
const won = await workflowRunStore.markTerminal(runId, status, now, tx);
|
|
365
368
|
if (won === null) {
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// anchor loudly rather than silently treating both as done.
|
|
372
|
-
const [existing] = await tx
|
|
373
|
-
.select({ id: workflowRun.id })
|
|
374
|
-
.from(workflowRun)
|
|
375
|
-
.where(eq(workflowRun.id, runId));
|
|
376
|
-
if (existing === undefined) {
|
|
377
|
-
logger.error `Terminal event for run ${runId} (deployment ${anchor.id}, target status ${status}) has no workflow_run row; the run terminated before its anchor committed`;
|
|
378
|
-
}
|
|
369
|
+
// The row exists (the mint above guarantees it) and belongs to
|
|
370
|
+
// this deployment (the guard above), so no running row matched
|
|
371
|
+
// only because the run is already terminal -- a benign replay
|
|
372
|
+
// against an already-settled row. Leave its settled status and
|
|
373
|
+
// `endedAt` alone.
|
|
379
374
|
return;
|
|
380
375
|
}
|
|
381
376
|
// Deactivate the run's own principal, if it has one. Externally-
|
|
@@ -656,8 +651,9 @@ export async function findRoutableById(db, id, tenantId) {
|
|
|
656
651
|
// return cannot do.
|
|
657
652
|
if (runRow === undefined ||
|
|
658
653
|
!isTopLevelRun(runRow) ||
|
|
659
|
-
runRow.address === null
|
|
654
|
+
runRow.address === null ||
|
|
655
|
+
runRow.definitionId === null) {
|
|
660
656
|
return undefined;
|
|
661
657
|
}
|
|
662
|
-
return runRowToRoutableRecord(runRow, runRow.address);
|
|
658
|
+
return runRowToRoutableRecord({ ...runRow, definitionId: runRow.definitionId }, runRow.address);
|
|
663
659
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import type { DB } from "@intx/db";
|
|
2
|
-
import type { AgentRepoStore } from "./agent-repo.js";
|
|
3
2
|
import type { EventCollectorRegistry } from "./event-collector-registry.js";
|
|
4
3
|
import type { SidecarEventEmitter } from "./ws/sidecar-events.js";
|
|
4
|
+
import type { SenderDeploySettledOutcome } from "./ws/sidecar-handler.js";
|
|
5
5
|
/** Subset of `SidecarRouter` the orchestrator drives outbound. The
|
|
6
6
|
* narrow surface keeps tests honest and decouples the orchestrator
|
|
7
7
|
* from the rest of the router API. */
|
|
8
8
|
export type HubSessionRouterFacade = {
|
|
9
|
-
sendPack(agentAddress: string, pack: Uint8Array, ref: string, commitSha: string): Promise<void>;
|
|
10
9
|
dispatchAgentEvent(agentAddress: string, event: unknown): void;
|
|
10
|
+
noteSenderDeploySettled(address: string, outcome: SenderDeploySettledOutcome): void;
|
|
11
11
|
};
|
|
12
12
|
export type HubSessionOrchestratorDeps = {
|
|
13
13
|
events: SidecarEventEmitter;
|
|
14
14
|
router: HubSessionRouterFacade;
|
|
15
15
|
db: DB["db"];
|
|
16
16
|
eventCollectors: EventCollectorRegistry;
|
|
17
|
-
agentRepoStore: AgentRepoStore;
|
|
18
17
|
};
|
|
19
18
|
export type HubSessionOrchestrator = {
|
|
20
19
|
/** Unsubscribe all listeners. Tests use this between cases; the hub
|
|
@@ -15,10 +15,9 @@ import { workflowRun } from "@intx/db/schema";
|
|
|
15
15
|
import { parseMailToEmail } from "@intx/mime";
|
|
16
16
|
import { parseInferenceEvent } from "@intx/types/runtime";
|
|
17
17
|
import { getLogger } from "@intx/log";
|
|
18
|
-
import { parseAgentId } from "./hub-session-lookups.js";
|
|
19
18
|
const log = getLogger(["hub", "orchestrator"]);
|
|
20
19
|
export function createHubSessionOrchestrator(deps) {
|
|
21
|
-
const { events, router, db, eventCollectors
|
|
20
|
+
const { events, router, db, eventCollectors } = deps;
|
|
22
21
|
const unsubscribers = [];
|
|
23
22
|
unsubscribers.push(events.on("agent.event", ({ agentAddress, event }) => {
|
|
24
23
|
const validated = parseInferenceEvent(event);
|
|
@@ -46,25 +45,27 @@ export function createHubSessionOrchestrator(deps) {
|
|
|
46
45
|
}));
|
|
47
46
|
unsubscribers.push(events.on("agent.deploy.ack", async (event) => {
|
|
48
47
|
const { agentAddress, publicKey, allocated } = event;
|
|
49
|
-
//
|
|
48
|
+
// Provisioned initialization publishes its key only after every deploy
|
|
50
49
|
// and asset pack succeeds under the allocation generation fence.
|
|
51
50
|
if (allocated !== undefined)
|
|
52
51
|
return;
|
|
53
52
|
// Every deploy address names a workflow run whose public key lives on its
|
|
54
53
|
// single self-anchored workflow_run row, keyed by address. Persist it
|
|
55
|
-
// there
|
|
56
|
-
//
|
|
57
|
-
// owns a row, so a stray
|
|
54
|
+
// there as the deployment's published identity. Reconnect routing is
|
|
55
|
+
// allocation-authenticated, so this projection is not connection
|
|
56
|
+
// authority. Only the deployment-level address owns a row, so a stray
|
|
57
|
+
// per-step ack updates nothing.
|
|
58
58
|
await db
|
|
59
59
|
.update(workflowRun)
|
|
60
60
|
.set({ publicKey })
|
|
61
61
|
.where(eq(workflowRun.address, agentAddress));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
// The key is now durable. Wake any mail the run parked while pre-ack so it
|
|
63
|
+
// is delivered with the sender key co-delivered, closing the window where
|
|
64
|
+
// a run sends before its key is recorded. The write above happens-before
|
|
65
|
+
// this settle, so a re-drive resolves the recorded key. The address is the
|
|
66
|
+
// run's own deploy address, byte-identical to the sender address its mail
|
|
67
|
+
// was sent under.
|
|
68
|
+
router.noteSenderDeploySettled(agentAddress, { recorded: publicKey });
|
|
68
69
|
}));
|
|
69
70
|
unsubscribers.push(events.on("mail.persisted", (row) => {
|
|
70
71
|
const parsed = parseMailToEmail(row.raw, row.id);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
export { createAgentRepoStore, type AgentRepoStore, type DeployContent, } from "./agent-repo.js";
|
|
2
|
-
export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, type SessionService, type DeployWorkflowDefinitionResult, type
|
|
2
|
+
export { createSessionService, recoverSenderDeploy, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, type SessionService, type DeployWorkflowDefinitionResult, type DeployPreparedCodeSourcedWorkflowParams, type InstallAndApproveWorkflowSourceParams, type PreparedWorkflowDeployer, type DeployCodeSourcedWorkflowArgs, } from "./session-service.js";
|
|
3
3
|
export { installAndApproveWorkflowDefinition, createDbFrozenApprovalWriter, type InstallAndApproveArgs, type InstallAndApproveResult, type ProbeGateResult, type ProbeApprovalPolicy, type ApproveProbedGrants, } from "./workflow-probe-gate.js";
|
|
4
4
|
export { committedReadsToSourceTree } from "./committed-source-tree.js";
|
|
5
5
|
export type { SourceTreeReads } from "./workflow-source-closure.js";
|
|
6
6
|
export type { WorkflowDefinition } from "@intx/workflow/definition";
|
|
7
7
|
export { createEventCollectorRegistry, type EventCollectorRegistry, } from "./event-collector-registry.js";
|
|
8
|
-
export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SidecarAuthIdentity, type SidecarAuthenticator, type AllocatedSidecarTarget, type SidecarAllocationRouter, createSidecarCredentialResolver, createSidecarTokenAuthenticator, type CreateSidecarTokenAuthenticatorDeps, type WsHandle, createSidecarEmitter, type SidecarEventEmitter, type SidecarEventMap, type SidecarEventType, type SidecarEventListener, type SidecarLookups, type SidecarMailPersistedPayload, type SidecarMailPersistedRow, type MailTriggeredRunGrantsResult, type WorkflowRunPackSource, } from "./ws/index.js";
|
|
8
|
+
export { createSidecarRouter, type SidecarRouter, type SidecarRouterConfig, type SenderDeploySettledOutcome, type AllocatedSenderDeployAttempt, type SidecarAuthIdentity, type SidecarAuthenticator, type AllocatedSidecarTarget, type SidecarAllocationRouter, SidecarIdentityValidationError, createSidecarCredentialResolver, createSidecarTokenAuthenticator, type CreateSidecarTokenAuthenticatorDeps, type WsHandle, createSidecarEmitter, type SidecarEventEmitter, type SidecarEventMap, type SidecarEventType, type SidecarEventListener, type SidecarLookups, type SidecarMailPersistedPayload, type SidecarMailPersistedRow, type MailTriggeredRunGrantsResult, type WorkflowRunPackSource, } from "./ws/index.js";
|
|
9
9
|
export { createHubSessionLookups, findRoutableById, parseAgentId, resolveRoutableAddress, resolveRunIdForSession, resolveRunSessionId, runRowToRoutableRecord, type HubSessionLookupsDeps, type RoutableEndpoint, type RoutableRecord, } from "./hub-session-lookups.js";
|
|
10
10
|
export { createHubSessionOrchestrator, type HubSessionOrchestrator, type HubSessionOrchestratorDeps, type HubSessionRouterFacade, } from "./hub-session-orchestrator.js";
|
|
11
|
-
export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
|
|
12
|
-
export { createSidecarPluginRegistry, createSidecarAllocationReconciler,
|
|
11
|
+
export { pushSourceUpdates, pushSourceUpdatesSubtree, pushCredentialRevoke, pushCredentialReconcile, } from "./credential-push.js";
|
|
12
|
+
export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, createSidecarAllocationReconciler, type CreateSidecarPluginRegistryOpts, type DestroySidecarRequest, type DestroySidecarResult, type EnsureSidecarRequest, type EnsureSidecarResult, type SidecarCredentialIdentity, type SidecarCredentialResolver, type SidecarOperationFailure, type SidecarPluginRegistry, type SidecarProvisioner, type SidecarProvisionerChooser, type SidecarAllocationReconciler, type SidecarAllocationReconcilerDeps, type SidecarReconciliationContext, } from "./sidecar-allocation/index.js";
|
|
13
13
|
export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
|
|
14
14
|
export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
|
|
15
|
-
export {
|
|
15
|
+
export { createReconciliationScheduler, DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, type ReconciliationSchedulerOptions, } from "./reconciliation-scheduler.js";
|
|
16
|
+
export { createWorkflowAllocationService, WorkflowProvisioningError, type PrepareProvisionedWorkflowDeploymentArgs, type PreparedProvisionedWorkflowDeployment, type WorkflowAllocationService, type WorkflowAllocationServiceDeps, } from "./workflow-allocation-service.js";
|
|
16
17
|
export { createWorkflowDispatchService, type WorkflowDispatchAcknowledgement, type WorkflowDispatchService, type WorkflowDispatchServiceDeps, } from "./workflow-dispatch-service.js";
|
|
17
18
|
export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, type AcceptedWorkflowDispatch, type ConsumedWorkflowDispatch, type ReceivedWorkflowSignal, } from "./workflow-dispatch-settlement.js";
|
|
18
19
|
export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, type SkillIndexEntry, type SkillFrontmatter, type SkillPrincipal, type SkillHubPrincipal, type SkillSidecarPrincipal, } from "./skill-kind.js";
|
|
19
20
|
export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
|
|
20
21
|
export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, type WorkflowPrincipal, type WorkflowHubPrincipal, type WorkflowSidecarPrincipal, } from "./workflow-kind.js";
|
|
21
|
-
export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed,
|
|
22
|
+
export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, classifyTerminalEvent, scanRunsForBoot, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_GRANTS_FILE, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_ADDRESSES_PREFIX, WORKFLOW_RUN_CONTROL_PREFIX, WORKFLOW_RUN_INBOX_DIR, WORKFLOW_RUN_PROCESSING_DIR, WORKFLOW_RUN_CONSUMED_DIR, WORKFLOW_RUN_WATERMARK_FILE, DEFAULT_CONSUMED_RETENTION_MS, type ClaimCheckEnvelope, type ConsumedEnvelope, type EnqueueAlreadyPresentReason, type EnqueueInboxArgs, type EnqueueInboxOutcome, type EnqueueInboxResult, type DequeueToProcessingResult, type ReadProcessingEntryResult, type MarkConsumedArgs, type MarkConsumedResult, type WorkflowRunLifecycle, type ReplayProcessingToInboxOpts, type ReplayProcessingToInboxResult, type WorkflowRunPrincipal, type WorkflowRunHubPrincipal, type WorkflowRunSidecarPrincipal, type WorkflowRunWorkflowProcessPrincipal, type WorkflowRunSupervisorPrincipal, } from "./workflow-run-kind.js";
|
|
22
23
|
export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
|
|
23
24
|
export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, type AssetService, type Asset, type CreateAssetParams, type PopulateAssetParams, type AssetServiceErrorReason, type ReadAssetBlobParams, type ListAssetBlobsParams, } from "./asset-service.js";
|
|
24
25
|
export { createWorkflowRunReader, type WorkflowRunReader, type WorkflowRunEvent, } from "./workflow-run-reader.js";
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
export { createAgentRepoStore, } from "./agent-repo.js";
|
|
2
|
-
export { createSessionService, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, } from "./session-service.js";
|
|
2
|
+
export { createSessionService, recoverSenderDeploy, SessionLaunchError, bridgeOrchestratorDeployContent, deployCodeSourcedWorkflow, } from "./session-service.js";
|
|
3
3
|
export { installAndApproveWorkflowDefinition, createDbFrozenApprovalWriter, } from "./workflow-probe-gate.js";
|
|
4
4
|
export { committedReadsToSourceTree } from "./committed-source-tree.js";
|
|
5
5
|
export { createEventCollectorRegistry, } from "./event-collector-registry.js";
|
|
6
|
-
export { createSidecarRouter, createSidecarCredentialResolver, createSidecarTokenAuthenticator, createSidecarEmitter, } from "./ws/index.js";
|
|
6
|
+
export { createSidecarRouter, SidecarIdentityValidationError, createSidecarCredentialResolver, createSidecarTokenAuthenticator, createSidecarEmitter, } from "./ws/index.js";
|
|
7
7
|
export { createHubSessionLookups, findRoutableById, parseAgentId, resolveRoutableAddress, resolveRunIdForSession, resolveRunSessionId, runRowToRoutableRecord, } from "./hub-session-lookups.js";
|
|
8
8
|
export { createHubSessionOrchestrator, } from "./hub-session-orchestrator.js";
|
|
9
|
-
export { pushSourceUpdates, pushSourceUpdatesSubtree } from "./credential-push.js";
|
|
10
|
-
export { createSidecarPluginRegistry, createSidecarAllocationReconciler,
|
|
9
|
+
export { pushSourceUpdates, pushSourceUpdatesSubtree, pushCredentialRevoke, pushCredentialReconcile, } from "./credential-push.js";
|
|
10
|
+
export { chooseFirstSidecarProvisioner, createSidecarPluginRegistry, createSidecarAllocationReconciler, } from "./sidecar-allocation/index.js";
|
|
11
11
|
export { ensureWorkflowDefinitionForAsset } from "./workflow-definition-ensure.js";
|
|
12
12
|
export { workflowSourceAssetMountPath } from "./workflow-closure-resolution.js";
|
|
13
|
-
export {
|
|
13
|
+
export { createReconciliationScheduler, DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, } from "./reconciliation-scheduler.js";
|
|
14
|
+
export { createWorkflowAllocationService, WorkflowProvisioningError, } from "./workflow-allocation-service.js";
|
|
14
15
|
export { createWorkflowDispatchService, } from "./workflow-dispatch-service.js";
|
|
15
16
|
export { listAcceptedWorkflowDispatches, listConsumedWorkflowDispatches, listReceivedWorkflowSignals, } from "./workflow-dispatch-settlement.js";
|
|
16
17
|
export { skillKindHandler, skillAuthorize, skillFrontmatterSchema, getSkillIndex, } from "./skill-kind.js";
|
|
17
18
|
export { packageRegistryKindHandler, packageRegistryAuthorize, asTarballEntry, validateTarballPackageJSON, TARBALLS_PREFIX, TARBALL_FILENAME_PATTERN, REGISTRY_INDEX_PATH, WORKSPACE_BUILTINS_REGISTRY, } from "./package-registry-kind.js";
|
|
18
19
|
export { workflowKindHandler, workflowAuthorize, workflowDefinitionEnvelopeSchema, WORKFLOW_JSON_PATH, CAPABILITY_DECLARATIONS_JSON_PATH, } from "./workflow-kind.js";
|
|
19
|
-
export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed,
|
|
20
|
+
export { workflowRunKindHandler, workflowRunAuthorize, enqueueInbox, StaleInboxEnqueueError, dequeueToProcessing, readProcessingEntry, markConsumed, classifyTerminalEvent, scanRunsForBoot, readCommittedWorkflowRunLifecycle, readWorkflowRunLifecycle, replayProcessingToInbox, WORKFLOW_RUN_GITIGNORE_PATH, WORKFLOW_RUN_RUNS_PREFIX, WORKFLOW_RUN_EVENTS_DIR, WORKFLOW_RUN_GRANTS_FILE, WORKFLOW_RUN_AGENT_STATE_PREFIX, WORKFLOW_RUN_ADDRESSES_PREFIX, WORKFLOW_RUN_CONTROL_PREFIX, WORKFLOW_RUN_INBOX_DIR, WORKFLOW_RUN_PROCESSING_DIR, WORKFLOW_RUN_CONSUMED_DIR, WORKFLOW_RUN_WATERMARK_FILE, DEFAULT_CONSUMED_RETENTION_MS, } from "./workflow-run-kind.js";
|
|
20
21
|
export { restoreWorkflowRunToAllocation, WORKFLOW_RUN_RESTORE_REFS, } from "./workflow-run-restore.js";
|
|
21
22
|
export { createAssetService, AssetServiceError, DEFAULT_ASSET_REF, } from "./asset-service.js";
|
|
22
23
|
export { createWorkflowRunReader, } from "./workflow-run-reader.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY = 8;
|
|
2
|
+
export type ReconciliationSchedulerOptions = {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
/** Claim and process at most one item; false means no due work was found. */
|
|
5
|
+
readonly reconcileNext: () => Promise<boolean>;
|
|
6
|
+
readonly concurrency?: number;
|
|
7
|
+
readonly intervalMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/** Polls for work without waiting for other occupied slots to finish. */
|
|
10
|
+
export declare function createReconciliationScheduler({ name, reconcileNext, concurrency, intervalMs, }: ReconciliationSchedulerOptions): {
|
|
11
|
+
start(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
wake: () => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getLogger } from "@intx/log";
|
|
2
|
+
const logger = getLogger(["hub", "reconciliation"]);
|
|
3
|
+
export const DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY = 8;
|
|
4
|
+
/** Polls for work without waiting for other occupied slots to finish. */
|
|
5
|
+
export function createReconciliationScheduler({ name, reconcileNext, concurrency = DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, intervalMs = 1_000, }) {
|
|
6
|
+
if (!Number.isSafeInteger(concurrency) || concurrency <= 0) {
|
|
7
|
+
throw new Error("Reconciliation concurrency must be a positive integer");
|
|
8
|
+
}
|
|
9
|
+
if (!Number.isSafeInteger(intervalMs) || intervalMs <= 0) {
|
|
10
|
+
throw new Error("Reconciliation interval must be a positive integer");
|
|
11
|
+
}
|
|
12
|
+
let stopped = true;
|
|
13
|
+
let active = 0;
|
|
14
|
+
let timer;
|
|
15
|
+
function finish(worked) {
|
|
16
|
+
active -= 1;
|
|
17
|
+
if (worked)
|
|
18
|
+
wake();
|
|
19
|
+
}
|
|
20
|
+
function wake() {
|
|
21
|
+
if (stopped)
|
|
22
|
+
return;
|
|
23
|
+
while (active < concurrency) {
|
|
24
|
+
active += 1;
|
|
25
|
+
void Promise.resolve()
|
|
26
|
+
.then(() => (stopped ? false : reconcileNext()))
|
|
27
|
+
.then(finish, (error) => {
|
|
28
|
+
logger.error `${name} reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
29
|
+
finish(false);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function schedule(delayMs) {
|
|
34
|
+
timer = setTimeout(() => {
|
|
35
|
+
if (stopped)
|
|
36
|
+
return;
|
|
37
|
+
wake();
|
|
38
|
+
schedule(intervalMs);
|
|
39
|
+
}, delayMs);
|
|
40
|
+
timer.unref?.();
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
start() {
|
|
44
|
+
if (!stopped)
|
|
45
|
+
return;
|
|
46
|
+
stopped = false;
|
|
47
|
+
schedule(0);
|
|
48
|
+
},
|
|
49
|
+
stop() {
|
|
50
|
+
stopped = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
},
|
|
53
|
+
wake,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { AuthorizeFn, CommittedReads, CommittedTreeEntry, InitRepoOpts, KindHandler, NewlyTerminalRun, PriorDeltaReads, Principal, RefEntry, RepoAction, RepoId, RepoKind, RepoStore, RepoStoreSubscribeEvent, TreeContent, ValidatePushResult, WriteResult, WriteTreePreservingPrefixArgs, } from "./types.js";
|
|
2
2
|
export { UserPrincipal } from "./types.js";
|
|
3
|
+
export { authorizeUserPrincipal, type AuthorizeUserPrincipalArgs, } from "./user-principal-gate.js";
|
|
3
4
|
export { createRepoStore, type CreateRepoStoreConfig } from "./store.js";
|
|
4
5
|
export { subscribeKind, type SubscribeKindOpts, type SubscribeKindEntry, } from "./subscribe-kind.js";
|
package/dist/repo-store/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RepoAction, RepoId, Principal } from "./types.js";
|
|
2
|
+
export type AuthorizeUserPrincipalArgs = {
|
|
3
|
+
principal: Principal;
|
|
4
|
+
repoId: RepoId;
|
|
5
|
+
ref: string;
|
|
6
|
+
action: RepoAction;
|
|
7
|
+
/**
|
|
8
|
+
* The resource-kind prefix the pre-resolved authz verdict must carry
|
|
9
|
+
* for this kind: the verdict's `resource` is compared against
|
|
10
|
+
* `<resourcePrefix>:<repoId.id>`.
|
|
11
|
+
*/
|
|
12
|
+
resourcePrefix: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Verdict for a `user` principal performing `action` on `ref` of
|
|
16
|
+
* `repoId`, in the shape the substrate's `AuthorizeFn` contract
|
|
17
|
+
* expects. The caller dispatches on `principal.kind === "user"` first;
|
|
18
|
+
* this function narrows with `UserPrincipal` and applies the full
|
|
19
|
+
* claim/verdict cross-check.
|
|
20
|
+
*/
|
|
21
|
+
export declare function authorizeUserPrincipal({ principal, repoId, ref, action, resourcePrefix, }: AuthorizeUserPrincipalArgs): {
|
|
22
|
+
allowed: true;
|
|
23
|
+
} | {
|
|
24
|
+
allowed: false;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Shared authorization gate for the `user` principal variant, used by
|
|
2
|
+
// every kind handler that accepts user-token-authenticated requests
|
|
3
|
+
// (workflow, skill, agent-state, workflow-run).
|
|
4
|
+
//
|
|
5
|
+
// The route layer has already pre-resolved the grant verdict and
|
|
6
|
+
// attached it as `authz`; the kind handlers do NOT re-query the grant
|
|
7
|
+
// store here. This gate (a) checks the bearer-token's claims bound the
|
|
8
|
+
// requested (ref, action) and have not expired, and (b) sanity-checks
|
|
9
|
+
// that the pre-resolved verdict targets this exact resource and grant
|
|
10
|
+
// verb. Both gates must pass before the verdict's `effect` is honoured.
|
|
11
|
+
//
|
|
12
|
+
// Funnelling every kind through this one gate keeps the security-
|
|
13
|
+
// critical claim/verdict cross-check from drifting between kinds. The
|
|
14
|
+
// only per-kind input is the `resourcePrefix` the verdict's `resource`
|
|
15
|
+
// must carry (`asset:<id>` for the codebase kinds, `agent-state:<id>`,
|
|
16
|
+
// `workflow-run:<id>`).
|
|
17
|
+
import { type } from "arktype";
|
|
18
|
+
import { glob, repoActionToGrantVerb } from "@intx/hub-common";
|
|
19
|
+
import { UserPrincipal } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Verdict for a `user` principal performing `action` on `ref` of
|
|
22
|
+
* `repoId`, in the shape the substrate's `AuthorizeFn` contract
|
|
23
|
+
* expects. The caller dispatches on `principal.kind === "user"` first;
|
|
24
|
+
* this function narrows with `UserPrincipal` and applies the full
|
|
25
|
+
* claim/verdict cross-check.
|
|
26
|
+
*/
|
|
27
|
+
export function authorizeUserPrincipal({ principal, repoId, ref, action, resourcePrefix, }) {
|
|
28
|
+
const parsed = UserPrincipal(principal);
|
|
29
|
+
if (parsed instanceof type.errors) {
|
|
30
|
+
return {
|
|
31
|
+
allowed: false,
|
|
32
|
+
reason: `user principal is malformed: ${parsed.summary}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!parsed.tokenClaims.actions.includes(action)) {
|
|
36
|
+
return {
|
|
37
|
+
allowed: false,
|
|
38
|
+
reason: `token does not grant action ${action}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// `ref === "*"` is the substrate's sentinel for the bulk read
|
|
42
|
+
// performed by `listRefs`. Per-ref filtering is the advertise-refs
|
|
43
|
+
// layer's responsibility, so the bulk read is gated on action and
|
|
44
|
+
// expiry alone.
|
|
45
|
+
if (ref !== "*" && !glob.match(parsed.tokenClaims.refPattern, ref)) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
reason: `token refPattern ${parsed.tokenClaims.refPattern} does not match ${ref}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() >= parsed.tokenClaims.expiresAt) {
|
|
52
|
+
return {
|
|
53
|
+
allowed: false,
|
|
54
|
+
reason: `token expired at ${parsed.tokenClaims.expiresAt}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const expectedResource = `${resourcePrefix}:${repoId.id}`;
|
|
58
|
+
if (parsed.authz.resource !== expectedResource) {
|
|
59
|
+
return {
|
|
60
|
+
allowed: false,
|
|
61
|
+
reason: `authz verdict resource ${parsed.authz.resource} does not match ${expectedResource}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const expectedGrantVerb = repoActionToGrantVerb(action);
|
|
65
|
+
if (parsed.authz.grantVerb !== expectedGrantVerb) {
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
reason: `authz verdict grantVerb ${parsed.authz.grantVerb} does not match ${expectedGrantVerb}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (parsed.authz.effect === "allow") {
|
|
72
|
+
return { allowed: true };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
allowed: false,
|
|
76
|
+
reason: `authz verdict denied for ${expectedResource} ${expectedGrantVerb}`,
|
|
77
|
+
};
|
|
78
|
+
}
|