@agentcomms/events-daemon 0.17.0 → 0.18.1

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/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as createRule$1, $t as assertSocketPathFits, A as normaliseWhatsAppSourceOptions, At as deliveriesList, B as removeTarget$1, Bt as ControlFrameReader, C as canonicalSseSubscriber, Ct as assertDoctorReportV1, D as normaliseResendSourceOptions, Dt as isActivationFailureCode, E as normaliseGmailSourceOptions, Et as ACTIVATION_FAILURE, F as canonicalJudgeBudget, Ft as catalogueShow, G as removeSubscriber, Gt as negotiateVersion, H as updateTarget, Ht as encodeControlFrame, I as canonicalJudgeKindEnablement, It as approvalView, J as stop, Jt as processIsLive, K as subscribersList, Kt as mayRecoverStaleInstance, L as judgeKindDefinition, Lt as EventControlClient, M as JUDGE_BUDGET_ID, Mt as heldList, N as JUDGE_KIND_DEFINITIONS, Nt as retryDelivery, O as normaliseSlackSourceOptions, Ot as disableAll, P as canonicalJudge, Pt as catalogueList, Q as sourcesList, Qt as assertControlSupported, R as createEventsMcpServer, Rt as probeControl, S as normaliseActivationDocument, St as enableAll, T as classifySourceOptionChange, Tt as doctor, U as targetTest, Ut as isControlHello, V as targetsList, Vt as controlFailure, W as addSubscriber, Wt as isControlRequest, X as loadSqlite, Xt as removeInstance, Y as status, Yt as readInstance, Z as sourceShow$1, Zt as writeInstance, _ as activationDocumentDigest, _t as judgesList, an as ensureOwnerOnlyFile, at as updateRule, b as canonicalTarget, bt as updateBudget, ct as pause, d as B3_SECRET_TERMINAL_ORIGIN, dt as budgetShow, en as controlEndpoint, et as disableRule$1, f as isB3ControlOperation, ft as disableJudgeKind, g as isOwnerControlWireOperation, gt as judgeTest, h as OWNER_CONTROL_WIRES, ht as judgeModelStatus, in as ensureEventSocketDirectory, it as rulesList, j as APPROVED_TYPESAFE_MODELS, jt as dropDelivery, k as normaliseSourceOptions, kt as decideHeld, lt as resume, m as renderB3ApprovalPreviewV1, mt as judgeKindsList, nn as verifyPrivateSocketDirectory, nt as removeRule, on as eventPaths, ot as ruleTest, p as isB3SecretOperation, pt as enableJudgeKind, q as updateSubscriber, qt as newInstanceRecord, rn as ensureEventPaths, rt as ruleShow$1, sn as installErrorSink, st as ruleTestRetained, tn as verifyControlSocket, tt as enableRule, ut as addJudge, v as canonicalFullRuleDocument, vt as removeJudge, w as sseDeliveryTarget, wt as assertDoctorReportV2, x as disclosureBindingFor, xt as updateJudge, y as canonicalSubscriber, yt as removeJudgeModel, z as addTarget, zt as CONTROL_PROTOCOL_VERSIONS } from "./b3-contract-DFxcHY0I.mjs";
2
+ import { $ as sourcesList, $t as assertControlSupported, A as normaliseWhatsAppSourceOptions, At as decideHeld, B as addTarget, Bt as CONTROL_PROTOCOL_VERSIONS, C as canonicalSseSubscriber, Ct as enableAll, D as normaliseResendSourceOptions, Dt as ACTIVATION_FAILURE, E as normaliseGmailSourceOptions, Et as doctor, F as canonicalJudgeBudget, Ft as catalogueList, G as addSubscriber, Gt as isControlRequest, H as targetsList, Ht as controlFailure, I as canonicalJudgeKindEnablement, It as catalogueShow, J as updateSubscriber, Jt as newInstanceRecord, K as removeSubscriber, Kt as negotiateVersion, L as judgeKindDefinition, Lt as approvalView, M as JUDGE_BUDGET_ID, Mt as dropDelivery, N as JUDGE_KIND_DEFINITIONS, Nt as heldList, O as normaliseSlackSourceOptions, Ot as isActivationFailureCode, P as canonicalJudge, Pt as retryDelivery, Q as sourceShow$1, Qt as writeInstance, R as createEventsMcpServer, Rt as EventControlClient, S as normaliseActivationDocument, St as updateJudge, T as classifySourceOptionChange, Tt as assertDoctorReportV2, U as updateTarget, Ut as encodeControlFrame, V as removeTarget$1, Vt as ControlFrameReader, W as targetTest, Wt as isControlHello, X as status, Xt as readInstance, Y as stop, Yt as processIsLive, Z as loadSqlite, Zt as removeInstance, _ as activationDocumentDigest, _t as judgeTest, an as ensureEventSocketDirectory, at as rulesList, b as canonicalTarget, bt as removeJudgeModel, cn as installErrorSink, ct as ruleTestRetained, d as B3_SECRET_TERMINAL_ORIGIN, dt as addJudge, en as assertSocketPathFits, et as createRule$1, f as isB3ControlOperation, ft as budgetShow, g as isOwnerControlWireOperation, gt as judgeModelStatus, h as OWNER_CONTROL_WIRES, ht as judgeKindsList, in as ensureEventPaths, it as ruleShow$1, j as APPROVED_TYPESAFE_MODELS, jt as deliveriesList, k as normaliseSourceOptions, kt as disableAll, lt as pause, m as renderB3ApprovalPreviewV1, mt as enableJudgeKind, nn as verifyControlSocket, nt as enableRule, on as ensureOwnerOnlyFile, ot as updateRule, p as isB3SecretOperation, pt as disableJudgeKind, q as subscribersList, qt as mayRecoverStaleInstance, rn as verifyPrivateSocketDirectory, rt as removeRule, sn as eventPaths, st as ruleTest, tn as controlEndpoint, tt as disableRule$1, ut as resume, v as canonicalFullRuleDocument, vt as judgesList, w as sseDeliveryTarget, wt as assertDoctorReportV1, x as disclosureBindingFor, xt as updateBudget, y as canonicalSubscriber, yt as removeJudge, z as VERSION, zt as probeControl } from "./b3-contract-BZIj-LWV.mjs";
3
3
  import { a as EventDomainError, d as isJudgeKind, f as validateLocalEndpoint, g as parseIpLiteral, h as assertApprovedAddressSet, i as webhookDeliveryTarget, l as JUDGE_KIND_TYPESAFE, m as assertAddressAllowed, n as canonicalWebhookTarget, o as JUDGE_KINDS, p as AddressPolicyError, r as secretWebhookUrlDescriptor, s as JUDGE_KIND_LAYA, t as canonicalSecretWebhookUrl, u as assertJudgeKindEnabled } from "./webhook-target-DyDTwWhV.mjs";
4
4
  import { a as WriterQuiescer, c as openEventSecretStore, o as withEventWriterLease, r as RecordStorageError, s as migrateEventSecrets, t as EventRecordCipher } from "./records-DsUxYBwb.mjs";
5
5
  import process$1 from "node:process";
@@ -3365,6 +3365,7 @@ var ImmutableVersions = class {
3365
3365
  assertEmbeddedVersions(rule) {
3366
3366
  const embedded = [
3367
3367
  ...rule.targets.map((target) => ({
3368
+ kind: "target",
3368
3369
  table: "target_versions",
3369
3370
  column: "target_id",
3370
3371
  id: target.targetId,
@@ -3372,6 +3373,7 @@ var ImmutableVersions = class {
3372
3373
  document: target
3373
3374
  })),
3374
3375
  ...rule.subscribers.map((subscriber) => ({
3376
+ kind: "subscriber",
3375
3377
  table: "subscriber_versions",
3376
3378
  column: "subscriber_id",
3377
3379
  id: subscriber.subscriberId,
@@ -3379,6 +3381,7 @@ var ImmutableVersions = class {
3379
3381
  document: subscriber
3380
3382
  })),
3381
3383
  ...rule.agentic === void 0 ? [] : [{
3384
+ kind: "judge",
3382
3385
  table: "judge_versions",
3383
3386
  column: "judge_id",
3384
3387
  id: rule.agentic.judge.judgeId,
@@ -3390,8 +3393,19 @@ var ImmutableVersions = class {
3390
3393
  const row = this.#database.prepare(`SELECT document FROM ${entry.table} WHERE ${entry.column} = ? AND version = ?`).get(entry.id, entry.version);
3391
3394
  if (row === void 0) throw new EventDomainError("VERSION_DOCUMENT_INVALID", `the rule names ${entry.id} version ${entry.version}, which is not a stored version`);
3392
3395
  if (row.document !== canonicalJson(entry.document)) throw new EventDomainError("VERSION_DOCUMENT_INVALID", `the rule carries a copy of ${entry.id} version ${entry.version} that differs from the stored version`);
3396
+ if (entry.kind === "judge" && this.#embeddedJudgeIsRevoked(entry.id, entry.version)) throw new EventDomainError("VERSION_DOCUMENT_INVALID", `the rule names revoked judge ${entry.id} version ${entry.version}`);
3393
3397
  }
3394
3398
  }
3399
+ #embeddedJudgeIsRevoked(judgeId, version) {
3400
+ return this.#database.prepare(`SELECT 1 AS present
3401
+ FROM judge_versions AS stored
3402
+ LEFT JOIN object_revocations AS revoked
3403
+ ON revoked.kind = 'judge'
3404
+ AND revoked.object_id = stored.judge_id
3405
+ AND revoked.version = stored.version
3406
+ WHERE stored.judge_id = ? AND stored.version = ?
3407
+ AND (stored.revoked_at IS NOT NULL OR revoked.revoked_at IS NOT NULL)`).get(judgeId, version) !== void 0;
3408
+ }
3395
3409
  insert(table, kind, objectId, version, document) {
3396
3410
  const id = versionId(objectId, version);
3397
3411
  const persisted = documentRecord(document);
@@ -7292,7 +7306,7 @@ function readReservation(database, id) {
7292
7306
  function readReservationForSourceWork(database, workId, scope) {
7293
7307
  return database.prepare(`SELECT * FROM judge_budget_reservations
7294
7308
  WHERE work_id = ? AND work_scope = 'source-owned' AND source = ? AND account_id = ?
7295
- AND event_id = ? AND rule_id = ? AND rule_version = ?`).get(workId, scope.source, scope.accountId, scope.eventId, scope.ruleId, scope.ruleVersion) ?? null;
7309
+ AND event_id = ? AND rule_id = ? AND rule_version = ? AND state IN ('reserved', 'in-flight')`).get(workId, scope.source, scope.accountId, scope.eventId, scope.ruleId, scope.ruleVersion) ?? null;
7296
7310
  }
7297
7311
  function reservationView(row) {
7298
7312
  const scope = sourceScope(row);
@@ -7399,8 +7413,7 @@ function reserveJudgeWork(input) {
7399
7413
  activeKind(input.store.database, judge.kind);
7400
7414
  const limits = ruleAuthority(input.store.database, input.scope, judge);
7401
7415
  if (!sameCanonical(limits, input.limits)) refusal$2("JUDGE_AUTHORITY_CHANGED", "the supplied limits differ from the immutable rule authority");
7402
- const prior = readReservationForSourceWork(input.store.database, input.workId, input.scope);
7403
- if (prior?.state === "reserved" || prior?.state === "in-flight") refusal$2("JUDGE_WORK_OWNER_HELD", "a live judge reservation already owns this source work");
7416
+ if (readReservationForSourceWork(input.store.database, input.workId, input.scope) !== null) refusal$2("JUDGE_WORK_OWNER_HELD", "a live judge reservation already owns this source work");
7404
7417
  assertReservationLimits(input.store.database, {
7405
7418
  scope: input.scope,
7406
7419
  budget,
@@ -7410,15 +7423,6 @@ function reserveJudgeWork(input) {
7410
7423
  now: input.now,
7411
7424
  adding: true
7412
7425
  });
7413
- if (prior?.state === "released") {
7414
- input.store.database.prepare(`UPDATE judge_budget_reservations
7415
- SET estimated_input_tokens = ?, actual_input_tokens = NULL, state = 'reserved', concurrency_owned = 1,
7416
- reserved_at = ?, in_flight_at = NULL, settled_at = NULL, released_at = NULL, switch_generation = ?
7417
- WHERE id = ? AND state = 'released' AND concurrency_owned = 0`).run(estimate, input.now, input.switchGeneration, prior.id);
7418
- const reactivated = readReservation(input.store.database, prior.id);
7419
- if (reactivated === null || reactivated.state !== "reserved") refusal$2("JUDGE_AUTHORITY_CHANGED", "the released judge work could not be re-reserved");
7420
- return reservationView(reactivated);
7421
- }
7422
7426
  return insertReservation(input.store.database, {
7423
7427
  ...input,
7424
7428
  judge,
@@ -7477,12 +7481,14 @@ function beginJudgeTransport(input) {
7477
7481
  if (row === null || row.state !== "reserved") return false;
7478
7482
  try {
7479
7483
  const authority = assertReservationAuthority(input.store.database, row, input.now);
7480
- const scope = sourceScope(row) ?? {
7484
+ const source = sourceScope(row);
7485
+ const scope = source ?? {
7481
7486
  scope: "synthetic-test",
7482
7487
  testWorkId: row.test_work_id,
7483
7488
  judgeId: row.judge_id,
7484
7489
  judgeVersion: row.judge_version
7485
7490
  };
7491
+ if (source !== null) assertSourceReservationBoundary(input.store.database, source, row.switch_generation);
7486
7492
  assertReservationLimits(input.store.database, {
7487
7493
  scope,
7488
7494
  budget: authority.budget,
@@ -7492,6 +7498,8 @@ function beginJudgeTransport(input) {
7492
7498
  now: input.now,
7493
7499
  adding: false
7494
7500
  });
7501
+ assertHostedKeyGeneration(input.store.database, row, input.hostedKey);
7502
+ input.assertBeforeTransition?.();
7495
7503
  } catch (error) {
7496
7504
  releaseReservationInTransaction(input.store.database, row.id, input.now);
7497
7505
  refused = true;
@@ -7506,6 +7514,67 @@ function beginJudgeTransport(input) {
7506
7514
  return began;
7507
7515
  }
7508
7516
  /**
7517
+ * Couples the durable I/O transition to the sealed transport's synchronous gate. Hosted work enters at DNS; a
7518
+ * literal-loopback judge has no DNS action and enters at TCP. Every later gate rechecks the complete live authority.
7519
+ */
7520
+ var JudgeReservationIoGate = class {
7521
+ #store;
7522
+ #reservationId;
7523
+ #judgeKind;
7524
+ #now;
7525
+ #hostedKey;
7526
+ #entered = false;
7527
+ #firstGateError;
7528
+ constructor(input) {
7529
+ this.#store = input.store;
7530
+ this.#reservationId = input.reservationId;
7531
+ this.#judgeKind = input.judgeKind;
7532
+ this.#now = input.now;
7533
+ this.#hostedKey = input.hostedKey;
7534
+ }
7535
+ get firstGateError() {
7536
+ return this.#firstGateError;
7537
+ }
7538
+ finalGate = (phase, assertBeforeTransition) => {
7539
+ const firstPhysicalPhase = this.#judgeKind === "typesafe" ? "dns" : "tcp";
7540
+ if (!this.#entered) {
7541
+ if (phase !== firstPhysicalPhase) {
7542
+ assertBeforeTransition?.();
7543
+ return;
7544
+ }
7545
+ let deadlineRefused = false;
7546
+ try {
7547
+ if (!beginJudgeTransport({
7548
+ store: this.#store,
7549
+ reservationId: this.#reservationId,
7550
+ now: this.#now(),
7551
+ ...this.#hostedKey === void 0 ? {} : { hostedKey: this.#hostedKey },
7552
+ assertBeforeTransition: () => {
7553
+ try {
7554
+ assertBeforeTransition?.();
7555
+ } catch (error) {
7556
+ deadlineRefused = true;
7557
+ throw error;
7558
+ }
7559
+ }
7560
+ })) refusal$2("JUDGE_AUTHORITY_CHANGED", "the judge reservation was lost before physical I/O");
7561
+ } catch (error) {
7562
+ if (!deadlineRefused) this.#firstGateError = error;
7563
+ throw error;
7564
+ }
7565
+ this.#entered = true;
7566
+ return;
7567
+ }
7568
+ assertJudgeReservationStillInFlight({
7569
+ store: this.#store,
7570
+ reservationId: this.#reservationId,
7571
+ now: this.#now(),
7572
+ ...this.#hostedKey === void 0 ? {} : { hostedKey: this.#hostedKey }
7573
+ });
7574
+ assertBeforeTransition?.();
7575
+ };
7576
+ };
7577
+ /**
7509
7578
  * The synchronous final transport/decision fence. It reuses the one reservation authority reader rather than
7510
7579
  * manufacturing a second reservation path, and is safe to call immediately beside each Task-6 I/O action.
7511
7580
  */
@@ -7513,21 +7582,23 @@ function assertJudgeReservationInFlightInTransaction(database, reservationId, no
7513
7582
  const row = readReservation(database, reservationId);
7514
7583
  if (row === null || row.state !== "in-flight" || row.concurrency_owned !== 1) refusal$2("JUDGE_AUTHORITY_CHANGED", "the judge reservation is no longer in flight");
7515
7584
  assertReservationAuthority(database, row, now);
7516
- if (hostedKey !== void 0) {
7517
- if (row.judge_id !== hostedKey.judgeId || row.judge_version !== hostedKey.judgeVersion) refusal$2("JUDGE_AUTHORITY_CHANGED", "the hosted judge key does not belong to this reservation");
7518
- if (database.prepare(`SELECT 1 AS present
7519
- FROM event_secret_generations AS generation
7520
- JOIN judge_versions AS judge
7521
- ON judge.judge_id = generation.owner_id AND judge.version = generation.owner_version
7522
- WHERE generation.owner_kind = 'judge' AND generation.purpose = 'judge-key'
7523
- AND generation.owner_id = ? AND generation.owner_version = ? AND generation.generation = ?
7524
- AND generation.lifecycle = 'current' AND generation.owner_digest = judge.digest
7525
- AND judge.revoked_at IS NULL`).get(hostedKey.judgeId, hostedKey.judgeVersion, hostedKey.generation) === void 0) refusal$2("JUDGE_AUTHORITY_CHANGED", "the hosted judge key generation is no longer current");
7526
- }
7585
+ assertHostedKeyGeneration(database, row, hostedKey);
7527
7586
  const scope = sourceScope(row);
7528
7587
  if (scope === null) return;
7529
7588
  assertSourceReservationBoundary(database, scope, row.switch_generation);
7530
7589
  }
7590
+ function assertHostedKeyGeneration(database, row, hostedKey) {
7591
+ if (hostedKey === void 0) return;
7592
+ if (row.judge_id !== hostedKey.judgeId || row.judge_version !== hostedKey.judgeVersion) refusal$2("JUDGE_AUTHORITY_CHANGED", "the hosted judge key does not belong to this reservation");
7593
+ if (database.prepare(`SELECT 1 AS present
7594
+ FROM event_secret_generations AS generation
7595
+ JOIN judge_versions AS judge
7596
+ ON judge.judge_id = generation.owner_id AND judge.version = generation.owner_version
7597
+ WHERE generation.owner_kind = 'judge' AND generation.purpose = 'judge-key'
7598
+ AND generation.owner_id = ? AND generation.owner_version = ? AND generation.generation = ?
7599
+ AND generation.lifecycle = 'current' AND generation.owner_digest = judge.digest
7600
+ AND judge.revoked_at IS NULL`).get(hostedKey.judgeId, hostedKey.judgeVersion, hostedKey.generation) === void 0) refusal$2("JUDGE_AUTHORITY_CHANGED", "the hosted judge key generation is no longer current");
7601
+ }
7531
7602
  function assertSourceReservationBoundary(database, scope, switchGeneration) {
7532
7603
  const settings = database.prepare("SELECT enabled, paused, switch_generation FROM event_settings WHERE singleton = 1").get();
7533
7604
  if (settings?.paused === 1) refusal$2("JUDGE_PAUSED", "the event switch is paused before the judge boundary");
@@ -7540,20 +7611,10 @@ function assertJudgeReservationStillInFlight(input) {
7540
7611
  assertJudgeReservationInFlightInTransaction(input.store.database, input.reservationId, input.now, input.hostedKey);
7541
7612
  });
7542
7613
  }
7543
- function releaseReservationInTransaction(database, reservationId, now, mayReleaseInFlight = false) {
7614
+ function releaseReservationInTransaction(database, reservationId, now) {
7544
7615
  return database.prepare(`UPDATE judge_budget_reservations
7545
7616
  SET state = 'released', concurrency_owned = 0, released_at = ?
7546
- WHERE id = ? AND concurrency_owned = 1
7547
- AND state != 'settled' AND state != 'released'
7548
- AND state = CASE WHEN ? = 1 THEN state ELSE 'reserved' END`).run(now, reservationId, mayReleaseInFlight ? 1 : 0).changes === 1;
7549
- }
7550
- /**
7551
- * Pre-I/O loss is free: a reserved call releases normally. `beforeIo` is an intentionally narrow transport proof
7552
- * that lets a final fence refusal release the just-marked in-flight row too, because no DNS, TCP, TLS, or raw write
7553
- * was entered and therefore there is no provider work to charge.
7554
- */
7555
- function releaseJudgeWork(input) {
7556
- return input.store.immediate(() => releaseReservationInTransaction(input.store.database, input.reservationId, input.now, input.beforeIo === true));
7617
+ WHERE id = ? AND concurrency_owned = 1 AND state = 'reserved'`).run(now, reservationId).changes === 1;
7557
7618
  }
7558
7619
  function terminalReason(result) {
7559
7620
  if (result === "unknown" || result === "timeout" || result === "transport-failure") return "judge-io-unknown";
@@ -7607,6 +7668,24 @@ function settleJudgeWork(input) {
7607
7668
  testFailpoint: input.testFailpoint
7608
7669
  }));
7609
7670
  }
7671
+ /**
7672
+ * Closes a refused transport from the durable boundary state, not from the transport's advisory `preIo` flag. A
7673
+ * refusal that reached the synchronous physical-I/O gate is already in flight and must settle pessimistically;
7674
+ * every earlier refusal is still reserved and releases without a rolling charge.
7675
+ */
7676
+ function finalizeJudgeTransportRefusal(input) {
7677
+ return input.store.immediate(() => {
7678
+ const row = readReservation(input.store.database, input.reservationId);
7679
+ if (row === null) return null;
7680
+ if (row.state === "released" || row.state === "settled") return row.state;
7681
+ if (row.state === "reserved") return releaseReservationInTransaction(input.store.database, row.id, input.now) ? "released" : null;
7682
+ return settleReservationInTransaction(input.store.database, {
7683
+ reservationId: row.id,
7684
+ result: input.result,
7685
+ now: input.now
7686
+ }) ? "settled" : null;
7687
+ });
7688
+ }
7610
7689
  /** Used only by the shared recovery closure; no other module may reimplement these state transitions. */
7611
7690
  function recoverOneJudgeReservationInTransaction(database, reservationId, now) {
7612
7691
  const row = readReservation(database, reservationId);
@@ -9476,6 +9555,26 @@ async function assertLiveEventAccount(config, scope) {
9476
9555
  source: scope.source
9477
9556
  } });
9478
9557
  }
9558
+ /**
9559
+ * Every connected account a rule may name, loaded afresh: Gmail's inboxes by their stable `ibx_…` id and the other
9560
+ * channels' accounts by their `acc_…` id, each beside the name a person knows it by. What `sources list` shows, so
9561
+ * a first rule's account id can be read from it before any rule is active (CUE-474).
9562
+ */
9563
+ async function connectedEventAccounts(config) {
9564
+ const current = await config.load();
9565
+ const found = [];
9566
+ for (const [account, inbox] of Object.entries(current.inboxes)) if (inbox.provider === "gmail") found.push({
9567
+ source: "gmail",
9568
+ accountId: inbox.id,
9569
+ account
9570
+ });
9571
+ for (const [account, entry] of Object.entries(current.accounts)) if (entry.platform === "slack" || entry.platform === "resend" || entry.platform === "whatsapp") found.push({
9572
+ source: entry.platform,
9573
+ accountId: entry.id,
9574
+ account
9575
+ });
9576
+ return found.sort((left, right) => `${left.source}\u0000${left.account}`.localeCompare(`${right.source}\u0000${right.account}`));
9577
+ }
9479
9578
  /** The current stable ids for one source, loaded afresh at the boundary. */
9480
9579
  async function liveEventAccountIds(config, source) {
9481
9580
  const current = await config.load();
@@ -11468,6 +11567,119 @@ const EVENT_MIGRATIONS = [
11468
11567
  "CREATE INDEX account_revocations_account_source ON account_revocations(account_id, source)",
11469
11568
  "UPDATE meta SET value = '21' WHERE key = 'schema_version'"
11470
11569
  ]
11570
+ },
11571
+ {
11572
+ version: 22,
11573
+ name: "event-judge-budget-attempts-v1",
11574
+ statements: [
11575
+ `CREATE TABLE judge_budget_reservations_v22 (
11576
+ id TEXT PRIMARY KEY,
11577
+ work_id TEXT NOT NULL,
11578
+ work_scope TEXT NOT NULL CHECK (work_scope IN ('source-owned', 'synthetic-test')),
11579
+ source TEXT,
11580
+ account_id TEXT,
11581
+ event_id TEXT,
11582
+ rule_id TEXT,
11583
+ rule_version INTEGER CHECK (rule_version > 0),
11584
+ judge_id TEXT NOT NULL,
11585
+ judge_version INTEGER NOT NULL CHECK (judge_version > 0),
11586
+ judge_kind TEXT NOT NULL CHECK (judge_kind IN (${JUDGE_KIND_SQL_VALUES})),
11587
+ test_work_id TEXT,
11588
+ budget_version INTEGER NOT NULL REFERENCES judge_budget_versions(version),
11589
+ provider TEXT,
11590
+ global_ledger_key TEXT NOT NULL CHECK (global_ledger_key = 'installation'),
11591
+ state TEXT NOT NULL CHECK (state IN ('reserved', 'in-flight', 'settled', 'released')),
11592
+ estimated_input_tokens INTEGER NOT NULL CHECK (estimated_input_tokens >= 0),
11593
+ actual_input_tokens INTEGER CHECK (actual_input_tokens >= 0),
11594
+ concurrency_owned INTEGER NOT NULL CHECK (concurrency_owned IN (0, 1)),
11595
+ reserved_at INTEGER NOT NULL,
11596
+ in_flight_at INTEGER,
11597
+ settled_at INTEGER,
11598
+ released_at INTEGER,
11599
+ switch_generation INTEGER NOT NULL CHECK (switch_generation >= 0),
11600
+ test_authority_rule_id TEXT,
11601
+ test_authority_rule_version INTEGER CHECK (test_authority_rule_version > 0),
11602
+ test_authority_activation_id TEXT,
11603
+ test_authority_approval_id TEXT,
11604
+ test_authority_challenge_hash TEXT CHECK (
11605
+ test_authority_challenge_hash IS NULL
11606
+ OR (length(test_authority_challenge_hash) = 64
11607
+ AND test_authority_challenge_hash NOT GLOB '*[^0-9a-f]*')
11608
+ ),
11609
+ test_authority_challenge_origin TEXT CHECK (test_authority_challenge_origin IN ('terminal', 'view')),
11610
+ test_authority_challenge_expires_at INTEGER,
11611
+ FOREIGN KEY (rule_id, rule_version) REFERENCES rule_versions(rule_id, version),
11612
+ FOREIGN KEY (judge_id, judge_version) REFERENCES judge_versions(judge_id, version),
11613
+ CHECK (
11614
+ (work_scope = 'source-owned'
11615
+ AND source IS NOT NULL
11616
+ AND source IN ('gmail', 'slack', 'resend', 'whatsapp')
11617
+ AND account_id IS NOT NULL
11618
+ AND event_id IS NOT NULL
11619
+ AND rule_id IS NOT NULL
11620
+ AND rule_version IS NOT NULL
11621
+ AND test_work_id IS NULL
11622
+ AND provider IS NOT NULL)
11623
+ OR
11624
+ (work_scope = 'synthetic-test'
11625
+ AND source IS NULL
11626
+ AND account_id IS NULL
11627
+ AND event_id IS NULL
11628
+ AND rule_id IS NULL
11629
+ AND rule_version IS NULL
11630
+ AND test_work_id IS NOT NULL
11631
+ AND provider IS NULL)
11632
+ )
11633
+ ) STRICT`,
11634
+ `INSERT INTO judge_budget_reservations_v22
11635
+ (id, work_id, work_scope, source, account_id, event_id, rule_id, rule_version, judge_id, judge_version,
11636
+ judge_kind, test_work_id, budget_version, provider, global_ledger_key, state, estimated_input_tokens,
11637
+ actual_input_tokens, concurrency_owned, reserved_at, in_flight_at, settled_at, released_at, switch_generation,
11638
+ test_authority_rule_id, test_authority_rule_version, test_authority_activation_id, test_authority_approval_id,
11639
+ test_authority_challenge_hash, test_authority_challenge_origin, test_authority_challenge_expires_at)
11640
+ SELECT id, work_id, work_scope, source, account_id, event_id, rule_id, rule_version, judge_id, judge_version,
11641
+ judge_kind, test_work_id, budget_version, provider, global_ledger_key, state, estimated_input_tokens,
11642
+ actual_input_tokens, concurrency_owned, reserved_at, in_flight_at, settled_at, released_at,
11643
+ switch_generation, test_authority_rule_id, test_authority_rule_version, test_authority_activation_id,
11644
+ test_authority_approval_id, test_authority_challenge_hash, test_authority_challenge_origin,
11645
+ test_authority_challenge_expires_at
11646
+ FROM judge_budget_reservations`,
11647
+ "DROP TABLE judge_budget_reservations",
11648
+ "ALTER TABLE judge_budget_reservations_v22 RENAME TO judge_budget_reservations",
11649
+ `CREATE UNIQUE INDEX judge_budget_reservations_live_work_identity
11650
+ ON judge_budget_reservations(work_id)
11651
+ WHERE state IN ('reserved', 'in-flight')`,
11652
+ `CREATE INDEX judge_budget_reservations_rule_rolling
11653
+ ON judge_budget_reservations(rule_id, reserved_at)
11654
+ WHERE work_scope = 'source-owned'`,
11655
+ `CREATE INDEX judge_budget_reservations_provider_rolling
11656
+ ON judge_budget_reservations(provider, reserved_at)
11657
+ WHERE provider IS NOT NULL`,
11658
+ `CREATE INDEX judge_budget_reservations_global_rolling
11659
+ ON judge_budget_reservations(global_ledger_key, reserved_at)`,
11660
+ `CREATE UNIQUE INDEX judge_budget_reservations_source_scope
11661
+ ON judge_budget_reservations(source, account_id, event_id, rule_id, rule_version)
11662
+ WHERE work_scope = 'source-owned' AND state IN ('reserved', 'in-flight')`,
11663
+ `CREATE INDEX judge_budget_reservations_synthetic_scope
11664
+ ON judge_budget_reservations(test_work_id, judge_id, judge_version, state)
11665
+ WHERE work_scope = 'synthetic-test'`,
11666
+ `CREATE INDEX judge_budget_reservations_kind_scope
11667
+ ON judge_budget_reservations(judge_kind, work_scope, state)`,
11668
+ `CREATE INDEX judge_budget_reservations_synthetic_authority
11669
+ ON judge_budget_reservations(test_authority_rule_id, test_authority_rule_version, test_authority_activation_id,
11670
+ test_authority_approval_id, test_authority_challenge_hash)
11671
+ WHERE work_scope = 'synthetic-test'`,
11672
+ `CREATE TRIGGER judge_budget_reservations_state_machine
11673
+ BEFORE UPDATE OF state ON judge_budget_reservations
11674
+ WHEN NEW.state <> OLD.state AND NOT (
11675
+ (OLD.state = 'reserved' AND NEW.state IN ('in-flight', 'released'))
11676
+ OR (OLD.state = 'in-flight' AND NEW.state = 'settled')
11677
+ )
11678
+ BEGIN
11679
+ SELECT RAISE(ABORT, 'invalid judge reservation state transition');
11680
+ END`,
11681
+ "UPDATE meta SET value = '22' WHERE key = 'schema_version'"
11682
+ ]
11471
11683
  }
11472
11684
  ];
11473
11685
  function initialiseLedger(database) {
@@ -11477,12 +11689,32 @@ function initialiseLedger(database) {
11477
11689
  applied_at INTEGER NOT NULL
11478
11690
  ) STRICT`);
11479
11691
  }
11692
+ /** The highest schema version a database's ledger records, or 0 when it has no ledger yet. It only reads. */
11693
+ function recordedSchemaVersion(database) {
11694
+ if (database.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'").get() === void 0) return 0;
11695
+ return database.prepare("SELECT MAX(version) AS version FROM schema_migrations").get().version ?? 0;
11696
+ }
11697
+ /** Refuses a recorded schema version newer than this binary's, naming both and nothing else. */
11698
+ function refuseNewerVersion(databaseVersion, migrations = EVENT_MIGRATIONS) {
11699
+ const binaryVersion = migrations.at(-1)?.version ?? 0;
11700
+ if (databaseVersion > binaryVersion) throw new Error(`event database schema version ${databaseVersion} is newer than this release's schema version ${binaryVersion}; only the newer release that migrated it can open it`);
11701
+ }
11702
+ /**
11703
+ * Refuses a database a newer agent-events has migrated, by reading its ledger and nothing else: no table is created and
11704
+ * nothing is written, so it runs before the opener sets a journal mode or this file creates its ledger. A database
11705
+ * with no ledger yet is new, and passes.
11706
+ */
11707
+ function refuseNewerSchema(database, migrations = EVENT_MIGRATIONS) {
11708
+ refuseNewerVersion(recordedSchemaVersion(database), migrations);
11709
+ }
11480
11710
  /** Applies ordered daemon-owned migrations, one BEGIN IMMEDIATE transaction per version. */
11481
11711
  function applyMigrations(database, migrations = EVENT_MIGRATIONS, hooks = {}) {
11712
+ refuseNewerSchema(database, migrations);
11482
11713
  initialiseLedger(database);
11714
+ for (const [index, migration] of migrations.entries()) if (migration.version !== index + 1) throw new Error("event migrations must have consecutive versions starting at 1");
11483
11715
  const applied = database.prepare("SELECT version, name FROM schema_migrations ORDER BY version").all();
11484
- for (const [index, migration] of migrations.entries()) {
11485
- if (migration.version !== index + 1) throw new Error("event migrations must have consecutive versions starting at 1");
11716
+ refuseNewerVersion(applied.at(-1)?.version ?? 0, migrations);
11717
+ for (const migration of migrations) {
11486
11718
  const recorded = applied.find((entry) => entry.version === migration.version);
11487
11719
  if (recorded) {
11488
11720
  if (recorded.name !== migration.name) throw new Error(`event migration ${migration.version} does not match its ledger name`);
@@ -11517,18 +11749,44 @@ function installationIdFor(database) {
11517
11749
  database.prepare("INSERT INTO meta (key, value) VALUES (?, ?)").run("installation_id", created);
11518
11750
  return created;
11519
11751
  }
11752
+ /** SQLite's primary result code for a read that has to write first: recovering what a crash interrupted. */
11753
+ const SQLITE_READONLY = 8;
11754
+ /**
11755
+ * The schema version a database records, read through a read-only connection, so that a database a newer agent-events
11756
+ * migrated is refused without this release ever opening it for writing. It waits out a lock as the writable opener
11757
+ * would. Null only when SQLite must first recover an interrupted transaction, which takes a writable connection: the
11758
+ * opener checks again there. Any other failure, a lock held past the wait among them, refuses the open as it is.
11759
+ */
11760
+ function recordedVersionReadOnly(Database, path, busyTimeoutMs = 5e3) {
11761
+ try {
11762
+ const probe = new Database(path, { readOnly: true });
11763
+ try {
11764
+ probe.exec(`PRAGMA busy_timeout = ${Math.trunc(busyTimeoutMs)}`);
11765
+ return recordedSchemaVersion(probe);
11766
+ } finally {
11767
+ probe.close();
11768
+ }
11769
+ } catch (error) {
11770
+ const code = error.errcode;
11771
+ if (typeof code === "number" && (code & 255) === SQLITE_READONLY) return null;
11772
+ throw error;
11773
+ }
11774
+ }
11520
11775
  /** Opens the daemon’s one local authority after the Task 4 owner-only path checks have succeeded. */
11521
11776
  async function openEventDatabase(options) {
11522
11777
  const { DatabaseSync: Database } = await loadSqlite();
11523
11778
  const paths = await ensureEventPaths(eventPaths(options.stateDir));
11524
11779
  await ensureOwnerOnlyFile(paths.database);
11780
+ const recorded = recordedVersionReadOnly(Database, paths.database);
11781
+ if (recorded !== null) refuseNewerVersion(recorded);
11525
11782
  const database = new Database(paths.database);
11526
11783
  try {
11784
+ database.exec("PRAGMA busy_timeout = 5000");
11785
+ refuseNewerSchema(database);
11527
11786
  database.exec("PRAGMA foreign_keys = ON");
11528
11787
  database.exec("PRAGMA journal_mode = WAL");
11529
11788
  database.exec("PRAGMA synchronous = FULL");
11530
11789
  database.exec("PRAGMA secure_delete = ON");
11531
- database.exec("PRAGMA busy_timeout = 5000");
11532
11790
  applyMigrations(database);
11533
11791
  return {
11534
11792
  database,
@@ -11841,16 +12099,20 @@ var ActivationRuntime = class {
11841
12099
  }
11842
12100
  /** A narrowing revocation is allowed to win over a claimed completion, but no other pointer mutation is. */
11843
12101
  async cancelForRevocation(input) {
12102
+ this.#store.immediate(() => {
12103
+ this.cancelForRevocationInTransaction(input);
12104
+ });
12105
+ }
12106
+ /** A caller already holding the removal transaction cancels every matching intent before revocation commits. */
12107
+ cancelForRevocationInTransaction(input) {
11844
12108
  const rows = this.#store.database.prepare("SELECT id, document FROM activation_intents WHERE status IN ('pending', 'pending-completion') ORDER BY id").all();
11845
- const cancelled = [];
12109
+ let cancelled = 0;
11846
12110
  for (const row of rows) {
11847
12111
  if (!this.#isRevokedBinding(JSON.parse(row.document), input)) continue;
11848
- cancelled.push(row.id);
12112
+ this.#cancel(row.id, ACTIVATION_FAILURE.AUTHORIZATION_REVOKED);
12113
+ cancelled += 1;
11849
12114
  }
11850
- if (cancelled.length === 0) return;
11851
- this.#store.immediate(() => {
11852
- for (const intentId of cancelled) this.#cancel(intentId, ACTIVATION_FAILURE.AUTHORIZATION_REVOKED);
11853
- });
12115
+ return cancelled;
11854
12116
  }
11855
12117
  /** Called by the kind-disable transaction; no pending kind approval may recreate the pointer after disable wins. */
11856
12118
  cancelForJudgeKindInTransaction(kind) {
@@ -11858,7 +12120,7 @@ var ActivationRuntime = class {
11858
12120
  let cancelled = 0;
11859
12121
  for (const row of rows) {
11860
12122
  const document = JSON.parse(row.document);
11861
- if (document.kind === "judge-kind" && document.enablement.kind === kind || document.kind === "rule" && document.rule.agentic?.judge.kind === kind) {
12123
+ if (document.kind === "judge-kind" && document.enablement.kind === kind || document.kind === "rule" && document.rule.agentic?.judge.kind === kind || document.kind === "enable-all" && this.#ruleVersionsBindJudgeKind(document.ruleVersions, kind)) {
11862
12124
  this.#cancel(row.id, ACTIVATION_FAILURE.AUTHORIZATION_REVOKED);
11863
12125
  cancelled += 1;
11864
12126
  }
@@ -11976,6 +12238,8 @@ var ActivationRuntime = class {
11976
12238
  const now = this.#now();
11977
12239
  const plannedAccounts = [...new Map(points.map((point) => [`${point.source}\u0000${point.accountId}`, point])).values()];
11978
12240
  this.#store.immediate(() => {
12241
+ this.#assertBoundRuleAuthority(document);
12242
+ this.#assertPreparationPlanLive(document, effect, replacementOfVersion);
11979
12243
  this.#assertJudgeActivationLive(document, effect);
11980
12244
  const latestRevocation = plannedAccounts.reduce((latest, point) => {
11981
12245
  const row = this.#store.database.prepare(`SELECT MAX(revoked_at) AS revoked_at FROM account_revocations
@@ -11988,9 +12252,28 @@ var ActivationRuntime = class {
11988
12252
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`).run(intentId, document.kind, canonicalJson(document), binding.digest, canonicalJson(effect), replacementOfVersion ?? null, canonicalJson(points), canonicalJson([...new Set(points.map((point) => `${point.accountId}:${point.positionScope}`))]), createdAt, createdAt);
11989
12253
  });
11990
12254
  const approval = await this.#approvals.createDisclosure(binding);
12255
+ let settled;
11991
12256
  this.#store.immediate(() => {
11992
- this.#store.database.prepare("UPDATE activation_intents SET approval_id = ?, updated_at = ? WHERE id = ? AND approval_id IS NULL").run(approval.approvalId, this.#now(), intentId);
12257
+ const latest = this.#intent(intentId);
12258
+ const linkApproval = () => this.#store.database.prepare("UPDATE activation_intents SET approval_id = ?, updated_at = ? WHERE id = ? AND approval_id IS NULL").run(approval.approvalId, this.#now(), intentId).changes;
12259
+ if (latest.status !== "pending") {
12260
+ if (linkApproval() !== 1) throw new CommsError("APPROVAL_VOID", "the cancelled activation could not retain its disclosure link");
12261
+ settled = new CommsError("APPROVAL_VOID", "the activation was cancelled before its approval could be linked");
12262
+ return;
12263
+ }
12264
+ try {
12265
+ this.#assertBoundRuleAuthority(document);
12266
+ this.#assertPreparationPlanLive(document, effect, replacementOfVersion);
12267
+ this.#assertJudgeActivationLive(document, effect);
12268
+ } catch (error) {
12269
+ if (linkApproval() !== 1) throw new CommsError("APPROVAL_VOID", "the stale activation could not retain its disclosure link");
12270
+ this.#cancel(intentId, ACTIVATION_FAILURE.APPROVAL_BINDING_DRIFT);
12271
+ settled = error;
12272
+ return;
12273
+ }
12274
+ if (this.#store.database.prepare("UPDATE activation_intents SET approval_id = ?, updated_at = ? WHERE id = ? AND status = 'pending' AND approval_id IS NULL").run(approval.approvalId, this.#now(), intentId).changes !== 1) throw new CommsError("APPROVAL_VOID", "the activation approval link changed before it could be written");
11993
12275
  });
12276
+ if (settled !== void 0) throw settled;
11994
12277
  return {
11995
12278
  intentId,
11996
12279
  approvalId: approval.approvalId,
@@ -12013,7 +12296,7 @@ var ActivationRuntime = class {
12013
12296
  if (allowIssuingChallengeResidue) this.#assertApprovalIssuingChallengeResidue(intent.approval_id);
12014
12297
  if (row.status === "cancelled") throw new CommsError("APPROVAL_VOID", "the activation was cancelled before completion");
12015
12298
  const document = this.#document(row);
12016
- this.#assertRuleJudgeKindEnabled(document);
12299
+ this.#assertBoundRuleAuthority(document);
12017
12300
  if (document.kind === "judge-budget" || document.kind === "judge-kind") try {
12018
12301
  this.#assertJudgeActivationLive(document, JSON.parse(row.effect));
12019
12302
  } catch (_error) {
@@ -12166,7 +12449,7 @@ var ActivationRuntime = class {
12166
12449
  settled = new CommsError("APPROVAL_VOID", "the activation completion deadline has passed");
12167
12450
  return;
12168
12451
  }
12169
- this.#assertRuleJudgeKindEnabled(document);
12452
+ this.#assertBoundRuleAuthority(document);
12170
12453
  const effect = JSON.parse(latest.effect);
12171
12454
  const settings = this.#switch();
12172
12455
  if ((document.kind === "rule" || document.kind === "enable-all") && settings.generation !== effect.switchGeneration) {
@@ -12280,12 +12563,10 @@ var ActivationRuntime = class {
12280
12563
  const document = this.#document(intent);
12281
12564
  const binding = disclosureBindingFor(intent.id, document);
12282
12565
  if (binding.digest !== intent.digest || activationDocumentDigest(document) !== intent.digest) throw new CommsError("APPROVAL_VOID", "the activation document changed after preparation");
12566
+ this.#assertBoundRuleAuthority(document);
12283
12567
  if (document.kind === "rule") {
12284
- const versions = new ImmutableVersions(this.#store.database);
12285
- const active = versions.activeVersion("rule", document.rule.ruleId);
12568
+ const active = new ImmutableVersions(this.#store.database).activeVersion("rule", document.rule.ruleId);
12286
12569
  if (active !== null && (intent.replacement_of_version === null || `${document.rule.ruleId}@${active.version}` !== intent.replacement_of_version)) throw new CommsError("APPROVAL_VOID", "the rule gained an active pointer after preparation");
12287
- const live = versions.prepareRule(document.rule.ruleId, document.rule.version);
12288
- if (canonicalJson(live) !== canonicalJson(document)) throw new CommsError("APPROVAL_VOID", "the rule changed after preparation");
12289
12570
  } else if (document.kind === "enable-all") {
12290
12571
  const current = this.#switch();
12291
12572
  const live = this.#store.database.prepare("SELECT object_id, version, current_cutover_id FROM active_versions WHERE kind = 'rule' ORDER BY object_id, version").all();
@@ -12319,12 +12600,48 @@ var ActivationRuntime = class {
12319
12600
  #judgeKindDisableGeneration(kind) {
12320
12601
  return this.#store.database.prepare("SELECT generation FROM judge_kind_disable_generations WHERE kind = ?").get(kind)?.generation ?? 0;
12321
12602
  }
12322
- /** The absence of the sole judge-kind pointer is disabled; deterministic-only rules need no kind check. */
12323
- #assertRuleJudgeKindEnabled(document) {
12324
- if (document.kind !== "rule" || document.rule.agentic === void 0) return;
12325
- const kind = document.rule.agentic.judge.kind;
12603
+ /** Re-read every exact bound rule, including its embedded judge revocation and kind pointer, at each authority gate. */
12604
+ #assertBoundRuleAuthority(document) {
12605
+ if (document.kind !== "rule" && document.kind !== "enable-all") return;
12326
12606
  const versions = new ImmutableVersions(this.#store.database);
12327
- assertJudgeKindEnabled(kind, versions.activeVersion("judge-kind", kind) !== null);
12607
+ if (document.kind === "rule") {
12608
+ const live = versions.prepareRule(document.rule.ruleId, document.rule.version);
12609
+ if (canonicalJson(live) !== canonicalJson(document)) throw new CommsError("APPROVAL_VOID", "the rule changed after preparation");
12610
+ return;
12611
+ }
12612
+ for (const entry of document.ruleVersions) versions.prepareRule(entry.ruleId, entry.ruleVersion);
12613
+ }
12614
+ /** Preparation may await local stores; the exact pointer plan captured before those awaits must still be current. */
12615
+ #assertPreparationPlanLive(document, effect, replacementOfVersion) {
12616
+ if (document.kind === "rule") {
12617
+ const active = new ImmutableVersions(this.#store.database).activeVersion("rule", document.rule.ruleId);
12618
+ if ((active === null ? null : `${document.rule.ruleId}@${active.version}`) !== (replacementOfVersion ?? null) || this.#switch().generation !== effect.switchGeneration) throw new CommsError("APPROVAL_VOID", "the rule activation pointer plan changed during preparation");
12619
+ return;
12620
+ }
12621
+ if (document.kind !== "enable-all") return;
12622
+ const settings = this.#switch();
12623
+ const live = this.#store.database.prepare("SELECT object_id, version, current_cutover_id FROM active_versions WHERE kind = 'rule' ORDER BY object_id, version").all();
12624
+ if (settings.enabled || settings.generation !== document.switchGeneration || settings.generation !== effect.switchGeneration || canonicalJson(live.map((row) => ({
12625
+ ruleId: row.object_id,
12626
+ ruleVersion: row.version
12627
+ }))) !== canonicalJson(document.ruleVersions) || canonicalJson(live.map((row) => ({
12628
+ ruleId: row.object_id,
12629
+ ruleVersion: row.version,
12630
+ cutoverId: row.current_cutover_id
12631
+ }))) !== canonicalJson(effect.currentCutovers ?? [])) throw new CommsError("APPROVAL_VOID", "the global enablement pointer plan changed during preparation");
12632
+ }
12633
+ /** A damaged captured rule row cannot prove that a kind disable is unrelated, so cancellation is fail-closed. */
12634
+ #ruleVersionsBindJudgeKind(entries, kind) {
12635
+ for (const entry of entries) {
12636
+ const row = this.#store.database.prepare("SELECT document FROM rule_versions WHERE rule_id = ? AND version = ?").get(entry.ruleId, entry.ruleVersion);
12637
+ if (row === void 0) return true;
12638
+ try {
12639
+ if (canonicalFullRuleDocument(JSON.parse(row.document)).agentic?.judge.kind === kind) return true;
12640
+ } catch {
12641
+ return true;
12642
+ }
12643
+ }
12644
+ return false;
12328
12645
  }
12329
12646
  #document(intent) {
12330
12647
  return JSON.parse(intent.document);
@@ -13583,10 +13900,10 @@ var B3OperatorControl = class {
13583
13900
  }).disable(kind);
13584
13901
  }
13585
13902
  async removeJudge(judgeId) {
13586
- await this.#activations?.cancelForRevocation({ judgeId });
13587
13903
  return this.#store.immediate(() => {
13588
13904
  const rows = this.#store.database.prepare("SELECT version FROM judge_versions WHERE judge_id = ? AND revoked_at IS NULL ORDER BY version").all(judgeId);
13589
13905
  if (rows.length === 0) throw new CommsError("NOT_FOUND", "the judge has no live version to remove");
13906
+ this.#activations?.cancelForRevocationInTransaction({ judgeId });
13590
13907
  const revokedAt = this.#now();
13591
13908
  const removedVersions = new Set(rows.map((row) => row.version));
13592
13909
  for (const row of rows) {
@@ -14246,7 +14563,8 @@ var B3TargetTestControl = class {
14246
14563
  SELECT 1 FROM object_revocations AS revoked
14247
14564
  WHERE revoked.kind = 'target' AND revoked.object_id = target.target_id AND revoked.version = target.version
14248
14565
  )`).get(targetId, targetVersion);
14249
- if (row === void 0 || row.enabled !== 1) return refuse("the exact target version is not live");
14566
+ if (row === void 0) return refuse("the exact target version is not live");
14567
+ if (row.enabled !== 1) return refuse("collection is switched off: switch it on with enable-all first, or approve without the test");
14250
14568
  if (selectorKind === "active") {
14251
14569
  if (this.#store.database.prepare(`SELECT 1 AS present
14252
14570
  FROM target_version_references AS reference
@@ -15547,9 +15865,9 @@ var JudgeTransport = class {
15547
15865
  let enteredIo = false;
15548
15866
  try {
15549
15867
  await this.#beforeGate(hooks, "dns");
15550
- this.#finalGate(hooks, "dns");
15551
- assertBeforeDeadline(deadline, this.#clock);
15868
+ this.#finalGate(hooks, "dns", deadline);
15552
15869
  enteredIo = call.judge.kind === JUDGE_KIND_TYPESAFE;
15870
+ assertBeforeDeadline(deadline, this.#clock);
15553
15871
  const connection = await beforeDeadline$1(preparePinnedConnection(call.judge.kind === "typesafe" ? {
15554
15872
  url: endpoint,
15555
15873
  addressMode: "global-only",
@@ -15562,9 +15880,9 @@ var JudgeTransport = class {
15562
15880
  }), deadline, this.#clock);
15563
15881
  assertBeforeDeadline(deadline, this.#clock);
15564
15882
  await this.#beforeGate(hooks, "tcp");
15565
- this.#finalGate(hooks, "tcp");
15566
- assertBeforeDeadline(deadline, this.#clock);
15883
+ this.#finalGate(hooks, "tcp", deadline);
15567
15884
  enteredIo = true;
15885
+ assertBeforeDeadline(deadline, this.#clock);
15568
15886
  const tcp = await beforeDeadline$1(this.#tcpConnect({
15569
15887
  host: connection.address,
15570
15888
  port: connection.port
@@ -15573,7 +15891,7 @@ var JudgeTransport = class {
15573
15891
  socket = tcp;
15574
15892
  if (connection.useTls) {
15575
15893
  await this.#beforeGate(hooks, "tls");
15576
- this.#finalGate(hooks, "tls");
15894
+ this.#finalGate(hooks, "tls", deadline);
15577
15895
  assertBeforeDeadline(deadline, this.#clock);
15578
15896
  socket = await beforeDeadline$1(this.#tlsConnect({
15579
15897
  socket: tcp,
@@ -15582,7 +15900,7 @@ var JudgeTransport = class {
15582
15900
  assertBeforeDeadline(deadline, this.#clock);
15583
15901
  }
15584
15902
  await this.#beforeGate(hooks, "write");
15585
- this.#finalGate(hooks, "write");
15903
+ this.#finalGate(hooks, "write", deadline);
15586
15904
  assertBeforeDeadline(deadline, this.#clock);
15587
15905
  socket.write(request);
15588
15906
  const body = await beforeDeadline$1(boundedResponse(socket), deadline, this.#clock);
@@ -15613,14 +15931,16 @@ var JudgeTransport = class {
15613
15931
  }
15614
15932
  }
15615
15933
  /**
15616
- * At every call site only the synchronous deadline check stands between this gate and the physical B2 network action;
15617
- * no await may be inserted between them.
15934
+ * The deadline is checked after the awaited preparation and again inside the durable production gate immediately
15935
+ * before its state transition. No await may be inserted between this gate and the physical B2 network action.
15618
15936
  */
15619
- #finalGate(hooks, phase) {
15937
+ #finalGate(hooks, phase, deadline) {
15938
+ assertBeforeDeadline(deadline, this.#clock);
15620
15939
  let decision;
15621
15940
  try {
15622
- decision = hooks.finalGate(phase);
15623
- } catch {
15941
+ decision = hooks.finalGate(phase, () => assertBeforeDeadline(deadline, this.#clock));
15942
+ } catch (error) {
15943
+ if (error instanceof DeadlineExceeded) throw error;
15624
15944
  throw new FenceRefused();
15625
15945
  }
15626
15946
  if (decision !== void 0) {
@@ -15798,33 +16118,19 @@ var JudgeTestControl = class {
15798
16118
  } catch (error) {
15799
16119
  return refusal(input.judgeId, input.version, error);
15800
16120
  }
15801
- try {
15802
- if (!beginJudgeTransport({
15803
- store: this.#store,
15804
- reservationId: reservation.id,
15805
- now: this.#now()
15806
- })) return {
15807
- kind: "refused",
15808
- judgeId: input.judgeId,
15809
- version: input.version,
15810
- code: "JUDGE_AUTHORITY_CHANGED"
15811
- };
15812
- } catch (error) {
15813
- return refusal(input.judgeId, input.version, error);
15814
- }
16121
+ const ioGate = new JudgeReservationIoGate({
16122
+ store: this.#store,
16123
+ reservationId: reservation.id,
16124
+ judgeKind: authority.judge.kind,
16125
+ now: this.#now,
16126
+ ...prepared.hostedKeyGeneration === null ? {} : { hostedKey: {
16127
+ judgeId: authority.judge.judgeId,
16128
+ judgeVersion: authority.judge.version,
16129
+ generation: prepared.hostedKeyGeneration
16130
+ } }
16131
+ });
15815
16132
  const result = await this.#transport.execute(call, {
15816
- finalGate: () => {
15817
- assertJudgeReservationStillInFlight({
15818
- store: this.#store,
15819
- reservationId: reservation.id,
15820
- now: this.#now(),
15821
- ...prepared.hostedKeyGeneration === null ? {} : { hostedKey: {
15822
- judgeId: authority.judge.judgeId,
15823
- judgeVersion: authority.judge.version,
15824
- generation: prepared.hostedKeyGeneration
15825
- } }
15826
- });
15827
- },
16133
+ finalGate: ioGate.finalGate,
15828
16134
  preparedRequest: prepared
15829
16135
  });
15830
16136
  if (result.kind === "answer") {
@@ -15844,27 +16150,13 @@ var JudgeTestControl = class {
15844
16150
  reservationId: reservation.id
15845
16151
  };
15846
16152
  }
15847
- if (result.preIo) {
15848
- releaseJudgeWork({
15849
- store: this.#store,
15850
- reservationId: reservation.id,
15851
- now: this.#now(),
15852
- beforeIo: true
15853
- });
15854
- return {
15855
- kind: "deferred",
15856
- judgeId: input.judgeId,
15857
- version: input.version,
15858
- code: result.code,
15859
- reservationId: reservation.id
15860
- };
15861
- }
15862
- settleJudgeWork({
16153
+ finalizeJudgeTransportRefusal({
15863
16154
  store: this.#store,
15864
16155
  reservationId: reservation.id,
15865
16156
  result: result.code === "OUTPUT_TOKEN_LIMIT_EXCEEDED" ? "output-token-limit" : result.code === "NETWORK_DEADLINE_EXCEEDED" ? "timeout" : "transport-failure",
15866
16157
  now: this.#now()
15867
16158
  });
16159
+ if (ioGate.firstGateError !== void 0) return refusal(input.judgeId, input.version, ioGate.firstGateError);
15868
16160
  return {
15869
16161
  kind: "deferred",
15870
16162
  judgeId: input.judgeId,
@@ -16586,61 +16878,44 @@ var JudgeCoordinator = class {
16586
16878
  reservationId: null
16587
16879
  };
16588
16880
  }
16589
- try {
16590
- if (!beginJudgeTransport({
16591
- store: this.#store,
16592
- reservationId: reservation.id,
16593
- now: this.#now()
16594
- })) return {
16595
- kind: "closed",
16596
- reason: "reservation-lost",
16597
- reservationId: reservation.id
16598
- };
16599
- } catch (error) {
16600
- if (error instanceof JudgeBudgetError && error.code === "JUDGE_PAUSED") return {
16601
- kind: "deferred",
16602
- reservationId: reservation.id
16603
- };
16604
- return {
16605
- kind: "closed",
16606
- reason: error instanceof Error && "code" in error ? String(error.code) : "authority-changed",
16607
- reservationId: reservation.id
16608
- };
16609
- }
16881
+ const ioGate = new JudgeReservationIoGate({
16882
+ store: this.#store,
16883
+ reservationId: reservation.id,
16884
+ judgeKind: agentic.judge.kind,
16885
+ now: this.#now,
16886
+ ...prepared.hostedKeyGeneration === null ? {} : { hostedKey: {
16887
+ judgeId: agentic.judge.judgeId,
16888
+ judgeVersion: agentic.judge.version,
16889
+ generation: prepared.hostedKeyGeneration
16890
+ } }
16891
+ });
16610
16892
  const result = await this.#transport.execute(call, {
16611
- finalGate: () => {
16612
- assertJudgeReservationStillInFlight({
16613
- store: this.#store,
16614
- reservationId: reservation.id,
16615
- ...prepared.hostedKeyGeneration === null ? {} : { hostedKey: {
16616
- judgeId: agentic.judge.judgeId,
16617
- judgeVersion: agentic.judge.version,
16618
- generation: prepared.hostedKeyGeneration
16619
- } }
16620
- });
16621
- },
16893
+ finalGate: ioGate.finalGate,
16622
16894
  preparedRequest: prepared
16623
16895
  });
16624
16896
  if (result.kind !== "answer") {
16625
- if (result.preIo === true) {
16626
- releaseJudgeWork({
16627
- store: this.#store,
16628
- reservationId: reservation.id,
16629
- now: this.#now(),
16630
- beforeIo: true
16631
- });
16632
- return {
16633
- kind: "deferred",
16634
- reservationId: reservation.id
16635
- };
16636
- }
16637
16897
  const settlement = result.code === "OUTPUT_TOKEN_LIMIT_EXCEEDED" ? "output-token-limit" : result.code === "NETWORK_DEADLINE_EXCEEDED" ? "timeout" : "unknown";
16638
- settleJudgeWork({
16898
+ const disposition = finalizeJudgeTransportRefusal({
16639
16899
  store: this.#store,
16640
16900
  reservationId: reservation.id,
16641
16901
  result: settlement,
16642
16902
  now: this.#now()
16643
16903
  });
16904
+ if (ioGate.firstGateError !== void 0) {
16905
+ if (ioGate.firstGateError instanceof JudgeBudgetError && ioGate.firstGateError.code === "JUDGE_PAUSED") return {
16906
+ kind: "deferred",
16907
+ reservationId: reservation.id
16908
+ };
16909
+ return {
16910
+ kind: "closed",
16911
+ reason: ioGate.firstGateError instanceof Error && "code" in ioGate.firstGateError ? String(ioGate.firstGateError.code) : "authority-changed",
16912
+ reservationId: reservation.id
16913
+ };
16914
+ }
16915
+ if (disposition === "released") return {
16916
+ kind: "deferred",
16917
+ reservationId: reservation.id
16918
+ };
16644
16919
  return {
16645
16920
  kind: "closed",
16646
16921
  reason: closedReason(result),
@@ -19851,7 +20126,7 @@ function sourceOfDeliveryRule(document) {
19851
20126
  /** Composes every Task 3/4 control behind the authenticated owner; public adapters never reach a runtime class. */
19852
20127
  function createOwnerB3ControlService(options) {
19853
20128
  const now = options.now ?? Date.now;
19854
- const judgeActivations = typeof options.activations.prepareJudgeBudget === "function" && typeof options.activations.prepareJudgeKind === "function" && typeof options.activations.cancelForJudgeKindInTransaction === "function" && typeof options.activations.cancelForRevocation === "function" ? options.activations : void 0;
20129
+ const judgeActivations = typeof options.activations.prepareJudgeBudget === "function" && typeof options.activations.prepareJudgeKind === "function" && typeof options.activations.cancelForJudgeKindInTransaction === "function" && typeof options.activations.cancelForRevocationInTransaction === "function" ? options.activations : void 0;
19855
20130
  const operator = new B3OperatorControl({
19856
20131
  store: options.store,
19857
20132
  ports: options.ports,
@@ -20815,7 +21090,7 @@ async function startOwnerWithLock(options, stateDir, paths, lock, started, onDat
20815
21090
  control = await (options.startControlServer ?? startControlServer)({
20816
21091
  endpoint: instance.endpoint,
20817
21092
  token,
20818
- handle: async (request) => handleControl(owner, lifecycle, database, activations, dryrun, sourceRegistry, writerQuiescer, request, ownerB3Service, doctorReport),
21093
+ handle: async (request) => handleControl(owner, lifecycle, database, activations, dryrun, sourceRegistry, writerQuiescer, request, ownerB3Service, doctorReport, () => connectedEventAccounts(core.config)),
20819
21094
  verifyEndpoint: () => verifyPrivateSocketDirectory(paths)
20820
21095
  });
20821
21096
  if (options.startControlServer === void 0) {
@@ -20846,7 +21121,7 @@ async function runEventOwner(options = {}) {
20846
21121
  await owner.stop();
20847
21122
  }
20848
21123
  }
20849
- async function handleControl(owner, lifecycle, database, activations, dispatcher, sourceRegistry, writerQuiescer, request, b3OwnerService, doctorReport) {
21124
+ async function handleControl(owner, lifecycle, database, activations, dispatcher, sourceRegistry, writerQuiescer, request, b3OwnerService, doctorReport, connectedAccounts = async () => []) {
20850
21125
  if (!isOwnerControlWireOperation(request.operation)) throw new CommsError("USAGE", "the local event control operation is not recognised");
20851
21126
  if (isB3ControlOperation(request.operation)) {
20852
21127
  if (!b3OwnerService) throw new CommsError("USAGE", "the local B3 control operation is not available");
@@ -20889,8 +21164,8 @@ async function handleControl(owner, lifecycle, database, activations, dispatcher
20889
21164
  channel: definition.type.split(".")[0]
20890
21165
  };
20891
21166
  }
20892
- case OWNER_CONTROL_WIRES.sourcesList: return sourceRows(database, sourceRegistry);
20893
- case OWNER_CONTROL_WIRES.sourceShow: return sourceShow(database, sourceRegistry, requiredText(request.args.source, "source"));
21167
+ case OWNER_CONTROL_WIRES.sourcesList: return sourceRows(database, sourceRegistry, await connectedAccounts());
21168
+ case OWNER_CONTROL_WIRES.sourceShow: return sourceShow(database, sourceRegistry, requiredText(request.args.source, "source"), await connectedAccounts());
20894
21169
  case OWNER_CONTROL_WIRES.rulesList: return ruleRows(database);
20895
21170
  case OWNER_CONTROL_WIRES.ruleShow: return ruleShow(database, requiredText(request.args.ruleId, "rule id"));
20896
21171
  case OWNER_CONTROL_WIRES.createRule:
@@ -20963,7 +21238,12 @@ function targetRows(database) {
20963
21238
  function createTarget(database, document) {
20964
21239
  return domain(() => new ImmutableVersions(database.database).createTarget(document));
20965
21240
  }
20966
- function sourceRows(database, sourceRegistry) {
21241
+ /**
21242
+ * Each source's accounts: the scopes active rules read, and every connected account a rule may name, which has no
21243
+ * cursor scope until a rule reads it. Each carries the name it was connected under, so a person, or an agent writing
21244
+ * a first rule, can tell `ibx_…` from `ibx_…` (CUE-474).
21245
+ */
21246
+ function sourceRows(database, sourceRegistry, connected = []) {
20967
21247
  const rows = database.database.prepare("SELECT document FROM rule_versions WHERE state IN ('active', 'superseded')").all();
20968
21248
  const scopes = /* @__PURE__ */ new Map();
20969
21249
  for (const row of rows) {
@@ -20985,17 +21265,28 @@ function sourceRows(database, sourceRegistry) {
20985
21265
  } catch {}
20986
21266
  }
20987
21267
  }
20988
- const ordered = [...scopes.values()].sort((left, right) => `${left.source}\u0000${left.accountId}\u0000${left.cursorScope}`.localeCompare(`${right.source}\u0000${right.accountId}\u0000${right.cursorScope}`));
21268
+ const nameOf = new Map(connected.map((entry) => [`${entry.source}\u0000${entry.accountId}`, entry.account]));
21269
+ const accounts = [...scopes.values()].map((scope) => ({
21270
+ ...scope,
21271
+ account: nameOf.get(`${scope.source}\u0000${scope.accountId}`) ?? null
21272
+ }));
21273
+ for (const entry of connected) if (!accounts.some((row) => row.source === entry.source && row.accountId === entry.accountId)) accounts.push({
21274
+ source: entry.source,
21275
+ accountId: entry.accountId,
21276
+ account: entry.account,
21277
+ cursorScope: null
21278
+ });
21279
+ const ordered = accounts.sort((left, right) => `${left.source}\u0000${left.accountId}\u0000${left.cursorScope ?? ""}`.localeCompare(`${right.source}\u0000${right.accountId}\u0000${right.cursorScope ?? ""}`));
20989
21280
  return sourceRegistry.sources().map((source) => ({
20990
21281
  source,
20991
21282
  accounts: ordered.filter((scope) => scope.source === source)
20992
21283
  }));
20993
21284
  }
20994
- function sourceShow(database, sourceRegistry, source) {
21285
+ function sourceShow(database, sourceRegistry, source, connected = []) {
20995
21286
  if (!sourceRegistry.sources().includes(source)) throw new CommsError("NOT_FOUND", "the requested source is not configured");
20996
21287
  return {
20997
21288
  source,
20998
- accounts: sourceRows(database, sourceRegistry).find((row) => row.source === source).accounts
21289
+ accounts: sourceRows(database, sourceRegistry, connected).find((row) => row.source === source).accounts
20999
21290
  };
21000
21291
  }
21001
21292
  async function acquireWithStaleRecovery(paths) {
@@ -21143,6 +21434,22 @@ function terminalSafeJson(value) {
21143
21434
  function renderApprovalViewForTerminal(result) {
21144
21435
  return escapeForDisplay(renderB3ApprovalPreviewV1(result).terminal);
21145
21436
  }
21437
+ /**
21438
+ * An optional test the owner refused is reported and the approval goes on to its challenge (CUE-475): declining the
21439
+ * test was always allowed, so a test that could not run must not end the approval for the person.
21440
+ */
21441
+ async function optionalTest(label, test) {
21442
+ try {
21443
+ return await test();
21444
+ } catch (error) {
21445
+ if (!isCommsError(error)) throw error;
21446
+ return {
21447
+ ...label,
21448
+ test: "not run",
21449
+ reason: error.message
21450
+ };
21451
+ }
21452
+ }
21146
21453
  /** Runs the only interactive terminal approval path; the owner, not this client, made every pending-target selector. */
21147
21454
  async function runTerminalApprovalFlow(input) {
21148
21455
  const start = await input.start(input.approvalId);
@@ -21150,13 +21457,14 @@ async function runTerminalApprovalFlow(input) {
21150
21457
  const selector = start.pendingTargetSelectors[index];
21151
21458
  if (selector === void 0) continue;
21152
21459
  const answer = await input.terminal.question(`Test pending target ${index + 1} with the fixed synthetic payload before approving? [y/N] `);
21153
- if (/^\s*(?:y|yes)\s*$/iu.test(answer)) input.print(await input.testPendingTarget(selector));
21460
+ if (/^\s*(?:y|yes)\s*$/iu.test(answer)) input.print(await optionalTest({ pendingTarget: index + 1 }, () => input.testPendingTarget(selector)));
21154
21461
  }
21155
21462
  for (let index = 0; index < start.pendingRuleJudgeTestSelectors.length; index += 1) {
21156
21463
  const selector = start.pendingRuleJudgeTestSelectors[index];
21157
21464
  if (selector === void 0) continue;
21158
21465
  const answer = await input.terminal.question(`Test pending rule judge ${index + 1} with the fixed synthetic payload before approving? [y/N] `);
21159
- if (/^\s*(?:y|yes)\s*$/iu.test(answer) && input.testPendingRuleJudge !== void 0) input.print(await input.testPendingRuleJudge(selector));
21466
+ const testPendingRuleJudge = input.testPendingRuleJudge;
21467
+ if (/^\s*(?:y|yes)\s*$/iu.test(answer) && testPendingRuleJudge !== void 0) input.print(await optionalTest({ pendingRuleJudge: index + 1 }, () => testPendingRuleJudge(selector)));
21160
21468
  }
21161
21469
  const answer = await input.terminal.question(`Type this approval challenge: ${escapeForDisplay(start.challenge)}\n> `);
21162
21470
  input.print(await input.approve(input.approvalId, answer));
@@ -21176,8 +21484,13 @@ async function run(argv, deps = {}) {
21176
21484
  stdout: process.stdout,
21177
21485
  stderr: process.stderr
21178
21486
  };
21487
+ if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-V")) {
21488
+ streams.stdout.write(`${VERSION}\n`);
21489
+ return 0;
21490
+ }
21179
21491
  const env = deps.env ?? process.env;
21180
21492
  const program = new Command();
21493
+ runEnvironment.set(program, env);
21181
21494
  let ran = false;
21182
21495
  program.name("agent-events").description("The local event-emission service: one foreground owner and local control clients.").option("--json", "write machine-readable output").option("--no-color", "never colour the output").option("--config-dir <dir>", "pin the configuration directory for this run").option("--state-dir <dir>", "pin the state directory for this run").option("--data-dir <dir>", "pin the data directory for this run").option("--secrets-dir <dir>", "pin the secrets directory for this run").option("--downloads-dir <dir>", "pin the downloads directory for this run").configureOutput({
21183
21496
  writeOut: (text) => streams.stdout.write(escapeForDisplay(text)),
@@ -21610,10 +21923,19 @@ async function run(argv, deps = {}) {
21610
21923
  return 1;
21611
21924
  }
21612
21925
  }
21926
+ /**
21927
+ * The state directory a control client looks in: the one pinned for this run, or the one the environment this CLI was
21928
+ * given resolves to. A caller that passes its own `env` to `run` reaches its own owner, never the one the process
21929
+ * environment names.
21930
+ */
21613
21931
  function controlOptions(program) {
21614
21932
  const { stateDir } = program.opts();
21615
- return stateDir === void 0 ? {} : { stateDir };
21933
+ if (stateDir !== void 0) return { stateDir };
21934
+ const env = runEnvironment.get(program);
21935
+ return env === void 0 ? {} : { stateDir: resolvePaths({ env }).stateDir };
21616
21936
  }
21937
+ /** The environment each program was made with, read by `controlOptions` outside `run`'s closure. */
21938
+ const runEnvironment = /* @__PURE__ */ new WeakMap();
21617
21939
  function parseVersion(value) {
21618
21940
  const version = Number(value);
21619
21941
  if (!Number.isSafeInteger(version) || version < 1) throw new CommsError("USAGE", "rule version must be a positive integer");