@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.
Files changed (67) hide show
  1. package/dist/agent-repo.d.ts +14 -2
  2. package/dist/agent-repo.js +17 -4
  3. package/dist/agent-state-kind.js +14 -63
  4. package/dist/asset-service.js +14 -10
  5. package/dist/credential-push.d.ts +48 -4
  6. package/dist/credential-push.js +138 -6
  7. package/dist/event-collector-registry.d.ts +2 -1
  8. package/dist/event-collector-registry.js +38 -9
  9. package/dist/event-collector.d.ts +11 -1
  10. package/dist/event-collector.js +36 -3
  11. package/dist/hub-session-lookups.d.ts +1 -1
  12. package/dist/hub-session-lookups.js +68 -72
  13. package/dist/hub-session-orchestrator.d.ts +2 -3
  14. package/dist/hub-session-orchestrator.js +13 -12
  15. package/dist/index.d.ts +7 -6
  16. package/dist/index.js +7 -6
  17. package/dist/reconciliation-scheduler.d.ts +14 -0
  18. package/dist/reconciliation-scheduler.js +55 -0
  19. package/dist/repo-store/index.d.ts +1 -0
  20. package/dist/repo-store/index.js +1 -0
  21. package/dist/repo-store/user-principal-gate.d.ts +26 -0
  22. package/dist/repo-store/user-principal-gate.js +78 -0
  23. package/dist/session-service.d.ts +66 -121
  24. package/dist/session-service.js +444 -411
  25. package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
  26. package/dist/sidecar-allocation/capability-policy.js +124 -0
  27. package/dist/sidecar-allocation/contracts.d.ts +29 -6
  28. package/dist/sidecar-allocation/contracts.js +7 -2
  29. package/dist/sidecar-allocation/index.d.ts +4 -3
  30. package/dist/sidecar-allocation/index.js +3 -2
  31. package/dist/sidecar-allocation/operation.d.ts +10 -0
  32. package/dist/sidecar-allocation/operation.js +54 -0
  33. package/dist/sidecar-allocation/plugin-registry.d.ts +16 -3
  34. package/dist/sidecar-allocation/plugin-registry.js +36 -12
  35. package/dist/sidecar-allocation/reconciler.d.ts +16 -4
  36. package/dist/sidecar-allocation/reconciler.js +486 -92
  37. package/dist/skill-kind.js +8 -62
  38. package/dist/substrate.d.ts +1 -1
  39. package/dist/substrate.js +1 -1
  40. package/dist/workflow-allocation-service.d.ts +21 -15
  41. package/dist/workflow-allocation-service.js +440 -125
  42. package/dist/workflow-dispatch-service.d.ts +4 -2
  43. package/dist/workflow-dispatch-service.js +89 -26
  44. package/dist/workflow-kind.d.ts +12 -0
  45. package/dist/workflow-kind.js +17 -60
  46. package/dist/workflow-probe-gate.d.ts +99 -27
  47. package/dist/workflow-probe-gate.js +196 -21
  48. package/dist/workflow-run-kind.d.ts +112 -19
  49. package/dist/workflow-run-kind.js +626 -210
  50. package/dist/workflow-run-restore.d.ts +1 -0
  51. package/dist/workflow-run-restore.js +5 -1
  52. package/dist/workflow-source-pins.d.ts +8 -0
  53. package/dist/workflow-source-pins.js +14 -0
  54. package/dist/ws/index.d.ts +1 -1
  55. package/dist/ws/index.js +1 -1
  56. package/dist/ws/pending-tracker.d.ts +93 -0
  57. package/dist/ws/pending-tracker.js +132 -0
  58. package/dist/ws/sidecar-events.d.ts +43 -29
  59. package/dist/ws/sidecar-events.js +0 -2
  60. package/dist/ws/sidecar-handler.d.ts +122 -85
  61. package/dist/ws/sidecar-handler.js +925 -878
  62. package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
  63. package/dist/ws/sidecar-handler.test-helpers.js +95 -0
  64. package/dist/ws/sidecar-token-authenticator.js +37 -23
  65. package/package.json +13 -13
  66. package/dist/sidecar-allocation/placement-policy.d.ts +0 -11
  67. package/dist/sidecar-allocation/placement-policy.js +0 -21
@@ -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, usage, and other events are
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
- if (source.kind === "allocated") {
286
- newlyTerminalRuns = await db.transaction(async (tx) => {
287
- const [allocation] = await tx
288
- .select()
289
- .from(sidecarAllocation)
290
- .where(eq(sidecarAllocation.anchorRunId, anchor.id))
291
- .limit(1)
292
- .for("update");
293
- if (allocation === undefined ||
294
- allocation.id !== source.allocationId ||
295
- allocation.anchorRunId !== source.anchorRunId ||
296
- source.anchorRunId !== anchor.id ||
297
- allocation.status !== "allocated" ||
298
- allocation.generation !== source.generation ||
299
- allocation.ensureAcceptedGeneration !== source.generation) {
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
- else {
314
- const allocation = await db.query.sidecarAllocation.findFirst({
315
- where: eq(sidecarAllocation.anchorRunId, anchor.id),
316
- });
317
- if (allocation !== undefined) {
318
- logger.warn `Workflow-run pack rejected for ${workflowRunRepoId}: source connection does not own the deployment's current allocation`;
319
- return { accepted: false, reason: "path_violation" };
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
- // No running row matched. Either the run is already terminal (a
367
- // benign replay against an already-settled row) or no row exists
368
- // at all -- the run reached a terminal event before its anchor
369
- // committed, so its terminal state has nowhere to land. Only the
370
- // second case is a defect; distinguish them and log the missing
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, agentRepoStore } = deps;
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
- // Exclusive initialization publishes its key only after every deploy
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 so the reconnect ownership challenge can verify the address off
56
- // the same row `lookupPublicKey` reads. Only the deployment-level address
57
- // owns a row, so a stray per-step ack updates nothing.
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
- unsubscribers.push(events.on("deploy.ref.stale", async ({ agentAddress }) => {
64
- const agentId = parseAgentId(agentAddress);
65
- const { pack, commitSha, ref } = await agentRepoStore.createDeployPack(agentId);
66
- await router.sendPack(agentAddress, pack, ref, commitSha);
67
- log.info("Re-deployed stale agent {agentAddress}", { agentAddress });
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 DeployWorkflowFromSourceParams, type DeployPreparedCodeSourcedWorkflowParams, type InstallAndApproveWorkflowSourceParams, type PreparedWorkflowDeployer, type DeployCodeSourcedWorkflowArgs, } from "./session-service.js";
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, resolveEffectiveSidecarPlacement, type CreateSidecarPluginRegistryOpts, type DestroySidecarRequest, type DestroySidecarResult, type EnsureSidecarRequest, type EnsureSidecarResult, type ResolveEffectiveSidecarPlacementOpts, type SidecarCredentialIdentity, type SidecarCredentialResolver, type SidecarOperationFailure, type SidecarPluginRegistry, type SidecarProvisioner, type SidecarAllocationReconciler, type SidecarAllocationReconcilerDeps, } from "./sidecar-allocation/index.js";
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 { createWorkflowAllocationService, ExclusiveWorkflowPlacementError, resolveWorkflowSidecarPlacement, type PrepareExclusiveWorkflowDeploymentArgs, type PreparedExclusiveWorkflowDeployment, type WorkflowAllocationService, type WorkflowAllocationServiceDeps, } from "./workflow-allocation-service.js";
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, readOwnedMessageIds, 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
+ 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, resolveEffectiveSidecarPlacement, } from "./sidecar-allocation/index.js";
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 { createWorkflowAllocationService, ExclusiveWorkflowPlacementError, resolveWorkflowSidecarPlacement, } from "./workflow-allocation-service.js";
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, readOwnedMessageIds, 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
+ 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";
@@ -1,3 +1,4 @@
1
1
  export { UserPrincipal } from "./types.js";
2
+ export { authorizeUserPrincipal, } from "./user-principal-gate.js";
2
3
  export { createRepoStore } from "./store.js";
3
4
  export { subscribeKind, } from "./subscribe-kind.js";
@@ -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
+ }