@sanctuary-framework/mcp-server 1.2.5 → 1.2.7

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/index.cjs CHANGED
@@ -4922,7 +4922,8 @@ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4922
4922
  var RESERVED_EVENT_TYPE_PREFIXES = [
4923
4923
  "EXTENSION_",
4924
4924
  "cross_fortress_",
4925
- "multi_master_"
4925
+ "multi_master_",
4926
+ "cross_harness_approval_"
4926
4927
  ];
4927
4928
  function isReservedEventType(s) {
4928
4929
  return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
@@ -9102,9 +9103,9 @@ function fingerprintDID(did) {
9102
9103
  return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
9103
9104
  }
9104
9105
  function countInjectionsToday(audit) {
9105
- const startOfDay = /* @__PURE__ */ new Date();
9106
- startOfDay.setHours(0, 0, 0, 0);
9107
- const cutoff = startOfDay.getTime();
9106
+ const startOfDay2 = /* @__PURE__ */ new Date();
9107
+ startOfDay2.setHours(0, 0, 0, 0);
9108
+ const cutoff = startOfDay2.getTime();
9108
9109
  return audit.filter((e) => {
9109
9110
  const ts = new Date(e.timestamp).getTime();
9110
9111
  if (isNaN(ts) || ts < cutoff) return false;
@@ -9118,9 +9119,9 @@ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
9118
9119
  "proof_commitment"
9119
9120
  ]);
9120
9121
  function countProofsToday(audit) {
9121
- const startOfDay = /* @__PURE__ */ new Date();
9122
- startOfDay.setHours(0, 0, 0, 0);
9123
- const cutoff = startOfDay.getTime();
9122
+ const startOfDay2 = /* @__PURE__ */ new Date();
9123
+ startOfDay2.setHours(0, 0, 0, 0);
9124
+ const cutoff = startOfDay2.getTime();
9124
9125
  return audit.filter((e) => {
9125
9126
  if (e.layer !== "l3") return false;
9126
9127
  if (!PROOF_CREATION_OPS.has(e.operation)) return false;
@@ -16319,6 +16320,45 @@ async function handleApprovalInboxRoute(deps, req, res) {
16319
16320
  await handleStream2(deps, res);
16320
16321
  return true;
16321
16322
  }
16323
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/revision`) {
16324
+ const revision = await deps.aggregator.getRevision();
16325
+ writeJSON4(res, 200, { ok: true, data: { revision } });
16326
+ return true;
16327
+ }
16328
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/sync`) {
16329
+ const sinceRaw = url.searchParams.get("since_revision");
16330
+ const sinceParsed = sinceRaw === null ? 0 : Number.parseInt(sinceRaw, 10);
16331
+ const sinceRevision = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : 0;
16332
+ const limit = parseLimit2(
16333
+ url.searchParams.get("limit"),
16334
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16335
+ APPROVAL_INBOX_MAX_LIMIT
16336
+ );
16337
+ const delta = await deps.aggregator.getSync({ sinceRevision, limit });
16338
+ writeJSON4(res, 200, { ok: true, data: delta });
16339
+ return true;
16340
+ }
16341
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
16342
+ const limit = parseLimit2(
16343
+ url.searchParams.get("limit"),
16344
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16345
+ APPROVAL_INBOX_MAX_LIMIT
16346
+ );
16347
+ const statusRaw = url.searchParams.get("status");
16348
+ const sinceTs = url.searchParams.get("since") ?? void 0;
16349
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16350
+ const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
16351
+ const entries = await deps.aggregator.getHistory(
16352
+ {
16353
+ limit,
16354
+ ...filterStatus !== void 0 ? { status: filterStatus } : {},
16355
+ ...sinceTs !== void 0 ? { sinceTs } : {}
16356
+ },
16357
+ operatorId
16358
+ );
16359
+ writeJSON4(res, 200, { ok: true, data: { entries } });
16360
+ return true;
16361
+ }
16322
16362
  if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
16323
16363
  const limit = parseLimit2(
16324
16364
  url.searchParams.get("limit"),
@@ -16341,11 +16381,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
16341
16381
  writeJSON4(res, 404, { ok: false, error: "not_found", path });
16342
16382
  return true;
16343
16383
  }
16344
- if (method === "GET" && entryMatch.action === null) {
16345
- const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
16346
- const entry = entries.find(
16347
- (e) => e.aggregator_id === entryMatch.aggregatorId
16384
+ if (method === "GET" && entryMatch.action === "audit-trail") {
16385
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16386
+ if (!entry) {
16387
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16388
+ return true;
16389
+ }
16390
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16391
+ const trail = await deps.aggregator.getAuditTrail(
16392
+ entryMatch.aggregatorId,
16393
+ operatorId
16348
16394
  );
16395
+ writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
16396
+ return true;
16397
+ }
16398
+ if (method === "GET" && entryMatch.action === "payload") {
16399
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16400
+ if (!entry) {
16401
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16402
+ return true;
16403
+ }
16404
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16405
+ const payload = await deps.aggregator.getFullPayloadWithAudit(
16406
+ entryMatch.aggregatorId,
16407
+ operatorId
16408
+ );
16409
+ writeJSON4(res, 200, {
16410
+ ok: true,
16411
+ data: { entry, request_payload: payload }
16412
+ });
16413
+ return true;
16414
+ }
16415
+ if (method === "GET" && entryMatch.action === null) {
16416
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16349
16417
  if (!entry) {
16350
16418
  writeJSON4(res, 404, { ok: false, error: "not_found" });
16351
16419
  return true;
@@ -19332,7 +19400,10 @@ var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
19332
19400
  var APPROVAL_AGGREGATOR_AUDIT_OPS = {
19333
19401
  AGGREGATED: "cross_harness_approval_aggregated",
19334
19402
  RESOLVED: "cross_harness_approval_resolved",
19335
- DEDUPED: "cross_harness_approval_deduped"
19403
+ DEDUPED: "cross_harness_approval_deduped",
19404
+ PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
19405
+ AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
19406
+ REPLAYED: "cross_harness_approval_replayed"
19336
19407
  };
19337
19408
  var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
19338
19409
  var DEFAULT_MAX_LIST_LIMIT = 200;
@@ -19348,6 +19419,8 @@ var ApprovalAggregator = class {
19348
19419
  now;
19349
19420
  resolveSourceContext;
19350
19421
  resolveHubInboxItemId;
19422
+ payloadStore;
19423
+ resolveEnforcementChain;
19351
19424
  /** Cached entries by `aggregator_id`. */
19352
19425
  entries = /* @__PURE__ */ new Map();
19353
19426
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -19360,6 +19433,20 @@ var ApprovalAggregator = class {
19360
19433
  hydrated = false;
19361
19434
  /** Active SSE listeners. */
19362
19435
  listeners = /* @__PURE__ */ new Set();
19436
+ /**
19437
+ * Monotonic revision counter, bumped on every mutation (ingest of new
19438
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
19439
+ * across persisted entries on first read; in-memory after that. v1.3
19440
+ * Upsilon-4.
19441
+ */
19442
+ currentRevision = 0;
19443
+ /**
19444
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
19445
+ * sync API to surface "removed" entries to mobile consumers between
19446
+ * polls. In-memory only; server restart clears tombstones (mobile
19447
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
19448
+ */
19449
+ removedTombstones = /* @__PURE__ */ new Map();
19363
19450
  constructor(deps) {
19364
19451
  this.storage = deps.storage;
19365
19452
  this.encryptionKey = derivePurposeKey(
@@ -19377,6 +19464,14 @@ var ApprovalAggregator = class {
19377
19464
  source_agent_id: this.fortressId
19378
19465
  }));
19379
19466
  this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
19467
+ this.payloadStore = deps.payloadStore ?? null;
19468
+ this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
19469
+ {
19470
+ layer: "l2",
19471
+ event: `approval_required:${event.operation}`,
19472
+ timestamp: event.request_timestamp
19473
+ }
19474
+ ]);
19380
19475
  }
19381
19476
  /**
19382
19477
  * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
@@ -19386,6 +19481,113 @@ var ApprovalAggregator = class {
19386
19481
  this.listeners.add(listener);
19387
19482
  return () => this.listeners.delete(listener);
19388
19483
  }
19484
+ /**
19485
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
19486
+ * poll the lightweight `/revision` route to detect that something
19487
+ * changed before fetching a full sync delta.
19488
+ */
19489
+ async getRevision() {
19490
+ await this.hydrate();
19491
+ return this.currentRevision;
19492
+ }
19493
+ /**
19494
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
19495
+ * clients poll this for cheap state-sync. Behavior:
19496
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
19497
+ * - `changed`: entries that existed at `sinceRevision` but had a
19498
+ * status transition (resolve, expire) since.
19499
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
19500
+ * - `revision`: current aggregator revision; pass this back as
19501
+ * `sinceRevision` on the next call.
19502
+ *
19503
+ * `limit` caps the total count returned across all three lists,
19504
+ * prioritized as added -> changed -> removed (newer-state first).
19505
+ * When more changes exist than fit, the next call with the returned
19506
+ * revision will pick up the rest because each entry's
19507
+ * last_modified_revision is unchanged by truncation.
19508
+ */
19509
+ async getSync(opts) {
19510
+ await this.hydrate();
19511
+ await this.expireStale();
19512
+ const sinceRevision = opts?.sinceRevision ?? 0;
19513
+ const cap = Math.min(
19514
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19515
+ this.maxListLimit
19516
+ );
19517
+ const added = [];
19518
+ const changed = [];
19519
+ for (const entry of this.entries.values()) {
19520
+ const lastMod = entry.last_modified_revision ?? 0;
19521
+ if (lastMod <= sinceRevision) continue;
19522
+ const createdRev = entry.created_at_revision ?? 0;
19523
+ if (createdRev > sinceRevision) {
19524
+ added.push(entry);
19525
+ } else {
19526
+ changed.push(entry);
19527
+ }
19528
+ }
19529
+ const removed = [];
19530
+ for (const [id, rev] of this.removedTombstones) {
19531
+ if (rev > sinceRevision) removed.push(id);
19532
+ }
19533
+ added.sort(
19534
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19535
+ );
19536
+ changed.sort(
19537
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19538
+ );
19539
+ let remaining = cap;
19540
+ const addedOut = added.slice(0, Math.max(0, remaining));
19541
+ remaining -= addedOut.length;
19542
+ const changedOut = changed.slice(0, Math.max(0, remaining));
19543
+ remaining -= changedOut.length;
19544
+ const removedOut = removed.slice(0, Math.max(0, remaining));
19545
+ return {
19546
+ revision: this.currentRevision,
19547
+ added: addedOut,
19548
+ changed: changedOut,
19549
+ removed: removedOut
19550
+ };
19551
+ }
19552
+ /**
19553
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
19554
+ * and the at-rest payload (if a payload store is wired). Records a
19555
+ * tombstone with the new revision so sync-API consumers see a
19556
+ * `removed` delta. Returns true when an entry was deleted, false on
19557
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
19558
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
19559
+ * removal path.
19560
+ */
19561
+ async deleteEntry(aggregatorId) {
19562
+ await this.hydrate();
19563
+ const entry = this.entries.get(aggregatorId);
19564
+ if (!entry) return false;
19565
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19566
+ this.entries.delete(aggregatorId);
19567
+ this.dedupIndex.delete(dedupKey);
19568
+ this.fullPayloads.delete(aggregatorId);
19569
+ for (const [corr, id] of this.correlationIndex) {
19570
+ if (id === aggregatorId) this.correlationIndex.delete(corr);
19571
+ }
19572
+ try {
19573
+ await this.storage.delete(APPROVAL_AGGREGATOR_NAMESPACE, aggregatorId);
19574
+ } catch {
19575
+ }
19576
+ if (this.payloadStore) {
19577
+ try {
19578
+ await this.payloadStore.deletePayload(aggregatorId);
19579
+ } catch {
19580
+ }
19581
+ }
19582
+ const revision = this.nextRevision();
19583
+ this.removedTombstones.set(aggregatorId, revision);
19584
+ this.emit({ type: "removed", entry: { ...entry } });
19585
+ return true;
19586
+ }
19587
+ nextRevision() {
19588
+ this.currentRevision += 1;
19589
+ return this.currentRevision;
19590
+ }
19389
19591
  /**
19390
19592
  * Ingest a gate event. Returns the aggregator entry on first sight,
19391
19593
  * `null` when deduped. Resolution events update the existing record;
@@ -19427,13 +19629,152 @@ var ApprovalAggregator = class {
19427
19629
  }
19428
19630
  /**
19429
19631
  * Return the original (unhashed) request payload for the entry. Returns
19430
- * `null` when the entry is unknown or the payload was evicted (e.g. the
19431
- * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
19632
+ * `null` when the entry is unknown. When the in-memory payload map has
19633
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
19634
+ * provided, the at-rest bundle is decrypted and the in-memory map is
19635
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
19636
+ * accessor is silent so internal callers can read without polluting the
19637
+ * audit trail.
19432
19638
  */
19433
19639
  async getFullPayload(aggregatorId) {
19434
19640
  await this.hydrate();
19435
19641
  if (!this.entries.has(aggregatorId)) return null;
19436
- return this.fullPayloads.get(aggregatorId) ?? null;
19642
+ const cached = this.fullPayloads.get(aggregatorId);
19643
+ if (cached !== void 0) return cached;
19644
+ if (this.payloadStore) {
19645
+ try {
19646
+ const restored = await this.payloadStore.loadPayload(aggregatorId);
19647
+ if (restored !== null) {
19648
+ this.fullPayloads.set(aggregatorId, restored);
19649
+ return restored;
19650
+ }
19651
+ } catch {
19652
+ }
19653
+ }
19654
+ return null;
19655
+ }
19656
+ /**
19657
+ * Return the entry record for the given id, or null when unknown.
19658
+ * Idempotent. v1.3 Upsilon-3.
19659
+ */
19660
+ async getEntry(aggregatorId) {
19661
+ await this.hydrate();
19662
+ return this.entries.get(aggregatorId) ?? null;
19663
+ }
19664
+ /**
19665
+ * Audited variant of `getFullPayload`. Emits the
19666
+ * `cross_harness_approval_payload_decrypted` audit event before
19667
+ * returning. Used by the operator-facing /payload replay route.
19668
+ * v1.3 Upsilon-3.
19669
+ */
19670
+ async getFullPayloadWithAudit(aggregatorId, operatorId) {
19671
+ const payload = await this.getFullPayload(aggregatorId);
19672
+ if (payload === null) return null;
19673
+ const entry = this.entries.get(aggregatorId);
19674
+ this.auditLog.append(
19675
+ "l2",
19676
+ APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
19677
+ operatorId,
19678
+ {
19679
+ aggregator_id: aggregatorId,
19680
+ ...entry ? {
19681
+ source_harness: entry.source_harness,
19682
+ source_agent_id: entry.source_agent_id,
19683
+ entry_status: entry.status
19684
+ } : {}
19685
+ }
19686
+ );
19687
+ return payload;
19688
+ }
19689
+ /**
19690
+ * Return the audit-log entries that led to and surround this approval.
19691
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
19692
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
19693
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
19694
+ * aggregator id at v1.3, so they are matched via timestamp window
19695
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
19696
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
19697
+ * v1.3 Upsilon-3.
19698
+ */
19699
+ async getAuditTrail(aggregatorId, operatorId) {
19700
+ await this.hydrate();
19701
+ const entry = this.entries.get(aggregatorId);
19702
+ if (!entry) {
19703
+ return [];
19704
+ }
19705
+ const sinceMs = Date.parse(entry.created_at) - 1e3;
19706
+ const sinceIso = new Date(sinceMs).toISOString();
19707
+ const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
19708
+ const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
19709
+ const lifetimeStart = sinceMs;
19710
+ const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
19711
+ const matches = [];
19712
+ for (const audit of queried.entries) {
19713
+ const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
19714
+ if (detailsId === aggregatorId) {
19715
+ matches.push(audit);
19716
+ continue;
19717
+ }
19718
+ const auditMs = Date.parse(audit.timestamp);
19719
+ if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
19720
+ if (audit.operation.endsWith(`:${operationPart}`)) {
19721
+ matches.push(audit);
19722
+ }
19723
+ }
19724
+ matches.sort(
19725
+ (a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
19726
+ );
19727
+ this.auditLog.append(
19728
+ "l2",
19729
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
19730
+ operatorId,
19731
+ {
19732
+ aggregator_id: aggregatorId,
19733
+ entry_status: entry.status,
19734
+ match_count: matches.length
19735
+ }
19736
+ );
19737
+ return matches;
19738
+ }
19739
+ /**
19740
+ * List historical (resolved) approvals. Excludes pending entries by
19741
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
19742
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
19743
+ * Upsilon-3.
19744
+ */
19745
+ async getHistory(opts, operatorId) {
19746
+ await this.hydrate();
19747
+ await this.expireStale();
19748
+ const limit = Math.min(
19749
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19750
+ this.maxListLimit
19751
+ );
19752
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
19753
+ const matching = [];
19754
+ for (const entry of this.entries.values()) {
19755
+ if (entry.status === "pending") continue;
19756
+ if (opts?.status && entry.status !== opts.status) continue;
19757
+ const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
19758
+ if (stamp < sinceMs) continue;
19759
+ matching.push(entry);
19760
+ }
19761
+ matching.sort((a, b) => {
19762
+ const aStamp = a.resolved_at ?? a.created_at;
19763
+ const bStamp = b.resolved_at ?? b.created_at;
19764
+ return bStamp.localeCompare(aStamp);
19765
+ });
19766
+ const sliced = matching.slice(0, limit);
19767
+ this.auditLog.append(
19768
+ "l2",
19769
+ APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
19770
+ operatorId,
19771
+ {
19772
+ result_count: sliced.length,
19773
+ ...opts?.status !== void 0 ? { status_filter: opts.status } : {},
19774
+ ...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
19775
+ }
19776
+ );
19777
+ return sliced;
19437
19778
  }
19438
19779
  /**
19439
19780
  * Resolve an entry. Used by both:
@@ -19457,6 +19798,7 @@ var ApprovalAggregator = class {
19457
19798
  entry.status = decision;
19458
19799
  entry.resolved_at = this.now().toISOString();
19459
19800
  entry.resolved_by = operatorId;
19801
+ entry.last_modified_revision = this.nextRevision();
19460
19802
  await this.persist(entry);
19461
19803
  this.auditLog.append(
19462
19804
  "l2",
@@ -19507,6 +19849,8 @@ var ApprovalAggregator = class {
19507
19849
  const now = this.now();
19508
19850
  const expires = new Date(now.getTime() + this.pendingTtlMs);
19509
19851
  const hubInboxId = this.resolveHubInboxItemId(event);
19852
+ const enforcementChain = this.resolveEnforcementChain(event);
19853
+ const revision = this.nextRevision();
19510
19854
  const entry = {
19511
19855
  aggregator_id: id,
19512
19856
  source_harness: ctx.source_harness,
@@ -19518,13 +19862,22 @@ var ApprovalAggregator = class {
19518
19862
  status: "pending",
19519
19863
  created_at: now.toISOString(),
19520
19864
  expires_at: expires.toISOString(),
19521
- ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
19865
+ created_at_revision: revision,
19866
+ last_modified_revision: revision,
19867
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
19868
+ ...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
19522
19869
  };
19523
19870
  this.entries.set(id, entry);
19524
19871
  this.dedupIndex.set(dedupKey, id);
19525
19872
  this.correlationIndex.set(event.correlation_id, id);
19526
19873
  this.fullPayloads.set(id, event.context);
19527
19874
  await this.persist(entry);
19875
+ if (this.payloadStore) {
19876
+ try {
19877
+ await this.payloadStore.savePayload(id, event.context);
19878
+ } catch {
19879
+ }
19880
+ }
19528
19881
  this.auditLog.append(
19529
19882
  "l2",
19530
19883
  APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
@@ -19553,6 +19906,7 @@ var ApprovalAggregator = class {
19553
19906
  entry.status = status;
19554
19907
  entry.resolved_at = event.resolution.decided_at;
19555
19908
  entry.resolved_by = event.resolution.decided_by;
19909
+ entry.last_modified_revision = this.nextRevision();
19556
19910
  await this.persist(entry);
19557
19911
  this.auditLog.append(
19558
19912
  "l2",
@@ -19615,6 +19969,7 @@ var ApprovalAggregator = class {
19615
19969
  entry.status = "expired";
19616
19970
  entry.resolved_at = this.now().toISOString();
19617
19971
  entry.resolved_by = "system_ttl";
19972
+ entry.last_modified_revision = this.nextRevision();
19618
19973
  await this.persist(entry);
19619
19974
  this.auditLog.append(
19620
19975
  "l2",
@@ -19661,6 +20016,10 @@ var ApprovalAggregator = class {
19661
20016
  this.entries.set(entry.aggregator_id, entry);
19662
20017
  const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19663
20018
  this.dedupIndex.set(dedupKey, entry.aggregator_id);
20019
+ const lastMod = entry.last_modified_revision ?? 0;
20020
+ if (lastMod > this.currentRevision) {
20021
+ this.currentRevision = lastMod;
20022
+ }
19664
20023
  } catch {
19665
20024
  }
19666
20025
  }
@@ -19833,6 +20192,143 @@ function makeRedirectResolverFromPolicySupplier(supplier) {
19833
20192
  };
19834
20193
  }
19835
20194
 
20195
+ // src/principal-policy/aggregator-store.ts
20196
+ init_encryption();
20197
+ init_encoding();
20198
+ var AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
20199
+ var AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
20200
+ var HKDF_INFO = "l2-approval-aggregator-payload-v1";
20201
+ var DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
20202
+ var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
20203
+ var AggregatorPayloadStore = class {
20204
+ storage;
20205
+ encryptionKey;
20206
+ fortressId;
20207
+ retentionDays;
20208
+ constructor(opts) {
20209
+ this.storage = opts.storage;
20210
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
20211
+ this.fortressId = opts.fortressId;
20212
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
20213
+ }
20214
+ /**
20215
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
20216
+ * twice with the same id rewrites the bundle (retention_until is
20217
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
20218
+ * so callers can log it.
20219
+ */
20220
+ async savePayload(aggregatorId, payload) {
20221
+ const now = /* @__PURE__ */ new Date();
20222
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
20223
+ const retentionUntil = new Date(now.getTime() + retentionMs);
20224
+ const bundle = {
20225
+ version: 1,
20226
+ aggregator_id: aggregatorId,
20227
+ fortress_id: this.fortressId,
20228
+ created_at: now.toISOString(),
20229
+ retention_until: retentionUntil.toISOString(),
20230
+ payload
20231
+ };
20232
+ const aad = stringToBytes(aggregatorId);
20233
+ const plaintext = stringToBytes(JSON.stringify(bundle));
20234
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
20235
+ await this.storage.write(
20236
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20237
+ payloadKey(aggregatorId),
20238
+ stringToBytes(JSON.stringify(envelope))
20239
+ );
20240
+ return bundle.retention_until;
20241
+ }
20242
+ /**
20243
+ * Read the persisted payload for the aggregator_id. Returns null if no
20244
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
20245
+ */
20246
+ async loadPayload(aggregatorId) {
20247
+ const key = payloadKey(aggregatorId);
20248
+ let raw;
20249
+ try {
20250
+ raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
20251
+ } catch {
20252
+ return null;
20253
+ }
20254
+ if (!raw) return null;
20255
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
20256
+ try {
20257
+ const envelope = JSON.parse(bytesToString(raw));
20258
+ const aad = stringToBytes(aggregatorId);
20259
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
20260
+ const parsed = JSON.parse(
20261
+ bytesToString(plaintext)
20262
+ );
20263
+ if (parsed.version !== 1) return null;
20264
+ if (parsed.aggregator_id !== aggregatorId) return null;
20265
+ return parsed.payload;
20266
+ } catch {
20267
+ return null;
20268
+ }
20269
+ }
20270
+ /**
20271
+ * Delete the persisted payload. Returns true when a bundle was removed,
20272
+ * false when none existed.
20273
+ */
20274
+ async deletePayload(aggregatorId) {
20275
+ const key = payloadKey(aggregatorId);
20276
+ const existed = await this.storage.exists(
20277
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20278
+ key
20279
+ );
20280
+ if (!existed) return false;
20281
+ try {
20282
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
20283
+ } catch {
20284
+ return false;
20285
+ }
20286
+ return true;
20287
+ }
20288
+ /**
20289
+ * Drop expired payload bundles. Returns the count of bundles pruned.
20290
+ * Caller wires this into the cocoon-unlock initialization path.
20291
+ */
20292
+ async pruneExpired(now) {
20293
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
20294
+ const entries = await this.storage.list(
20295
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20296
+ AGGREGATOR_PAYLOAD_KEY_PREFIX
20297
+ );
20298
+ let pruned = 0;
20299
+ for (const meta of entries) {
20300
+ const aggregatorId = stripKeyPrefix(meta.key);
20301
+ if (aggregatorId === null) continue;
20302
+ const raw = await this.storage.read(
20303
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20304
+ meta.key
20305
+ );
20306
+ if (!raw) continue;
20307
+ try {
20308
+ const envelope = JSON.parse(bytesToString(raw));
20309
+ const aad = stringToBytes(aggregatorId);
20310
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
20311
+ const parsed = JSON.parse(
20312
+ bytesToString(plaintext)
20313
+ );
20314
+ if (parsed.retention_until <= cutoff) {
20315
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
20316
+ pruned += 1;
20317
+ }
20318
+ } catch {
20319
+ }
20320
+ }
20321
+ return { pruned };
20322
+ }
20323
+ };
20324
+ function payloadKey(aggregatorId) {
20325
+ return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
20326
+ }
20327
+ function stripKeyPrefix(key) {
20328
+ if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
20329
+ return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
20330
+ }
20331
+
19836
20332
  // src/principal-policy/tools.ts
19837
20333
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
19838
20334
  return [
@@ -31974,6 +32470,13 @@ init_encoding();
31974
32470
  // src/chat/operator-chat-audit-events.ts
31975
32471
  var OPERATOR_CHAT_OPS = {
31976
32472
  CONCIERGE_CHAT: "operator_concierge_chat",
32473
+ /**
32474
+ * Click-to-inspect panel opened on an agent row. Repurposed from the
32475
+ * direct-agent session-open audit event in the v1.2 reshape; the click
32476
+ * affordance now opens an inspect/approve panel (recent activity +
32477
+ * pending approvals + policy summary) instead of a chat session.
32478
+ */
32479
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
31977
32480
  /**
31978
32481
  * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
31979
32482
  * when the operator hits the list-threads or read-thread route. Body
@@ -31993,20 +32496,831 @@ var OPERATOR_CHAT_OPS = {
31993
32496
  * turns; the concierge degrades to single-turn after emitting. Body
31994
32497
  * carries thread_id + a stable failure_reason enum.
31995
32498
  */
31996
- CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
32499
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
32500
+ /**
32501
+ * Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
32502
+ * when a category fetcher throws while assembling the dynamic context
32503
+ * fold. The concierge omits that category and continues; the user-
32504
+ * facing query is never broken. Body carries category + failure_reason.
32505
+ */
32506
+ CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
31997
32507
  };
31998
32508
 
31999
32509
  // src/chat/operator-chat-types.ts
32000
32510
  var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
32001
32511
  var CONCIERGE_THREAD_KEY = "_fortress";
32002
32512
 
32513
+ // src/chat/concierge-context-router.ts
32514
+ var APPROX_CHARS_PER_TOKEN = 4;
32515
+ var DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
32516
+ var DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
32517
+ var CONTEXT_CATEGORIES = [
32518
+ "templates",
32519
+ "agent_state",
32520
+ "agent_activity",
32521
+ "audit_log",
32522
+ "sentinel_findings",
32523
+ "anomaly_alerts",
32524
+ "recent_receipts",
32525
+ "verascore_deltas"
32526
+ ];
32527
+ function phrasePattern(phrase) {
32528
+ const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
32529
+ return { source: `\\b${escaped}\\b`, phrase };
32530
+ }
32531
+ var CATEGORY_KEYWORDS = [
32532
+ {
32533
+ category: "templates",
32534
+ patterns: [
32535
+ "templates",
32536
+ "template",
32537
+ "channel templates",
32538
+ "channel template",
32539
+ "list templates",
32540
+ "available templates",
32541
+ "what templates"
32542
+ ].map(phrasePattern)
32543
+ },
32544
+ {
32545
+ category: "agent_state",
32546
+ patterns: [
32547
+ "state",
32548
+ "status",
32549
+ "agent state",
32550
+ "agent status",
32551
+ "status of agent",
32552
+ "status of agents",
32553
+ "state of",
32554
+ "doing"
32555
+ ].map(phrasePattern)
32556
+ },
32557
+ {
32558
+ category: "agent_activity",
32559
+ patterns: [
32560
+ "activity",
32561
+ "agent activity",
32562
+ "what did",
32563
+ "recent activity"
32564
+ ].map(phrasePattern)
32565
+ },
32566
+ {
32567
+ category: "audit_log",
32568
+ patterns: [
32569
+ "audit log",
32570
+ "audit",
32571
+ "log entry",
32572
+ "log entries",
32573
+ "what happened",
32574
+ "show me events",
32575
+ "event class"
32576
+ ].map(phrasePattern)
32577
+ },
32578
+ {
32579
+ category: "sentinel_findings",
32580
+ patterns: [
32581
+ "sentinel",
32582
+ "sentinels",
32583
+ "warning",
32584
+ "warnings",
32585
+ "alert",
32586
+ "alerts",
32587
+ "whats wrong",
32588
+ "what's wrong",
32589
+ "findings"
32590
+ ].map(phrasePattern)
32591
+ },
32592
+ {
32593
+ category: "anomaly_alerts",
32594
+ patterns: [
32595
+ "anomaly",
32596
+ "anomalies",
32597
+ "spike",
32598
+ "unusual",
32599
+ "outlier"
32600
+ ].map(phrasePattern)
32601
+ },
32602
+ {
32603
+ category: "recent_receipts",
32604
+ patterns: [
32605
+ "receipt",
32606
+ "receipts",
32607
+ "concordia",
32608
+ "commitment",
32609
+ "commitments",
32610
+ "chain",
32611
+ "chains"
32612
+ ].map(phrasePattern)
32613
+ },
32614
+ {
32615
+ category: "verascore_deltas",
32616
+ patterns: [
32617
+ "verascore",
32618
+ "vera score",
32619
+ "trust score",
32620
+ "reputation"
32621
+ ].map(phrasePattern)
32622
+ }
32623
+ ];
32624
+ function extractAgentNameHint(query) {
32625
+ const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
32626
+ const m = query.match(agentPattern);
32627
+ if (m && m[1]) return m[1];
32628
+ const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
32629
+ if (quoted && quoted[1]) return quoted[1];
32630
+ return null;
32631
+ }
32632
+ var TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
32633
+ "hi",
32634
+ "hello",
32635
+ "hey",
32636
+ "yo",
32637
+ "ok",
32638
+ "thanks",
32639
+ "thx",
32640
+ "thank you"
32641
+ ]);
32642
+ function isTrivialQuery(query) {
32643
+ const norm = query.trim().toLowerCase();
32644
+ if (norm.length === 0) return true;
32645
+ if (norm.length < 8) return true;
32646
+ return TRIVIAL_GREETINGS.has(norm);
32647
+ }
32648
+ function classifyQuery(query, parsedGrammar) {
32649
+ const normalized = query.toLowerCase();
32650
+ const matches = [];
32651
+ const grammarAgent = parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null;
32652
+ for (const spec of CATEGORY_KEYWORDS) {
32653
+ const matchedPhrases = [];
32654
+ for (const pattern of spec.patterns) {
32655
+ if (matchedPhrases.includes(pattern.phrase)) continue;
32656
+ const re = new RegExp(pattern.source, "i");
32657
+ if (re.test(normalized)) {
32658
+ matchedPhrases.push(pattern.phrase);
32659
+ }
32660
+ }
32661
+ if (matchedPhrases.length === 0) continue;
32662
+ const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
32663
+ const wantsAgentHint = spec.category === "agent_state" || spec.category === "agent_activity";
32664
+ const agent_name_hint = wantsAgentHint ? grammarAgent ?? extractAgentNameHint(query) : null;
32665
+ matches.push({
32666
+ category: spec.category,
32667
+ confidence,
32668
+ matched_keywords: matchedPhrases,
32669
+ agent_name_hint,
32670
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32671
+ });
32672
+ }
32673
+ matches.sort((a, b) => {
32674
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
32675
+ return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
32676
+ });
32677
+ return matches;
32678
+ }
32679
+ function fetcherHintsFromGrammar(parsed) {
32680
+ if (!parsed) return void 0;
32681
+ const hasTime = parsed.time_range !== null;
32682
+ const hasAgents = parsed.agent_names.length > 0;
32683
+ const hasEvents = parsed.event_types.length > 0;
32684
+ if (!hasTime && !hasAgents && !hasEvents) return void 0;
32685
+ const hints = {};
32686
+ if (parsed.time_range) {
32687
+ const range = parsed.time_range;
32688
+ hints.time_range = {
32689
+ start: range.start,
32690
+ end: range.end,
32691
+ ...range.relative_label !== void 0 ? { relative_label: range.relative_label } : {}
32692
+ };
32693
+ }
32694
+ if (hasAgents) hints.agent_names = parsed.agent_names;
32695
+ if (hasEvents) hints.event_types = parsed.event_types;
32696
+ return hints;
32697
+ }
32698
+ function approxTokenLen(text) {
32699
+ return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
32700
+ }
32701
+ var CATEGORY_LABELS = {
32702
+ templates: "Templates",
32703
+ agent_state: "Agent state",
32704
+ agent_activity: "Agent activity",
32705
+ audit_log: "Audit log",
32706
+ sentinel_findings: "Sentinel findings",
32707
+ anomaly_alerts: "Anomaly alerts",
32708
+ recent_receipts: "Recent receipts",
32709
+ verascore_deltas: "Verascore deltas"
32710
+ };
32711
+ async function runFetcher(match, fetchers, hints) {
32712
+ switch (match.category) {
32713
+ case "templates":
32714
+ return fetchers.templates(hints);
32715
+ case "agent_state":
32716
+ return fetchers.agent_state(match.agent_name_hint, hints);
32717
+ case "agent_activity":
32718
+ return fetchers.agent_activity(match.agent_name_hint, hints);
32719
+ case "audit_log":
32720
+ return fetchers.audit_log(hints);
32721
+ case "sentinel_findings":
32722
+ return fetchers.sentinel_findings(hints);
32723
+ case "anomaly_alerts":
32724
+ return fetchers.anomaly_alerts(hints);
32725
+ case "recent_receipts":
32726
+ return fetchers.recent_receipts(hints);
32727
+ case "verascore_deltas":
32728
+ return fetchers.verascore_deltas(hints);
32729
+ }
32730
+ }
32731
+ function trivialMatch(category, parsedGrammar) {
32732
+ return {
32733
+ category,
32734
+ confidence: 0.5,
32735
+ matched_keywords: ["llm-assist"],
32736
+ agent_name_hint: parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null,
32737
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32738
+ };
32739
+ }
32740
+ async function foldContext(query, fetchers, opts) {
32741
+ const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
32742
+ const parsed = opts?.parsed ?? null;
32743
+ const hints = fetcherHintsFromGrammar(parsed);
32744
+ let matches = classifyQuery(query, parsed);
32745
+ if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
32746
+ try {
32747
+ const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
32748
+ if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
32749
+ matches = [trivialMatch(picked, parsed)];
32750
+ }
32751
+ } catch {
32752
+ }
32753
+ }
32754
+ if (matches.length === 0) {
32755
+ return { section: "", categoriesIncluded: [] };
32756
+ }
32757
+ const attempts = [];
32758
+ for (const match of matches) {
32759
+ try {
32760
+ const text = await runFetcher(match, fetchers, hints);
32761
+ const trimmed = text.trim();
32762
+ if (trimmed.length > 0) {
32763
+ attempts.push({ category: match.category, text: trimmed });
32764
+ }
32765
+ } catch (err) {
32766
+ opts?.onFetcherFailure?.(match.category, err);
32767
+ }
32768
+ }
32769
+ if (attempts.length === 0) {
32770
+ return { section: "", categoriesIncluded: [] };
32771
+ }
32772
+ const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
32773
+ `);
32774
+ const sepTokens = approxTokenLen("\n\n");
32775
+ let runningTokens = headerTokens;
32776
+ const kept = [];
32777
+ for (const attempt of attempts) {
32778
+ const block = `### ${CATEGORY_LABELS[attempt.category]}
32779
+ ${attempt.text}`;
32780
+ const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
32781
+ if (kept.length === 0) {
32782
+ kept.push(attempt);
32783
+ runningTokens += tokens;
32784
+ continue;
32785
+ }
32786
+ if (runningTokens + tokens > budget) break;
32787
+ kept.push(attempt);
32788
+ runningTokens += tokens;
32789
+ }
32790
+ const blocks = kept.map(
32791
+ (k) => `### ${CATEGORY_LABELS[k.category]}
32792
+ ${k.text}`
32793
+ );
32794
+ const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
32795
+ ${blocks.join("\n\n")}`;
32796
+ return {
32797
+ section,
32798
+ categoriesIncluded: kept.map((k) => k.category)
32799
+ };
32800
+ }
32801
+
32802
+ // src/composition/constants.ts
32803
+ var COMPOSITION_EVENT_TYPES = [
32804
+ "composition_receipt_packed",
32805
+ "composition_receipt_verified",
32806
+ "composition_mandate_verified",
32807
+ "composition_verascore_published",
32808
+ "composition_sidecar_spawned",
32809
+ "composition_sidecar_crashed",
32810
+ "composition_sidecar_recovered",
32811
+ "composition_degraded",
32812
+ "composition_recovered"
32813
+ ];
32814
+
32815
+ // src/chat/concierge-query-grammar.ts
32816
+ var CANONICAL_AUDIT_EVENT_CLASSES = [
32817
+ // Lifecycle / policy
32818
+ "policy_change",
32819
+ "approval_request",
32820
+ "audit_truncate",
32821
+ "lockdown",
32822
+ "unwrap",
32823
+ // Exit bundle (Tier 1)
32824
+ "exit_bundle_export",
32825
+ "exit_bundle_import_activate",
32826
+ "exit_bundle_rekey",
32827
+ // Cross-harness approval aggregator
32828
+ "cross_harness_approval_aggregated",
32829
+ "cross_harness_approval_resolved",
32830
+ "cross_harness_approval_deduped",
32831
+ "cross_harness_approval_payload_decrypted",
32832
+ "cross_harness_approval_audit_trail_viewed",
32833
+ "cross_harness_approval_replayed",
32834
+ // Composition (full set from constants.ts)
32835
+ ...COMPOSITION_EVENT_TYPES,
32836
+ // Operator chat / concierge (full set from OPERATOR_CHAT_OPS)
32837
+ OPERATOR_CHAT_OPS.CONCIERGE_CHAT,
32838
+ OPERATOR_CHAT_OPS.AGENT_INSPECT_PANEL_OPENED,
32839
+ OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ,
32840
+ OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED,
32841
+ OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED,
32842
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
32843
+ // Bridge / commitment
32844
+ "bridge_commit",
32845
+ "bridge_verify",
32846
+ "bridge_attest",
32847
+ "proof_commitment",
32848
+ "proof_reveal",
32849
+ // Reputation
32850
+ "reputation_export",
32851
+ "reputation_import",
32852
+ "reputation_publish",
32853
+ "reputation_record",
32854
+ "reputation_query"
32855
+ ];
32856
+ var EVENT_SYNONYMS = [
32857
+ { phrase: "approvals", canonical: ["approval_request", "cross_harness_approval_aggregated", "cross_harness_approval_resolved"] },
32858
+ { phrase: "approval", canonical: ["approval_request"] },
32859
+ { phrase: "policy changes", canonical: ["policy_change"] },
32860
+ { phrase: "policy change", canonical: ["policy_change"] },
32861
+ { phrase: "policy edits", canonical: ["policy_change"] },
32862
+ { phrase: "lockdowns", canonical: ["lockdown"] },
32863
+ { phrase: "exit bundles", canonical: ["exit_bundle_export", "exit_bundle_import_activate"] },
32864
+ { phrase: "exit bundle", canonical: ["exit_bundle_export"] },
32865
+ { phrase: "audit truncations", canonical: ["audit_truncate"] },
32866
+ { phrase: "audit truncation", canonical: ["audit_truncate"] },
32867
+ { phrase: "compositions", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32868
+ { phrase: "receipts", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32869
+ { phrase: "receipt verifications", canonical: ["composition_receipt_verified"] },
32870
+ { phrase: "concierge chats", canonical: [OPERATOR_CHAT_OPS.CONCIERGE_CHAT] },
32871
+ { phrase: "cross harness approvals", canonical: ["cross_harness_approval_aggregated", "cross_harness_approval_resolved"] }
32872
+ ];
32873
+ var MS_PER_HOUR = 60 * 60 * 1e3;
32874
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
32875
+ var NUMBER_WORDS = {
32876
+ a: 1,
32877
+ an: 1,
32878
+ one: 1,
32879
+ two: 2,
32880
+ three: 3,
32881
+ four: 4,
32882
+ five: 5,
32883
+ six: 6,
32884
+ seven: 7,
32885
+ eight: 8,
32886
+ nine: 9,
32887
+ ten: 10,
32888
+ twelve: 12,
32889
+ twentyfour: 24
32890
+ };
32891
+ function resolveTimeRange(query, now) {
32892
+ const normalized = query.trim();
32893
+ const lower = normalized.toLowerCase();
32894
+ const fromTo = lower.match(
32895
+ /\b(?:from|between)\s+(.+?)\s+(?:to|and|-|until)\s+([\w:.\-+t /]+)/i
32896
+ );
32897
+ if (fromTo) {
32898
+ const aSlice = fromTo[1];
32899
+ const bSlice = fromTo[2];
32900
+ if (aSlice !== void 0 && bSlice !== void 0) {
32901
+ const a = parseInstant(aSlice, now);
32902
+ const b = parseInstant(bSlice, now);
32903
+ if (a && b) {
32904
+ const start = a.getTime() <= b.getTime() ? a : b;
32905
+ const end = a.getTime() <= b.getTime() ? b : a;
32906
+ return {
32907
+ range: { start, end },
32908
+ matchedSubstring: fromTo[0]
32909
+ };
32910
+ }
32911
+ }
32912
+ }
32913
+ const sinceMatch = lower.match(/\bsince\s+([\w:.\-+t /]+)/i);
32914
+ if (sinceMatch) {
32915
+ const slice = sinceMatch[1];
32916
+ if (slice !== void 0) {
32917
+ const start = parseInstant(slice, now);
32918
+ if (start) {
32919
+ return {
32920
+ range: { start, end: now },
32921
+ matchedSubstring: sinceMatch[0]
32922
+ };
32923
+ }
32924
+ }
32925
+ }
32926
+ if (/\byesterday\b/.test(lower)) {
32927
+ const startOfToday = startOfDay(now);
32928
+ const start = new Date(startOfToday.getTime() - MS_PER_DAY);
32929
+ const end = new Date(startOfToday.getTime() - 1);
32930
+ return {
32931
+ range: { start, end, relative_label: "yesterday" },
32932
+ matchedSubstring: "yesterday"
32933
+ };
32934
+ }
32935
+ if (/\btoday\b/.test(lower)) {
32936
+ return {
32937
+ range: {
32938
+ start: startOfDay(now),
32939
+ end: now,
32940
+ relative_label: "today"
32941
+ },
32942
+ matchedSubstring: "today"
32943
+ };
32944
+ }
32945
+ const compactHours = lower.match(/\blast\s+(\d+)\s*h\b/i);
32946
+ if (compactHours) {
32947
+ const tok = compactHours[1];
32948
+ if (tok !== void 0) {
32949
+ const n = Number.parseInt(tok, 10);
32950
+ if (Number.isFinite(n) && n > 0) {
32951
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32952
+ return {
32953
+ range: { start, end: now, relative_label: `last ${n}h` },
32954
+ matchedSubstring: compactHours[0]
32955
+ };
32956
+ }
32957
+ }
32958
+ }
32959
+ const hoursMatch = lower.match(
32960
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:hr\b|hrs\b|hour|hours)/i
32961
+ );
32962
+ if (hoursMatch) {
32963
+ const tok = hoursMatch[1];
32964
+ if (tok !== void 0) {
32965
+ const n = parseCount(tok);
32966
+ if (n !== null && n > 0) {
32967
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32968
+ return {
32969
+ range: { start, end: now, relative_label: `past ${n} hour${n === 1 ? "" : "s"}` },
32970
+ matchedSubstring: hoursMatch[0]
32971
+ };
32972
+ }
32973
+ }
32974
+ }
32975
+ if (/\b(?:past|last)\s+hour\b/.test(lower)) {
32976
+ const start = new Date(now.getTime() - MS_PER_HOUR);
32977
+ return {
32978
+ range: { start, end: now, relative_label: "past hour" },
32979
+ matchedSubstring: lower.match(/\b(?:past|last)\s+hour\b/i)[0]
32980
+ };
32981
+ }
32982
+ const daysMatch = lower.match(
32983
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:d\b|day|days)/i
32984
+ );
32985
+ if (daysMatch) {
32986
+ const tok = daysMatch[1];
32987
+ if (tok !== void 0) {
32988
+ const n = parseCount(tok);
32989
+ if (n !== null && n > 0) {
32990
+ const start = new Date(now.getTime() - n * MS_PER_DAY);
32991
+ return {
32992
+ range: { start, end: now, relative_label: `past ${n} day${n === 1 ? "" : "s"}` },
32993
+ matchedSubstring: daysMatch[0]
32994
+ };
32995
+ }
32996
+ }
32997
+ }
32998
+ if (/\b(?:past|last)\s+day\b/.test(lower)) {
32999
+ const start = new Date(now.getTime() - MS_PER_DAY);
33000
+ return {
33001
+ range: { start, end: now, relative_label: "past day" },
33002
+ matchedSubstring: lower.match(/\b(?:past|last)\s+day\b/i)[0]
33003
+ };
33004
+ }
33005
+ if (/\bthis\s+week\b/.test(lower)) {
33006
+ const start = startOfWeek(now);
33007
+ return {
33008
+ range: { start, end: now, relative_label: "this week" },
33009
+ matchedSubstring: lower.match(/\bthis\s+week\b/i)[0]
33010
+ };
33011
+ }
33012
+ if (/\b(?:past|last)\s+week\b/.test(lower)) {
33013
+ const start = new Date(now.getTime() - 7 * MS_PER_DAY);
33014
+ return {
33015
+ range: { start, end: now, relative_label: "past week" },
33016
+ matchedSubstring: lower.match(/\b(?:past|last)\s+week\b/i)[0]
33017
+ };
33018
+ }
33019
+ const isoMatch = normalized.match(
33020
+ /\b(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?)?)\b/
33021
+ );
33022
+ if (isoMatch) {
33023
+ const tok = isoMatch[1];
33024
+ if (tok !== void 0) {
33025
+ const parsed = parseInstant(tok, now);
33026
+ if (parsed) {
33027
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(tok);
33028
+ if (isDateOnly) {
33029
+ return {
33030
+ range: {
33031
+ start: parsed,
33032
+ end: new Date(parsed.getTime() + MS_PER_DAY - 1)
33033
+ },
33034
+ matchedSubstring: tok
33035
+ };
33036
+ }
33037
+ return {
33038
+ range: {
33039
+ start: new Date(parsed.getTime() - 30 * 60 * 1e3),
33040
+ end: new Date(parsed.getTime() + 30 * 60 * 1e3)
33041
+ },
33042
+ matchedSubstring: tok
33043
+ };
33044
+ }
33045
+ }
33046
+ }
33047
+ return null;
33048
+ }
33049
+ function parseInstant(token, now) {
33050
+ const trimmed = token.trim().replace(/[,.!?;]+$/g, "");
33051
+ if (!trimmed) return null;
33052
+ const lower = trimmed.toLowerCase();
33053
+ if (lower === "now") return now;
33054
+ if (lower === "today") return startOfDay(now);
33055
+ if (lower === "yesterday") {
33056
+ return new Date(startOfDay(now).getTime() - MS_PER_DAY);
33057
+ }
33058
+ const isoLike = trimmed.replace(" ", "T");
33059
+ const parsed = new Date(isoLike);
33060
+ if (!Number.isNaN(parsed.getTime())) return parsed;
33061
+ return null;
33062
+ }
33063
+ function parseCount(token) {
33064
+ const lower = token.toLowerCase();
33065
+ if (/^\d+$/.test(lower)) {
33066
+ const n = Number.parseInt(lower, 10);
33067
+ return Number.isFinite(n) ? n : null;
33068
+ }
33069
+ return NUMBER_WORDS[lower] ?? null;
33070
+ }
33071
+ function startOfDay(d) {
33072
+ const out = new Date(d);
33073
+ out.setHours(0, 0, 0, 0);
33074
+ return out;
33075
+ }
33076
+ function startOfWeek(d) {
33077
+ const out = startOfDay(d);
33078
+ const dayOfWeek = out.getDay();
33079
+ const offsetToMonday = (dayOfWeek + 6) % 7;
33080
+ out.setDate(out.getDate() - offsetToMonday);
33081
+ return out;
33082
+ }
33083
+ function listFromRegistry(registry) {
33084
+ if (!registry) return [];
33085
+ if (Array.isArray(registry)) return registry;
33086
+ if (typeof registry.list === "function") {
33087
+ return registry.list();
33088
+ }
33089
+ return [];
33090
+ }
33091
+ function extractAgentNames(query, registry) {
33092
+ const records = listFromRegistry(registry);
33093
+ if (records.length === 0) return { matched: [], flagged: false };
33094
+ const lowerQuery = query.toLowerCase();
33095
+ const compactQuery = lowerQuery.replace(/[\s_-]+/g, "");
33096
+ const matched = [];
33097
+ const seen = /* @__PURE__ */ new Set();
33098
+ for (const rec of records) {
33099
+ const id = rec.agent_id;
33100
+ if (!id || seen.has(id)) continue;
33101
+ const idLower = id.toLowerCase();
33102
+ if (idLower.length < 3) continue;
33103
+ const idCompact = idLower.replace(/[\s_-]+/g, "");
33104
+ const wordRe = new RegExp(
33105
+ `\\b${idLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
33106
+ "i"
33107
+ );
33108
+ if (wordRe.test(query)) {
33109
+ matched.push(id);
33110
+ seen.add(id);
33111
+ continue;
33112
+ }
33113
+ if (idCompact.length >= 4 && compactQuery.includes(idCompact)) {
33114
+ matched.push(id);
33115
+ seen.add(id);
33116
+ }
33117
+ }
33118
+ const agentMention = lowerQuery.match(/\bagent\s+([a-z0-9_-]{3,40})/i);
33119
+ const flagged = matched.length === 0 && agentMention !== null && agentMention[1] !== void 0 && !records.some((r) => r.agent_id.toLowerCase() === agentMention[1]?.toLowerCase());
33120
+ return { matched, flagged };
33121
+ }
33122
+ function escapeRegex(s) {
33123
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33124
+ }
33125
+ function extractEventTypes(query, enumValues) {
33126
+ const lower = query.toLowerCase();
33127
+ const matched = [];
33128
+ const seen = /* @__PURE__ */ new Set();
33129
+ for (const ev of enumValues) {
33130
+ if (seen.has(ev)) continue;
33131
+ const re = new RegExp(`\\b${escapeRegex(ev)}\\b`, "i");
33132
+ if (re.test(query)) {
33133
+ matched.push(ev);
33134
+ seen.add(ev);
33135
+ }
33136
+ }
33137
+ for (const syn of EVENT_SYNONYMS) {
33138
+ const re = new RegExp(
33139
+ `\\b${syn.phrase.split(/\s+/).map(escapeRegex).join("\\s+")}\\b`,
33140
+ "i"
33141
+ );
33142
+ if (re.test(query)) {
33143
+ for (const c of syn.canonical) {
33144
+ if (seen.has(c)) continue;
33145
+ if (!enumValues.includes(c)) continue;
33146
+ matched.push(c);
33147
+ seen.add(c);
33148
+ }
33149
+ }
33150
+ }
33151
+ const globMatches = lower.match(/\b([a-z_]+)_\*/g) ?? [];
33152
+ for (const glob of globMatches) {
33153
+ const prefix = glob.slice(0, -2);
33154
+ for (const ev of enumValues) {
33155
+ if (seen.has(ev)) continue;
33156
+ if (ev.startsWith(prefix)) {
33157
+ matched.push(ev);
33158
+ seen.add(ev);
33159
+ }
33160
+ }
33161
+ }
33162
+ const eventNounMention = /\b(?:event|events|class|classes)\b/i.test(query) && matched.length === 0;
33163
+ return { matched, flagged: eventNounMention };
33164
+ }
33165
+ function deriveIntentPhrase(query, stripTokens) {
33166
+ let out = query;
33167
+ for (const tok of stripTokens) {
33168
+ if (!tok) continue;
33169
+ const re = new RegExp(escapeRegex(tok), "gi");
33170
+ out = out.replace(re, " ");
33171
+ }
33172
+ return out.replace(/\s+/g, " ").trim();
33173
+ }
33174
+ function computeConfidence(parsed) {
33175
+ const dims = [
33176
+ { present: parsed.hasTimeMention, resolved: parsed.timeResolved },
33177
+ { present: parsed.hasAgentMention, resolved: parsed.agentResolved },
33178
+ { present: parsed.hasEventMention, resolved: parsed.eventResolved }
33179
+ ];
33180
+ const present = dims.filter((d) => d.present);
33181
+ let base;
33182
+ if (present.length === 0) {
33183
+ base = parsed.intentEmpty ? 0 : 0.3;
33184
+ } else {
33185
+ const resolved = present.filter((d) => d.resolved).length;
33186
+ base = resolved / present.length;
33187
+ }
33188
+ const adjusted = base - 0.15 * parsed.ambiguityCount;
33189
+ if (adjusted < 0) return 0;
33190
+ if (adjusted > 1) return 1;
33191
+ return adjusted;
33192
+ }
33193
+ var TIME_MENTION_PROBE = /\b(yesterday|today|now|past|last|this\s+week|this\s+month|since|from|between|\d{4}-\d{2}-\d{2})\b/i;
33194
+ var AGENT_MENTION_PROBE = /\bagent[s]?\b/i;
33195
+ var EVENT_MENTION_PROBE = /\b(event|events|class|classes|approvals?|policy)\b/i;
33196
+ function parseQuery(query, opts) {
33197
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
33198
+ const enumValues = opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES;
33199
+ const original = query ?? "";
33200
+ const trimmed = original.trim();
33201
+ if (trimmed.length === 0) {
33202
+ return {
33203
+ time_range: null,
33204
+ agent_names: [],
33205
+ event_types: [],
33206
+ intent_phrase: "",
33207
+ ambiguity_flags: ["no_signal_extracted"],
33208
+ parse_confidence: 0
33209
+ };
33210
+ }
33211
+ const ambiguity_flags = /* @__PURE__ */ new Set();
33212
+ const timeMatch = resolveTimeRange(trimmed, now);
33213
+ const hasTimeMention = TIME_MENTION_PROBE.test(trimmed);
33214
+ if (hasTimeMention && !timeMatch) {
33215
+ ambiguity_flags.add("unknown_time_token");
33216
+ }
33217
+ const agentResult = extractAgentNames(trimmed, opts?.registry);
33218
+ if (agentResult.flagged) {
33219
+ ambiguity_flags.add("unknown_agent_token");
33220
+ }
33221
+ const hasAgentMention = AGENT_MENTION_PROBE.test(trimmed);
33222
+ const eventResult = extractEventTypes(trimmed, enumValues);
33223
+ const hasEventMention = EVENT_MENTION_PROBE.test(trimmed);
33224
+ if (eventResult.flagged) {
33225
+ ambiguity_flags.add("unknown_event_token");
33226
+ }
33227
+ const stripTokens = [];
33228
+ if (timeMatch) stripTokens.push(timeMatch.matchedSubstring);
33229
+ for (const name of agentResult.matched) stripTokens.push(name);
33230
+ for (const ev of eventResult.matched) {
33231
+ if (trimmed.toLowerCase().includes(ev.toLowerCase())) {
33232
+ stripTokens.push(ev);
33233
+ }
33234
+ }
33235
+ const intent_phrase = deriveIntentPhrase(trimmed, stripTokens);
33236
+ const parse_confidence = computeConfidence({
33237
+ hasTimeMention,
33238
+ timeResolved: timeMatch !== null,
33239
+ hasAgentMention,
33240
+ agentResolved: agentResult.matched.length > 0,
33241
+ hasEventMention,
33242
+ eventResolved: eventResult.matched.length > 0,
33243
+ intentEmpty: intent_phrase.length === 0,
33244
+ ambiguityCount: ambiguity_flags.size
33245
+ });
33246
+ if (timeMatch === null && agentResult.matched.length === 0 && eventResult.matched.length === 0 && intent_phrase.length === 0) {
33247
+ ambiguity_flags.add("no_signal_extracted");
33248
+ }
33249
+ return {
33250
+ time_range: timeMatch ? timeMatch.range : null,
33251
+ agent_names: agentResult.matched,
33252
+ event_types: eventResult.matched,
33253
+ intent_phrase,
33254
+ ambiguity_flags: Array.from(ambiguity_flags),
33255
+ parse_confidence
33256
+ };
33257
+ }
33258
+ var LLM_ASSIST_THRESHOLD = 0.5;
33259
+ function isLowConfidence(parsed) {
33260
+ return parsed.parse_confidence < LLM_ASSIST_THRESHOLD;
33261
+ }
33262
+ async function parseQueryWithLlmAssist(query, llmAssist, opts) {
33263
+ const parsed = parseQuery(query, opts);
33264
+ if (!llmAssist || !isLowConfidence(parsed)) return parsed;
33265
+ let completion;
33266
+ try {
33267
+ completion = await llmAssist(query, parsed);
33268
+ } catch {
33269
+ return parsed;
33270
+ }
33271
+ if (!completion || typeof completion !== "object") return parsed;
33272
+ const merged = { ...parsed };
33273
+ if (parsed.time_range === null && completion.time_range) {
33274
+ merged.time_range = completion.time_range;
33275
+ }
33276
+ if (parsed.agent_names.length === 0 && Array.isArray(completion.agent_names)) {
33277
+ merged.agent_names = completion.agent_names.filter(
33278
+ (s) => typeof s === "string" && s.length > 0
33279
+ );
33280
+ }
33281
+ if (parsed.event_types.length === 0 && Array.isArray(completion.event_types)) {
33282
+ const allowed = new Set(opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES);
33283
+ merged.event_types = completion.event_types.filter(
33284
+ (s) => typeof s === "string" && allowed.has(s)
33285
+ );
33286
+ }
33287
+ merged.parse_confidence = Math.max(
33288
+ parsed.parse_confidence,
33289
+ computeConfidence({
33290
+ hasTimeMention: TIME_MENTION_PROBE.test(query),
33291
+ timeResolved: merged.time_range !== null,
33292
+ hasAgentMention: AGENT_MENTION_PROBE.test(query),
33293
+ agentResolved: merged.agent_names.length > 0,
33294
+ hasEventMention: EVENT_MENTION_PROBE.test(query),
33295
+ eventResolved: merged.event_types.length > 0,
33296
+ intentEmpty: merged.intent_phrase.length === 0,
33297
+ ambiguityCount: merged.ambiguity_flags.length
33298
+ })
33299
+ );
33300
+ return merged;
33301
+ }
33302
+ function auditSafeSummary(parsed) {
33303
+ return {
33304
+ time_range: parsed.time_range ? {
33305
+ start_iso: parsed.time_range.start.toISOString(),
33306
+ end_iso: parsed.time_range.end.toISOString(),
33307
+ ...parsed.time_range.relative_label !== void 0 ? { relative_label: parsed.time_range.relative_label } : {}
33308
+ } : null,
33309
+ agent_names: [...parsed.agent_names],
33310
+ event_types: [...parsed.event_types],
33311
+ ambiguity_flags: [...parsed.ambiguity_flags],
33312
+ parse_confidence: parsed.parse_confidence
33313
+ };
33314
+ }
33315
+
32003
33316
  // src/chat/operator-chat-service.ts
32004
33317
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
32005
33318
  var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
32006
33319
  var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
32007
33320
  var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
32008
33321
  var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32009
- function approxTokenLen(text) {
33322
+ var DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
33323
+ function approxTokenLen2(text) {
32010
33324
  return Math.ceil(text.length / 4);
32011
33325
  }
32012
33326
  var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
@@ -32050,6 +33364,11 @@ var OperatorChatService = class {
32050
33364
  historyTokenBudget;
32051
33365
  sessionTtlMs;
32052
33366
  clock;
33367
+ contextFetchers;
33368
+ contextLlmAssist;
33369
+ dynamicContextBudget;
33370
+ agentRegistry;
33371
+ grammarLlmAssist;
32053
33372
  /**
32054
33373
  * In-memory thread_id assigned to the active concierge session.
32055
33374
  * The first sendConcierge call after construction allocates a fresh
@@ -32081,6 +33400,19 @@ var OperatorChatService = class {
32081
33400
  this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
32082
33401
  this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
32083
33402
  this.clock = deps.conciergeClock ?? (() => Date.now());
33403
+ if (deps.conciergeContextFetchers) {
33404
+ this.contextFetchers = deps.conciergeContextFetchers;
33405
+ }
33406
+ if (deps.conciergeContextLlmAssist) {
33407
+ this.contextLlmAssist = deps.conciergeContextLlmAssist;
33408
+ }
33409
+ this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
33410
+ if (deps.conciergeAgentRegistry) {
33411
+ this.agentRegistry = deps.conciergeAgentRegistry;
33412
+ }
33413
+ if (deps.conciergeGrammarLlmAssist) {
33414
+ this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
33415
+ }
32084
33416
  }
32085
33417
  // ── Concierge ─────────────────────────────────────────────────────────
32086
33418
  /**
@@ -32139,11 +33471,13 @@ var OperatorChatService = class {
32139
33471
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
32140
33472
  });
32141
33473
  }
33474
+ const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
32142
33475
  const start = Date.now();
32143
33476
  let conciergeBody;
32144
33477
  let servedBy = "disabled";
32145
33478
  let displayLabel = "Concierge: substrate not configured";
32146
33479
  let outcome = "substrate_disabled";
33480
+ let dynamicCategoriesIncluded = [];
32147
33481
  if (!this.substrateSelector) {
32148
33482
  conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
32149
33483
  } else {
@@ -32155,7 +33489,15 @@ var OperatorChatService = class {
32155
33489
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
32156
33490
  outcome = "substrate_disabled";
32157
33491
  } else {
32158
- const context = await this.assembleConciergeContext(priorTurns);
33492
+ const dynamicResult = await this.runDynamicContextFold(
33493
+ filterResult.filtered,
33494
+ parsedGrammar
33495
+ );
33496
+ dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
33497
+ const context = await this.assembleConciergeContext(
33498
+ priorTurns,
33499
+ dynamicResult.section
33500
+ );
32159
33501
  const response = await this.substrateSelector.invokeSummarize(
32160
33502
  "concierge",
32161
33503
  {
@@ -32219,7 +33561,9 @@ var OperatorChatService = class {
32219
33561
  ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
32220
33562
  ...this.memory ? {
32221
33563
  prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32222
- } : {}
33564
+ } : {},
33565
+ ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
33566
+ parsed_grammar: auditSafeSummary(parsedGrammar)
32223
33567
  };
32224
33568
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
32225
33569
  return {
@@ -32370,10 +33714,13 @@ var OperatorChatService = class {
32370
33714
  * ## Sanctuary reference
32371
33715
  * <static domain reference block>
32372
33716
  *
33717
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
33718
+ * ### <Category>
33719
+ * <fetcher payload>
33720
+ *
32373
33721
  * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
32374
33722
  * OPERATOR: ...
32375
33723
  * CONCIERGE: ...
32376
- * ---
32377
33724
  *
32378
33725
  * ## Recent activity
32379
33726
  * <recentActivity output>
@@ -32392,13 +33739,14 @@ var OperatorChatService = class {
32392
33739
  * if available; the v1.2 selector does not expose one, so structured
32393
33740
  * serialization is the canonical path for v1.3.
32394
33741
  */
32395
- async assembleConciergeContext(priorTurns = []) {
33742
+ async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
32396
33743
  const ref = `## Sanctuary reference
32397
33744
  ${SANCTUARY_DOMAIN_REFERENCE}`;
32398
33745
  const priorSection = this.formatPriorTurnsSection(priorTurns);
32399
33746
  if (!this.contextProviders) {
32400
33747
  return [
32401
33748
  ref,
33749
+ ...dynamicSection ? [dynamicSection] : [],
32402
33750
  ...priorSection ? [priorSection] : [],
32403
33751
  "## Recent activity\n(no providers wired)",
32404
33752
  "## Wrapped agents\n(no providers wired)",
@@ -32412,6 +33760,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
32412
33760
  ]);
32413
33761
  return [
32414
33762
  ref,
33763
+ ...dynamicSection ? [dynamicSection] : [],
32415
33764
  ...priorSection ? [priorSection] : [],
32416
33765
  `## Recent activity
32417
33766
  ${activity}`,
@@ -32421,6 +33770,69 @@ ${agents}`,
32421
33770
  ${inbox}`
32422
33771
  ].join("\n\n");
32423
33772
  }
33773
+ /**
33774
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
33775
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
33776
+ * an empty fold, fetcher failures emit a per-category audit event
33777
+ * and are omitted from the rendered section, an LLM-assist failure
33778
+ * proceeds with no fold. Returns the rendered section + the list of
33779
+ * categories whose data made it into the section (used for the
33780
+ * round-trip audit emission).
33781
+ *
33782
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
33783
+ * `parsed` opt to `foldContext`, so fetchers see the structured
33784
+ * `FetcherHints` derived from it.
33785
+ */
33786
+ async runDynamicContextFold(query, parsedGrammar) {
33787
+ if (!this.contextFetchers) {
33788
+ return { section: "", categoriesIncluded: [] };
33789
+ }
33790
+ const result = await foldContext(query, this.contextFetchers, {
33791
+ maxTokens: this.dynamicContextBudget,
33792
+ ...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
33793
+ onFetcherFailure: (category, error) => {
33794
+ this.emitContextFetcherFailed(category, classifyFetcherError(error));
33795
+ },
33796
+ parsed: parsedGrammar
33797
+ });
33798
+ return result;
33799
+ }
33800
+ /**
33801
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
33802
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
33803
+ * configured and the rule-based parse is below
33804
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
33805
+ * throws) so the audit emission can carry the result unconditionally.
33806
+ */
33807
+ async runGrammarParse(query) {
33808
+ return parseQueryWithLlmAssist(query, this.grammarLlmAssist, {
33809
+ ...this.agentRegistry !== void 0 ? { registry: this.agentRegistry } : {},
33810
+ eventClassEnum: CANONICAL_AUDIT_EVENT_CLASSES
33811
+ });
33812
+ }
33813
+ /**
33814
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
33815
+ * of the fold path so the dynamic-context handler stays readable.
33816
+ * Emits with `result: "failure"` since the named category dropped
33817
+ * from the rendered section for this round-trip.
33818
+ */
33819
+ emitContextFetcherFailed(category, failureReason) {
33820
+ const payload = {
33821
+ version: "1.2",
33822
+ event_id: makeEventId("conc-ctxfail"),
33823
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33824
+ identity_id: this.identityId,
33825
+ kind: "operator_concierge_context_fetcher_failed",
33826
+ surface: "concierge",
33827
+ category,
33828
+ failure_reason: failureReason
33829
+ };
33830
+ this.emit(
33831
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
33832
+ payload,
33833
+ "failure"
33834
+ );
33835
+ }
32424
33836
  /**
32425
33837
  * Render the prior-conversation section with token-budget enforcement
32426
33838
  * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
@@ -32431,14 +33843,14 @@ ${inbox}`
32431
33843
  if (turns.length === 0) return "";
32432
33844
  const HEADER = "## Prior conversation";
32433
33845
  const lines = turns.map(formatPriorTurnLine);
32434
- const headerTokens = approxTokenLen(`${HEADER}
33846
+ const headerTokens = approxTokenLen2(`${HEADER}
32435
33847
  `);
32436
- const sepTokens = approxTokenLen("\n");
33848
+ const sepTokens = approxTokenLen2("\n");
32437
33849
  let runningTokens = headerTokens;
32438
33850
  let runningLines = [];
32439
33851
  for (let i = lines.length - 1; i >= 0; i--) {
32440
33852
  const line = lines[i];
32441
- const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
33853
+ const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
32442
33854
  if (runningTokens + tokens > this.historyTokenBudget) break;
32443
33855
  runningTokens += tokens;
32444
33856
  runningLines.push(line);
@@ -32462,6 +33874,17 @@ ${runningLines.join("\n")}`;
32462
33874
  function makeEventId(prefix) {
32463
33875
  return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
32464
33876
  }
33877
+ function classifyFetcherError(error) {
33878
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
33879
+ if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
33880
+ if (msg.includes("schema") || msg.includes("invalid shape")) {
33881
+ return "schema_mismatch";
33882
+ }
33883
+ if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
33884
+ return "io_failed";
33885
+ }
33886
+ return "unknown";
33887
+ }
32465
33888
  function formatPriorTurnLine(turn) {
32466
33889
  const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
32467
33890
  return `${label}: ${turn.content}`;
@@ -32474,7 +33897,7 @@ function hashOf(input) {
32474
33897
  init_encryption();
32475
33898
  init_encoding();
32476
33899
  var OPERATOR_CHAT_NAMESPACE = "_chat";
32477
- var HKDF_INFO = "operator-chat-store-v1";
33900
+ var HKDF_INFO2 = "operator-chat-store-v1";
32478
33901
  function chatStorageKey(surface, threadKey) {
32479
33902
  return `${surface}.${threadKey}`;
32480
33903
  }
@@ -32483,7 +33906,7 @@ var OperatorChatStore = class {
32483
33906
  encryptionKey;
32484
33907
  constructor(storage, masterKey) {
32485
33908
  this.storage = storage;
32486
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO);
33909
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
32487
33910
  }
32488
33911
  /**
32489
33912
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -32567,9 +33990,9 @@ init_encryption();
32567
33990
  init_encoding();
32568
33991
  var CONCIERGE_MEMORY_NAMESPACE = "_chat";
32569
33992
  var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
32570
- var HKDF_INFO2 = "concierge-memory-store-v1";
33993
+ var HKDF_INFO3 = "concierge-memory-store-v1";
32571
33994
  var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
32572
- var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
33995
+ var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
32573
33996
  var ConciergeMemoryStore = class {
32574
33997
  storage;
32575
33998
  encryptionKey;
@@ -32578,7 +34001,7 @@ var ConciergeMemoryStore = class {
32578
34001
  locks;
32579
34002
  constructor(opts) {
32580
34003
  this.storage = opts.storage;
32581
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
34004
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
32582
34005
  this.fortressId = opts.fortressId;
32583
34006
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
32584
34007
  this.locks = /* @__PURE__ */ new Map();
@@ -32657,7 +34080,7 @@ var ConciergeMemoryStore = class {
32657
34080
  return { ok: false, reason: "io_failed" };
32658
34081
  }
32659
34082
  if (!raw) return { ok: true, turns: [] };
32660
- if (raw.length > MAX_BUNDLE_BYTES2) {
34083
+ if (raw.length > MAX_BUNDLE_BYTES3) {
32661
34084
  return { ok: false, reason: "oversize_bundle" };
32662
34085
  }
32663
34086
  let envelope;
@@ -32704,7 +34127,7 @@ var ConciergeMemoryStore = class {
32704
34127
  );
32705
34128
  const summaries = [];
32706
34129
  for (const meta of entries) {
32707
- const threadId = stripKeyPrefix(meta.key);
34130
+ const threadId = stripKeyPrefix2(meta.key);
32708
34131
  if (threadId === null) continue;
32709
34132
  const bundle = await this.loadBundle(threadId);
32710
34133
  if (!bundle || bundle.turns.length === 0) continue;
@@ -32757,7 +34180,7 @@ var ConciergeMemoryStore = class {
32757
34180
  );
32758
34181
  let pruned = 0;
32759
34182
  for (const meta of entries) {
32760
- const threadId = stripKeyPrefix(meta.key);
34183
+ const threadId = stripKeyPrefix2(meta.key);
32761
34184
  if (threadId === null) continue;
32762
34185
  pruned += await this.withLock(threadId, async () => {
32763
34186
  const bundle = await this.loadBundle(threadId);
@@ -32788,7 +34211,7 @@ var ConciergeMemoryStore = class {
32788
34211
  return null;
32789
34212
  }
32790
34213
  if (!raw) return null;
32791
- if (raw.length > MAX_BUNDLE_BYTES2) return null;
34214
+ if (raw.length > MAX_BUNDLE_BYTES3) return null;
32792
34215
  try {
32793
34216
  const envelope = JSON.parse(bytesToString(raw));
32794
34217
  const aad = stringToBytes(threadId);
@@ -32841,7 +34264,7 @@ var ConciergeMemoryStore = class {
32841
34264
  function bundleKey(threadId) {
32842
34265
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
32843
34266
  }
32844
- function stripKeyPrefix(key) {
34267
+ function stripKeyPrefix2(key) {
32845
34268
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
32846
34269
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
32847
34270
  }
@@ -32910,7 +34333,18 @@ function buildV11Bindings(inputs) {
32910
34333
  registry
32911
34334
  }),
32912
34335
  conciergePiiFilter: buildConciergePiiFilter(),
32913
- conciergeMemory
34336
+ conciergeMemory,
34337
+ conciergeContextFetchers: buildConciergeContextFetchers({
34338
+ auditLog: inputs.auditLog,
34339
+ identityId: inputs.identityId,
34340
+ registry
34341
+ }),
34342
+ ...inputs.intelligenceSelector ? {
34343
+ conciergeContextLlmAssist: buildConciergeContextLlmAssist({
34344
+ selector: inputs.intelligenceSelector,
34345
+ identityId: inputs.identityId
34346
+ })
34347
+ } : {}
32914
34348
  });
32915
34349
  }
32916
34350
  const hubService = new HubService({
@@ -32971,6 +34405,107 @@ function buildConciergeContextProviders(args) {
32971
34405
  }
32972
34406
  };
32973
34407
  }
34408
+ function buildConciergeContextFetchers(args) {
34409
+ const empty = async () => "";
34410
+ return {
34411
+ templates: async () => {
34412
+ const entries = listTemplates();
34413
+ if (entries.length === 0) return "(no templates installed)";
34414
+ const lines = entries.map((e) => {
34415
+ const m = e.metadata;
34416
+ return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
34417
+ });
34418
+ return lines.join("\n");
34419
+ },
34420
+ agent_state: async (agentNameHint) => {
34421
+ const records = args.registry.list({ identity_id: args.identityId });
34422
+ if (records.length === 0) return "(no wrapped agents)";
34423
+ const filtered = agentNameHint ? records.filter(
34424
+ (r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
34425
+ ) : records;
34426
+ const target = filtered.length > 0 ? filtered : records;
34427
+ const lines = target.slice(0, 20).map((r) => {
34428
+ const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
34429
+ return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
34430
+ });
34431
+ return lines.join("\n");
34432
+ },
34433
+ agent_activity: async (agentNameHint) => {
34434
+ const result = await args.auditLog.query({ limit: 50 });
34435
+ const owned = result.entries.filter(
34436
+ (e) => e.identity_id === args.identityId
34437
+ );
34438
+ const filtered = agentNameHint ? owned.filter((e) => {
34439
+ const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
34440
+ return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
34441
+ }) : owned;
34442
+ const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
34443
+ if (tail.length === 0) return "(no activity)";
34444
+ return tail.map((e) => {
34445
+ const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
34446
+ return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
34447
+ }).join("\n");
34448
+ },
34449
+ audit_log: async () => {
34450
+ const result = await args.auditLog.query({ limit: 30 });
34451
+ const owned = result.entries.filter(
34452
+ (e) => e.identity_id === args.identityId
34453
+ );
34454
+ if (owned.length === 0) return "(no audit log entries)";
34455
+ return owned.slice(-30).map(
34456
+ (e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
34457
+ ).join("\n");
34458
+ },
34459
+ sentinel_findings: empty,
34460
+ anomaly_alerts: empty,
34461
+ recent_receipts: async () => {
34462
+ const result = await args.auditLog.query({ limit: 100 });
34463
+ const owned = result.entries.filter(
34464
+ (e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
34465
+ );
34466
+ if (owned.length === 0) return "(no recent composition events)";
34467
+ return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
34468
+ },
34469
+ verascore_deltas: empty
34470
+ };
34471
+ }
34472
+ function buildConciergeContextLlmAssist(args) {
34473
+ return async (query, categories) => {
34474
+ const labelList = categories.map((c) => `- ${c}`).join("\n");
34475
+ const prompt = `You are a router. Classify the operator's query into one of the categories below or "none".
34476
+ Reply with exactly one token: one category name or "none".
34477
+
34478
+ Categories:
34479
+ ${labelList}
34480
+
34481
+ Query: ${query}
34482
+
34483
+ Category:`;
34484
+ try {
34485
+ const handle = await args.selector.getSubstrate("concierge");
34486
+ if (!handle.capability.summarize) return "none";
34487
+ const response = await args.selector.invokeSummarize("concierge", {
34488
+ kind: "summarize",
34489
+ context: prompt,
34490
+ query: "Output the single category token.",
34491
+ maxTokens: 16
34492
+ });
34493
+ if (response.failureClass || response.body.kind !== "summarize") {
34494
+ return "none";
34495
+ }
34496
+ const raw = response.body.text.trim().toLowerCase();
34497
+ const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
34498
+ const normalized = head.replace(/[^a-z_]/g, "");
34499
+ const known = categories;
34500
+ if (known.includes(normalized)) {
34501
+ return normalized;
34502
+ }
34503
+ return "none";
34504
+ } catch {
34505
+ return "none";
34506
+ }
34507
+ };
34508
+ }
32974
34509
  function buildConciergePiiFilter() {
32975
34510
  return {
32976
34511
  filter(input) {
@@ -33102,13 +34637,13 @@ init_encryption();
33102
34637
  init_encoding();
33103
34638
  var INTELLIGENCE_NAMESPACE = "_intelligence";
33104
34639
  var SUBSTRATE_CONFIG_KEY = "substrate-config";
33105
- var HKDF_INFO3 = "intelligence-substrate-config";
34640
+ var HKDF_INFO4 = "intelligence-substrate-config";
33106
34641
  var IntelligenceConfigStore = class {
33107
34642
  storage;
33108
34643
  encryptionKey;
33109
34644
  constructor(storage, masterKey) {
33110
34645
  this.storage = storage;
33111
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
34646
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
33112
34647
  }
33113
34648
  /**
33114
34649
  * Load the operator's substrate config from disk. Returns the config
@@ -37054,12 +38589,18 @@ ${err.message}
37054
38589
  } : void 0;
37055
38590
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37056
38591
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
38592
+ const aggregatorPayloadStore = new AggregatorPayloadStore({
38593
+ storage,
38594
+ masterKey,
38595
+ fortressId: fortressIdForAggregator
38596
+ });
37057
38597
  const approvalAggregator = new ApprovalAggregator({
37058
38598
  storage,
37059
38599
  masterKey,
37060
38600
  auditLog,
37061
38601
  identityId: aggregatorIdentityId,
37062
- fortressId: fortressIdForAggregator
38602
+ fortressId: fortressIdForAggregator,
38603
+ payloadStore: aggregatorPayloadStore
37063
38604
  });
37064
38605
  const wrappedApprovalChannel = new AggregatorBackedChannel({
37065
38606
  underlying: approvalChannel,