@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.js CHANGED
@@ -4915,7 +4915,8 @@ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
4915
4915
  var RESERVED_EVENT_TYPE_PREFIXES = [
4916
4916
  "EXTENSION_",
4917
4917
  "cross_fortress_",
4918
- "multi_master_"
4918
+ "multi_master_",
4919
+ "cross_harness_approval_"
4919
4920
  ];
4920
4921
  function isReservedEventType(s) {
4921
4922
  return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
@@ -9095,9 +9096,9 @@ function fingerprintDID(did) {
9095
9096
  return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
9096
9097
  }
9097
9098
  function countInjectionsToday(audit) {
9098
- const startOfDay = /* @__PURE__ */ new Date();
9099
- startOfDay.setHours(0, 0, 0, 0);
9100
- const cutoff = startOfDay.getTime();
9099
+ const startOfDay2 = /* @__PURE__ */ new Date();
9100
+ startOfDay2.setHours(0, 0, 0, 0);
9101
+ const cutoff = startOfDay2.getTime();
9101
9102
  return audit.filter((e) => {
9102
9103
  const ts = new Date(e.timestamp).getTime();
9103
9104
  if (isNaN(ts) || ts < cutoff) return false;
@@ -9111,9 +9112,9 @@ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
9111
9112
  "proof_commitment"
9112
9113
  ]);
9113
9114
  function countProofsToday(audit) {
9114
- const startOfDay = /* @__PURE__ */ new Date();
9115
- startOfDay.setHours(0, 0, 0, 0);
9116
- const cutoff = startOfDay.getTime();
9115
+ const startOfDay2 = /* @__PURE__ */ new Date();
9116
+ startOfDay2.setHours(0, 0, 0, 0);
9117
+ const cutoff = startOfDay2.getTime();
9117
9118
  return audit.filter((e) => {
9118
9119
  if (e.layer !== "l3") return false;
9119
9120
  if (!PROOF_CREATION_OPS.has(e.operation)) return false;
@@ -16312,6 +16313,45 @@ async function handleApprovalInboxRoute(deps, req, res) {
16312
16313
  await handleStream2(deps, res);
16313
16314
  return true;
16314
16315
  }
16316
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/revision`) {
16317
+ const revision = await deps.aggregator.getRevision();
16318
+ writeJSON4(res, 200, { ok: true, data: { revision } });
16319
+ return true;
16320
+ }
16321
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/sync`) {
16322
+ const sinceRaw = url.searchParams.get("since_revision");
16323
+ const sinceParsed = sinceRaw === null ? 0 : Number.parseInt(sinceRaw, 10);
16324
+ const sinceRevision = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : 0;
16325
+ const limit = parseLimit2(
16326
+ url.searchParams.get("limit"),
16327
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16328
+ APPROVAL_INBOX_MAX_LIMIT
16329
+ );
16330
+ const delta = await deps.aggregator.getSync({ sinceRevision, limit });
16331
+ writeJSON4(res, 200, { ok: true, data: delta });
16332
+ return true;
16333
+ }
16334
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
16335
+ const limit = parseLimit2(
16336
+ url.searchParams.get("limit"),
16337
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16338
+ APPROVAL_INBOX_MAX_LIMIT
16339
+ );
16340
+ const statusRaw = url.searchParams.get("status");
16341
+ const sinceTs = url.searchParams.get("since") ?? void 0;
16342
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16343
+ const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
16344
+ const entries = await deps.aggregator.getHistory(
16345
+ {
16346
+ limit,
16347
+ ...filterStatus !== void 0 ? { status: filterStatus } : {},
16348
+ ...sinceTs !== void 0 ? { sinceTs } : {}
16349
+ },
16350
+ operatorId
16351
+ );
16352
+ writeJSON4(res, 200, { ok: true, data: { entries } });
16353
+ return true;
16354
+ }
16315
16355
  if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
16316
16356
  const limit = parseLimit2(
16317
16357
  url.searchParams.get("limit"),
@@ -16334,11 +16374,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
16334
16374
  writeJSON4(res, 404, { ok: false, error: "not_found", path });
16335
16375
  return true;
16336
16376
  }
16337
- if (method === "GET" && entryMatch.action === null) {
16338
- const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
16339
- const entry = entries.find(
16340
- (e) => e.aggregator_id === entryMatch.aggregatorId
16377
+ if (method === "GET" && entryMatch.action === "audit-trail") {
16378
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16379
+ if (!entry) {
16380
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16381
+ return true;
16382
+ }
16383
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16384
+ const trail = await deps.aggregator.getAuditTrail(
16385
+ entryMatch.aggregatorId,
16386
+ operatorId
16341
16387
  );
16388
+ writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
16389
+ return true;
16390
+ }
16391
+ if (method === "GET" && entryMatch.action === "payload") {
16392
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16393
+ if (!entry) {
16394
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16395
+ return true;
16396
+ }
16397
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16398
+ const payload = await deps.aggregator.getFullPayloadWithAudit(
16399
+ entryMatch.aggregatorId,
16400
+ operatorId
16401
+ );
16402
+ writeJSON4(res, 200, {
16403
+ ok: true,
16404
+ data: { entry, request_payload: payload }
16405
+ });
16406
+ return true;
16407
+ }
16408
+ if (method === "GET" && entryMatch.action === null) {
16409
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
16342
16410
  if (!entry) {
16343
16411
  writeJSON4(res, 404, { ok: false, error: "not_found" });
16344
16412
  return true;
@@ -19325,7 +19393,10 @@ var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
19325
19393
  var APPROVAL_AGGREGATOR_AUDIT_OPS = {
19326
19394
  AGGREGATED: "cross_harness_approval_aggregated",
19327
19395
  RESOLVED: "cross_harness_approval_resolved",
19328
- DEDUPED: "cross_harness_approval_deduped"
19396
+ DEDUPED: "cross_harness_approval_deduped",
19397
+ PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
19398
+ AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
19399
+ REPLAYED: "cross_harness_approval_replayed"
19329
19400
  };
19330
19401
  var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
19331
19402
  var DEFAULT_MAX_LIST_LIMIT = 200;
@@ -19341,6 +19412,8 @@ var ApprovalAggregator = class {
19341
19412
  now;
19342
19413
  resolveSourceContext;
19343
19414
  resolveHubInboxItemId;
19415
+ payloadStore;
19416
+ resolveEnforcementChain;
19344
19417
  /** Cached entries by `aggregator_id`. */
19345
19418
  entries = /* @__PURE__ */ new Map();
19346
19419
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -19353,6 +19426,20 @@ var ApprovalAggregator = class {
19353
19426
  hydrated = false;
19354
19427
  /** Active SSE listeners. */
19355
19428
  listeners = /* @__PURE__ */ new Set();
19429
+ /**
19430
+ * Monotonic revision counter, bumped on every mutation (ingest of new
19431
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
19432
+ * across persisted entries on first read; in-memory after that. v1.3
19433
+ * Upsilon-4.
19434
+ */
19435
+ currentRevision = 0;
19436
+ /**
19437
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
19438
+ * sync API to surface "removed" entries to mobile consumers between
19439
+ * polls. In-memory only; server restart clears tombstones (mobile
19440
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
19441
+ */
19442
+ removedTombstones = /* @__PURE__ */ new Map();
19356
19443
  constructor(deps) {
19357
19444
  this.storage = deps.storage;
19358
19445
  this.encryptionKey = derivePurposeKey(
@@ -19370,6 +19457,14 @@ var ApprovalAggregator = class {
19370
19457
  source_agent_id: this.fortressId
19371
19458
  }));
19372
19459
  this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
19460
+ this.payloadStore = deps.payloadStore ?? null;
19461
+ this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
19462
+ {
19463
+ layer: "l2",
19464
+ event: `approval_required:${event.operation}`,
19465
+ timestamp: event.request_timestamp
19466
+ }
19467
+ ]);
19373
19468
  }
19374
19469
  /**
19375
19470
  * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
@@ -19379,6 +19474,113 @@ var ApprovalAggregator = class {
19379
19474
  this.listeners.add(listener);
19380
19475
  return () => this.listeners.delete(listener);
19381
19476
  }
19477
+ /**
19478
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
19479
+ * poll the lightweight `/revision` route to detect that something
19480
+ * changed before fetching a full sync delta.
19481
+ */
19482
+ async getRevision() {
19483
+ await this.hydrate();
19484
+ return this.currentRevision;
19485
+ }
19486
+ /**
19487
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
19488
+ * clients poll this for cheap state-sync. Behavior:
19489
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
19490
+ * - `changed`: entries that existed at `sinceRevision` but had a
19491
+ * status transition (resolve, expire) since.
19492
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
19493
+ * - `revision`: current aggregator revision; pass this back as
19494
+ * `sinceRevision` on the next call.
19495
+ *
19496
+ * `limit` caps the total count returned across all three lists,
19497
+ * prioritized as added -> changed -> removed (newer-state first).
19498
+ * When more changes exist than fit, the next call with the returned
19499
+ * revision will pick up the rest because each entry's
19500
+ * last_modified_revision is unchanged by truncation.
19501
+ */
19502
+ async getSync(opts) {
19503
+ await this.hydrate();
19504
+ await this.expireStale();
19505
+ const sinceRevision = opts?.sinceRevision ?? 0;
19506
+ const cap = Math.min(
19507
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19508
+ this.maxListLimit
19509
+ );
19510
+ const added = [];
19511
+ const changed = [];
19512
+ for (const entry of this.entries.values()) {
19513
+ const lastMod = entry.last_modified_revision ?? 0;
19514
+ if (lastMod <= sinceRevision) continue;
19515
+ const createdRev = entry.created_at_revision ?? 0;
19516
+ if (createdRev > sinceRevision) {
19517
+ added.push(entry);
19518
+ } else {
19519
+ changed.push(entry);
19520
+ }
19521
+ }
19522
+ const removed = [];
19523
+ for (const [id, rev] of this.removedTombstones) {
19524
+ if (rev > sinceRevision) removed.push(id);
19525
+ }
19526
+ added.sort(
19527
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19528
+ );
19529
+ changed.sort(
19530
+ (a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
19531
+ );
19532
+ let remaining = cap;
19533
+ const addedOut = added.slice(0, Math.max(0, remaining));
19534
+ remaining -= addedOut.length;
19535
+ const changedOut = changed.slice(0, Math.max(0, remaining));
19536
+ remaining -= changedOut.length;
19537
+ const removedOut = removed.slice(0, Math.max(0, remaining));
19538
+ return {
19539
+ revision: this.currentRevision,
19540
+ added: addedOut,
19541
+ changed: changedOut,
19542
+ removed: removedOut
19543
+ };
19544
+ }
19545
+ /**
19546
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
19547
+ * and the at-rest payload (if a payload store is wired). Records a
19548
+ * tombstone with the new revision so sync-API consumers see a
19549
+ * `removed` delta. Returns true when an entry was deleted, false on
19550
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
19551
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
19552
+ * removal path.
19553
+ */
19554
+ async deleteEntry(aggregatorId) {
19555
+ await this.hydrate();
19556
+ const entry = this.entries.get(aggregatorId);
19557
+ if (!entry) return false;
19558
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19559
+ this.entries.delete(aggregatorId);
19560
+ this.dedupIndex.delete(dedupKey);
19561
+ this.fullPayloads.delete(aggregatorId);
19562
+ for (const [corr, id] of this.correlationIndex) {
19563
+ if (id === aggregatorId) this.correlationIndex.delete(corr);
19564
+ }
19565
+ try {
19566
+ await this.storage.delete(APPROVAL_AGGREGATOR_NAMESPACE, aggregatorId);
19567
+ } catch {
19568
+ }
19569
+ if (this.payloadStore) {
19570
+ try {
19571
+ await this.payloadStore.deletePayload(aggregatorId);
19572
+ } catch {
19573
+ }
19574
+ }
19575
+ const revision = this.nextRevision();
19576
+ this.removedTombstones.set(aggregatorId, revision);
19577
+ this.emit({ type: "removed", entry: { ...entry } });
19578
+ return true;
19579
+ }
19580
+ nextRevision() {
19581
+ this.currentRevision += 1;
19582
+ return this.currentRevision;
19583
+ }
19382
19584
  /**
19383
19585
  * Ingest a gate event. Returns the aggregator entry on first sight,
19384
19586
  * `null` when deduped. Resolution events update the existing record;
@@ -19420,13 +19622,152 @@ var ApprovalAggregator = class {
19420
19622
  }
19421
19623
  /**
19422
19624
  * Return the original (unhashed) request payload for the entry. Returns
19423
- * `null` when the entry is unknown or the payload was evicted (e.g. the
19424
- * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
19625
+ * `null` when the entry is unknown. When the in-memory payload map has
19626
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
19627
+ * provided, the at-rest bundle is decrypted and the in-memory map is
19628
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
19629
+ * accessor is silent so internal callers can read without polluting the
19630
+ * audit trail.
19425
19631
  */
19426
19632
  async getFullPayload(aggregatorId) {
19427
19633
  await this.hydrate();
19428
19634
  if (!this.entries.has(aggregatorId)) return null;
19429
- return this.fullPayloads.get(aggregatorId) ?? null;
19635
+ const cached = this.fullPayloads.get(aggregatorId);
19636
+ if (cached !== void 0) return cached;
19637
+ if (this.payloadStore) {
19638
+ try {
19639
+ const restored = await this.payloadStore.loadPayload(aggregatorId);
19640
+ if (restored !== null) {
19641
+ this.fullPayloads.set(aggregatorId, restored);
19642
+ return restored;
19643
+ }
19644
+ } catch {
19645
+ }
19646
+ }
19647
+ return null;
19648
+ }
19649
+ /**
19650
+ * Return the entry record for the given id, or null when unknown.
19651
+ * Idempotent. v1.3 Upsilon-3.
19652
+ */
19653
+ async getEntry(aggregatorId) {
19654
+ await this.hydrate();
19655
+ return this.entries.get(aggregatorId) ?? null;
19656
+ }
19657
+ /**
19658
+ * Audited variant of `getFullPayload`. Emits the
19659
+ * `cross_harness_approval_payload_decrypted` audit event before
19660
+ * returning. Used by the operator-facing /payload replay route.
19661
+ * v1.3 Upsilon-3.
19662
+ */
19663
+ async getFullPayloadWithAudit(aggregatorId, operatorId) {
19664
+ const payload = await this.getFullPayload(aggregatorId);
19665
+ if (payload === null) return null;
19666
+ const entry = this.entries.get(aggregatorId);
19667
+ this.auditLog.append(
19668
+ "l2",
19669
+ APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
19670
+ operatorId,
19671
+ {
19672
+ aggregator_id: aggregatorId,
19673
+ ...entry ? {
19674
+ source_harness: entry.source_harness,
19675
+ source_agent_id: entry.source_agent_id,
19676
+ entry_status: entry.status
19677
+ } : {}
19678
+ }
19679
+ );
19680
+ return payload;
19681
+ }
19682
+ /**
19683
+ * Return the audit-log entries that led to and surround this approval.
19684
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
19685
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
19686
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
19687
+ * aggregator id at v1.3, so they are matched via timestamp window
19688
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
19689
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
19690
+ * v1.3 Upsilon-3.
19691
+ */
19692
+ async getAuditTrail(aggregatorId, operatorId) {
19693
+ await this.hydrate();
19694
+ const entry = this.entries.get(aggregatorId);
19695
+ if (!entry) {
19696
+ return [];
19697
+ }
19698
+ const sinceMs = Date.parse(entry.created_at) - 1e3;
19699
+ const sinceIso = new Date(sinceMs).toISOString();
19700
+ const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
19701
+ const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
19702
+ const lifetimeStart = sinceMs;
19703
+ const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
19704
+ const matches = [];
19705
+ for (const audit of queried.entries) {
19706
+ const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
19707
+ if (detailsId === aggregatorId) {
19708
+ matches.push(audit);
19709
+ continue;
19710
+ }
19711
+ const auditMs = Date.parse(audit.timestamp);
19712
+ if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
19713
+ if (audit.operation.endsWith(`:${operationPart}`)) {
19714
+ matches.push(audit);
19715
+ }
19716
+ }
19717
+ matches.sort(
19718
+ (a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
19719
+ );
19720
+ this.auditLog.append(
19721
+ "l2",
19722
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
19723
+ operatorId,
19724
+ {
19725
+ aggregator_id: aggregatorId,
19726
+ entry_status: entry.status,
19727
+ match_count: matches.length
19728
+ }
19729
+ );
19730
+ return matches;
19731
+ }
19732
+ /**
19733
+ * List historical (resolved) approvals. Excludes pending entries by
19734
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
19735
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
19736
+ * Upsilon-3.
19737
+ */
19738
+ async getHistory(opts, operatorId) {
19739
+ await this.hydrate();
19740
+ await this.expireStale();
19741
+ const limit = Math.min(
19742
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19743
+ this.maxListLimit
19744
+ );
19745
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
19746
+ const matching = [];
19747
+ for (const entry of this.entries.values()) {
19748
+ if (entry.status === "pending") continue;
19749
+ if (opts?.status && entry.status !== opts.status) continue;
19750
+ const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
19751
+ if (stamp < sinceMs) continue;
19752
+ matching.push(entry);
19753
+ }
19754
+ matching.sort((a, b) => {
19755
+ const aStamp = a.resolved_at ?? a.created_at;
19756
+ const bStamp = b.resolved_at ?? b.created_at;
19757
+ return bStamp.localeCompare(aStamp);
19758
+ });
19759
+ const sliced = matching.slice(0, limit);
19760
+ this.auditLog.append(
19761
+ "l2",
19762
+ APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
19763
+ operatorId,
19764
+ {
19765
+ result_count: sliced.length,
19766
+ ...opts?.status !== void 0 ? { status_filter: opts.status } : {},
19767
+ ...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
19768
+ }
19769
+ );
19770
+ return sliced;
19430
19771
  }
19431
19772
  /**
19432
19773
  * Resolve an entry. Used by both:
@@ -19450,6 +19791,7 @@ var ApprovalAggregator = class {
19450
19791
  entry.status = decision;
19451
19792
  entry.resolved_at = this.now().toISOString();
19452
19793
  entry.resolved_by = operatorId;
19794
+ entry.last_modified_revision = this.nextRevision();
19453
19795
  await this.persist(entry);
19454
19796
  this.auditLog.append(
19455
19797
  "l2",
@@ -19500,6 +19842,8 @@ var ApprovalAggregator = class {
19500
19842
  const now = this.now();
19501
19843
  const expires = new Date(now.getTime() + this.pendingTtlMs);
19502
19844
  const hubInboxId = this.resolveHubInboxItemId(event);
19845
+ const enforcementChain = this.resolveEnforcementChain(event);
19846
+ const revision = this.nextRevision();
19503
19847
  const entry = {
19504
19848
  aggregator_id: id,
19505
19849
  source_harness: ctx.source_harness,
@@ -19511,13 +19855,22 @@ var ApprovalAggregator = class {
19511
19855
  status: "pending",
19512
19856
  created_at: now.toISOString(),
19513
19857
  expires_at: expires.toISOString(),
19514
- ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
19858
+ created_at_revision: revision,
19859
+ last_modified_revision: revision,
19860
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
19861
+ ...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
19515
19862
  };
19516
19863
  this.entries.set(id, entry);
19517
19864
  this.dedupIndex.set(dedupKey, id);
19518
19865
  this.correlationIndex.set(event.correlation_id, id);
19519
19866
  this.fullPayloads.set(id, event.context);
19520
19867
  await this.persist(entry);
19868
+ if (this.payloadStore) {
19869
+ try {
19870
+ await this.payloadStore.savePayload(id, event.context);
19871
+ } catch {
19872
+ }
19873
+ }
19521
19874
  this.auditLog.append(
19522
19875
  "l2",
19523
19876
  APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
@@ -19546,6 +19899,7 @@ var ApprovalAggregator = class {
19546
19899
  entry.status = status;
19547
19900
  entry.resolved_at = event.resolution.decided_at;
19548
19901
  entry.resolved_by = event.resolution.decided_by;
19902
+ entry.last_modified_revision = this.nextRevision();
19549
19903
  await this.persist(entry);
19550
19904
  this.auditLog.append(
19551
19905
  "l2",
@@ -19608,6 +19962,7 @@ var ApprovalAggregator = class {
19608
19962
  entry.status = "expired";
19609
19963
  entry.resolved_at = this.now().toISOString();
19610
19964
  entry.resolved_by = "system_ttl";
19965
+ entry.last_modified_revision = this.nextRevision();
19611
19966
  await this.persist(entry);
19612
19967
  this.auditLog.append(
19613
19968
  "l2",
@@ -19654,6 +20009,10 @@ var ApprovalAggregator = class {
19654
20009
  this.entries.set(entry.aggregator_id, entry);
19655
20010
  const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19656
20011
  this.dedupIndex.set(dedupKey, entry.aggregator_id);
20012
+ const lastMod = entry.last_modified_revision ?? 0;
20013
+ if (lastMod > this.currentRevision) {
20014
+ this.currentRevision = lastMod;
20015
+ }
19657
20016
  } catch {
19658
20017
  }
19659
20018
  }
@@ -19826,6 +20185,143 @@ function makeRedirectResolverFromPolicySupplier(supplier) {
19826
20185
  };
19827
20186
  }
19828
20187
 
20188
+ // src/principal-policy/aggregator-store.ts
20189
+ init_encryption();
20190
+ init_encoding();
20191
+ var AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
20192
+ var AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
20193
+ var HKDF_INFO = "l2-approval-aggregator-payload-v1";
20194
+ var DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
20195
+ var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
20196
+ var AggregatorPayloadStore = class {
20197
+ storage;
20198
+ encryptionKey;
20199
+ fortressId;
20200
+ retentionDays;
20201
+ constructor(opts) {
20202
+ this.storage = opts.storage;
20203
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
20204
+ this.fortressId = opts.fortressId;
20205
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
20206
+ }
20207
+ /**
20208
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
20209
+ * twice with the same id rewrites the bundle (retention_until is
20210
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
20211
+ * so callers can log it.
20212
+ */
20213
+ async savePayload(aggregatorId, payload) {
20214
+ const now = /* @__PURE__ */ new Date();
20215
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
20216
+ const retentionUntil = new Date(now.getTime() + retentionMs);
20217
+ const bundle = {
20218
+ version: 1,
20219
+ aggregator_id: aggregatorId,
20220
+ fortress_id: this.fortressId,
20221
+ created_at: now.toISOString(),
20222
+ retention_until: retentionUntil.toISOString(),
20223
+ payload
20224
+ };
20225
+ const aad = stringToBytes(aggregatorId);
20226
+ const plaintext = stringToBytes(JSON.stringify(bundle));
20227
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
20228
+ await this.storage.write(
20229
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20230
+ payloadKey(aggregatorId),
20231
+ stringToBytes(JSON.stringify(envelope))
20232
+ );
20233
+ return bundle.retention_until;
20234
+ }
20235
+ /**
20236
+ * Read the persisted payload for the aggregator_id. Returns null if no
20237
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
20238
+ */
20239
+ async loadPayload(aggregatorId) {
20240
+ const key = payloadKey(aggregatorId);
20241
+ let raw;
20242
+ try {
20243
+ raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
20244
+ } catch {
20245
+ return null;
20246
+ }
20247
+ if (!raw) return null;
20248
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
20249
+ try {
20250
+ const envelope = JSON.parse(bytesToString(raw));
20251
+ const aad = stringToBytes(aggregatorId);
20252
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
20253
+ const parsed = JSON.parse(
20254
+ bytesToString(plaintext)
20255
+ );
20256
+ if (parsed.version !== 1) return null;
20257
+ if (parsed.aggregator_id !== aggregatorId) return null;
20258
+ return parsed.payload;
20259
+ } catch {
20260
+ return null;
20261
+ }
20262
+ }
20263
+ /**
20264
+ * Delete the persisted payload. Returns true when a bundle was removed,
20265
+ * false when none existed.
20266
+ */
20267
+ async deletePayload(aggregatorId) {
20268
+ const key = payloadKey(aggregatorId);
20269
+ const existed = await this.storage.exists(
20270
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20271
+ key
20272
+ );
20273
+ if (!existed) return false;
20274
+ try {
20275
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
20276
+ } catch {
20277
+ return false;
20278
+ }
20279
+ return true;
20280
+ }
20281
+ /**
20282
+ * Drop expired payload bundles. Returns the count of bundles pruned.
20283
+ * Caller wires this into the cocoon-unlock initialization path.
20284
+ */
20285
+ async pruneExpired(now) {
20286
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
20287
+ const entries = await this.storage.list(
20288
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20289
+ AGGREGATOR_PAYLOAD_KEY_PREFIX
20290
+ );
20291
+ let pruned = 0;
20292
+ for (const meta of entries) {
20293
+ const aggregatorId = stripKeyPrefix(meta.key);
20294
+ if (aggregatorId === null) continue;
20295
+ const raw = await this.storage.read(
20296
+ AGGREGATOR_PAYLOAD_NAMESPACE,
20297
+ meta.key
20298
+ );
20299
+ if (!raw) continue;
20300
+ try {
20301
+ const envelope = JSON.parse(bytesToString(raw));
20302
+ const aad = stringToBytes(aggregatorId);
20303
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
20304
+ const parsed = JSON.parse(
20305
+ bytesToString(plaintext)
20306
+ );
20307
+ if (parsed.retention_until <= cutoff) {
20308
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
20309
+ pruned += 1;
20310
+ }
20311
+ } catch {
20312
+ }
20313
+ }
20314
+ return { pruned };
20315
+ }
20316
+ };
20317
+ function payloadKey(aggregatorId) {
20318
+ return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
20319
+ }
20320
+ function stripKeyPrefix(key) {
20321
+ if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
20322
+ return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
20323
+ }
20324
+
19829
20325
  // src/principal-policy/tools.ts
19830
20326
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
19831
20327
  return [
@@ -31967,6 +32463,13 @@ init_encoding();
31967
32463
  // src/chat/operator-chat-audit-events.ts
31968
32464
  var OPERATOR_CHAT_OPS = {
31969
32465
  CONCIERGE_CHAT: "operator_concierge_chat",
32466
+ /**
32467
+ * Click-to-inspect panel opened on an agent row. Repurposed from the
32468
+ * direct-agent session-open audit event in the v1.2 reshape; the click
32469
+ * affordance now opens an inspect/approve panel (recent activity +
32470
+ * pending approvals + policy summary) instead of a chat session.
32471
+ */
32472
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
31970
32473
  /**
31971
32474
  * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
31972
32475
  * when the operator hits the list-threads or read-thread route. Body
@@ -31986,20 +32489,831 @@ var OPERATOR_CHAT_OPS = {
31986
32489
  * turns; the concierge degrades to single-turn after emitting. Body
31987
32490
  * carries thread_id + a stable failure_reason enum.
31988
32491
  */
31989
- CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
32492
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
32493
+ /**
32494
+ * Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
32495
+ * when a category fetcher throws while assembling the dynamic context
32496
+ * fold. The concierge omits that category and continues; the user-
32497
+ * facing query is never broken. Body carries category + failure_reason.
32498
+ */
32499
+ CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
31990
32500
  };
31991
32501
 
31992
32502
  // src/chat/operator-chat-types.ts
31993
32503
  var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
31994
32504
  var CONCIERGE_THREAD_KEY = "_fortress";
31995
32505
 
32506
+ // src/chat/concierge-context-router.ts
32507
+ var APPROX_CHARS_PER_TOKEN = 4;
32508
+ var DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
32509
+ var DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
32510
+ var CONTEXT_CATEGORIES = [
32511
+ "templates",
32512
+ "agent_state",
32513
+ "agent_activity",
32514
+ "audit_log",
32515
+ "sentinel_findings",
32516
+ "anomaly_alerts",
32517
+ "recent_receipts",
32518
+ "verascore_deltas"
32519
+ ];
32520
+ function phrasePattern(phrase) {
32521
+ const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
32522
+ return { source: `\\b${escaped}\\b`, phrase };
32523
+ }
32524
+ var CATEGORY_KEYWORDS = [
32525
+ {
32526
+ category: "templates",
32527
+ patterns: [
32528
+ "templates",
32529
+ "template",
32530
+ "channel templates",
32531
+ "channel template",
32532
+ "list templates",
32533
+ "available templates",
32534
+ "what templates"
32535
+ ].map(phrasePattern)
32536
+ },
32537
+ {
32538
+ category: "agent_state",
32539
+ patterns: [
32540
+ "state",
32541
+ "status",
32542
+ "agent state",
32543
+ "agent status",
32544
+ "status of agent",
32545
+ "status of agents",
32546
+ "state of",
32547
+ "doing"
32548
+ ].map(phrasePattern)
32549
+ },
32550
+ {
32551
+ category: "agent_activity",
32552
+ patterns: [
32553
+ "activity",
32554
+ "agent activity",
32555
+ "what did",
32556
+ "recent activity"
32557
+ ].map(phrasePattern)
32558
+ },
32559
+ {
32560
+ category: "audit_log",
32561
+ patterns: [
32562
+ "audit log",
32563
+ "audit",
32564
+ "log entry",
32565
+ "log entries",
32566
+ "what happened",
32567
+ "show me events",
32568
+ "event class"
32569
+ ].map(phrasePattern)
32570
+ },
32571
+ {
32572
+ category: "sentinel_findings",
32573
+ patterns: [
32574
+ "sentinel",
32575
+ "sentinels",
32576
+ "warning",
32577
+ "warnings",
32578
+ "alert",
32579
+ "alerts",
32580
+ "whats wrong",
32581
+ "what's wrong",
32582
+ "findings"
32583
+ ].map(phrasePattern)
32584
+ },
32585
+ {
32586
+ category: "anomaly_alerts",
32587
+ patterns: [
32588
+ "anomaly",
32589
+ "anomalies",
32590
+ "spike",
32591
+ "unusual",
32592
+ "outlier"
32593
+ ].map(phrasePattern)
32594
+ },
32595
+ {
32596
+ category: "recent_receipts",
32597
+ patterns: [
32598
+ "receipt",
32599
+ "receipts",
32600
+ "concordia",
32601
+ "commitment",
32602
+ "commitments",
32603
+ "chain",
32604
+ "chains"
32605
+ ].map(phrasePattern)
32606
+ },
32607
+ {
32608
+ category: "verascore_deltas",
32609
+ patterns: [
32610
+ "verascore",
32611
+ "vera score",
32612
+ "trust score",
32613
+ "reputation"
32614
+ ].map(phrasePattern)
32615
+ }
32616
+ ];
32617
+ function extractAgentNameHint(query) {
32618
+ const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
32619
+ const m = query.match(agentPattern);
32620
+ if (m && m[1]) return m[1];
32621
+ const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
32622
+ if (quoted && quoted[1]) return quoted[1];
32623
+ return null;
32624
+ }
32625
+ var TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
32626
+ "hi",
32627
+ "hello",
32628
+ "hey",
32629
+ "yo",
32630
+ "ok",
32631
+ "thanks",
32632
+ "thx",
32633
+ "thank you"
32634
+ ]);
32635
+ function isTrivialQuery(query) {
32636
+ const norm = query.trim().toLowerCase();
32637
+ if (norm.length === 0) return true;
32638
+ if (norm.length < 8) return true;
32639
+ return TRIVIAL_GREETINGS.has(norm);
32640
+ }
32641
+ function classifyQuery(query, parsedGrammar) {
32642
+ const normalized = query.toLowerCase();
32643
+ const matches = [];
32644
+ const grammarAgent = parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null;
32645
+ for (const spec of CATEGORY_KEYWORDS) {
32646
+ const matchedPhrases = [];
32647
+ for (const pattern of spec.patterns) {
32648
+ if (matchedPhrases.includes(pattern.phrase)) continue;
32649
+ const re = new RegExp(pattern.source, "i");
32650
+ if (re.test(normalized)) {
32651
+ matchedPhrases.push(pattern.phrase);
32652
+ }
32653
+ }
32654
+ if (matchedPhrases.length === 0) continue;
32655
+ const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
32656
+ const wantsAgentHint = spec.category === "agent_state" || spec.category === "agent_activity";
32657
+ const agent_name_hint = wantsAgentHint ? grammarAgent ?? extractAgentNameHint(query) : null;
32658
+ matches.push({
32659
+ category: spec.category,
32660
+ confidence,
32661
+ matched_keywords: matchedPhrases,
32662
+ agent_name_hint,
32663
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32664
+ });
32665
+ }
32666
+ matches.sort((a, b) => {
32667
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
32668
+ return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
32669
+ });
32670
+ return matches;
32671
+ }
32672
+ function fetcherHintsFromGrammar(parsed) {
32673
+ if (!parsed) return void 0;
32674
+ const hasTime = parsed.time_range !== null;
32675
+ const hasAgents = parsed.agent_names.length > 0;
32676
+ const hasEvents = parsed.event_types.length > 0;
32677
+ if (!hasTime && !hasAgents && !hasEvents) return void 0;
32678
+ const hints = {};
32679
+ if (parsed.time_range) {
32680
+ const range = parsed.time_range;
32681
+ hints.time_range = {
32682
+ start: range.start,
32683
+ end: range.end,
32684
+ ...range.relative_label !== void 0 ? { relative_label: range.relative_label } : {}
32685
+ };
32686
+ }
32687
+ if (hasAgents) hints.agent_names = parsed.agent_names;
32688
+ if (hasEvents) hints.event_types = parsed.event_types;
32689
+ return hints;
32690
+ }
32691
+ function approxTokenLen(text) {
32692
+ return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
32693
+ }
32694
+ var CATEGORY_LABELS = {
32695
+ templates: "Templates",
32696
+ agent_state: "Agent state",
32697
+ agent_activity: "Agent activity",
32698
+ audit_log: "Audit log",
32699
+ sentinel_findings: "Sentinel findings",
32700
+ anomaly_alerts: "Anomaly alerts",
32701
+ recent_receipts: "Recent receipts",
32702
+ verascore_deltas: "Verascore deltas"
32703
+ };
32704
+ async function runFetcher(match, fetchers, hints) {
32705
+ switch (match.category) {
32706
+ case "templates":
32707
+ return fetchers.templates(hints);
32708
+ case "agent_state":
32709
+ return fetchers.agent_state(match.agent_name_hint, hints);
32710
+ case "agent_activity":
32711
+ return fetchers.agent_activity(match.agent_name_hint, hints);
32712
+ case "audit_log":
32713
+ return fetchers.audit_log(hints);
32714
+ case "sentinel_findings":
32715
+ return fetchers.sentinel_findings(hints);
32716
+ case "anomaly_alerts":
32717
+ return fetchers.anomaly_alerts(hints);
32718
+ case "recent_receipts":
32719
+ return fetchers.recent_receipts(hints);
32720
+ case "verascore_deltas":
32721
+ return fetchers.verascore_deltas(hints);
32722
+ }
32723
+ }
32724
+ function trivialMatch(category, parsedGrammar) {
32725
+ return {
32726
+ category,
32727
+ confidence: 0.5,
32728
+ matched_keywords: ["llm-assist"],
32729
+ agent_name_hint: parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null,
32730
+ ...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
32731
+ };
32732
+ }
32733
+ async function foldContext(query, fetchers, opts) {
32734
+ const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
32735
+ const parsed = opts?.parsed ?? null;
32736
+ const hints = fetcherHintsFromGrammar(parsed);
32737
+ let matches = classifyQuery(query, parsed);
32738
+ if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
32739
+ try {
32740
+ const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
32741
+ if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
32742
+ matches = [trivialMatch(picked, parsed)];
32743
+ }
32744
+ } catch {
32745
+ }
32746
+ }
32747
+ if (matches.length === 0) {
32748
+ return { section: "", categoriesIncluded: [] };
32749
+ }
32750
+ const attempts = [];
32751
+ for (const match of matches) {
32752
+ try {
32753
+ const text = await runFetcher(match, fetchers, hints);
32754
+ const trimmed = text.trim();
32755
+ if (trimmed.length > 0) {
32756
+ attempts.push({ category: match.category, text: trimmed });
32757
+ }
32758
+ } catch (err) {
32759
+ opts?.onFetcherFailure?.(match.category, err);
32760
+ }
32761
+ }
32762
+ if (attempts.length === 0) {
32763
+ return { section: "", categoriesIncluded: [] };
32764
+ }
32765
+ const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
32766
+ `);
32767
+ const sepTokens = approxTokenLen("\n\n");
32768
+ let runningTokens = headerTokens;
32769
+ const kept = [];
32770
+ for (const attempt of attempts) {
32771
+ const block = `### ${CATEGORY_LABELS[attempt.category]}
32772
+ ${attempt.text}`;
32773
+ const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
32774
+ if (kept.length === 0) {
32775
+ kept.push(attempt);
32776
+ runningTokens += tokens;
32777
+ continue;
32778
+ }
32779
+ if (runningTokens + tokens > budget) break;
32780
+ kept.push(attempt);
32781
+ runningTokens += tokens;
32782
+ }
32783
+ const blocks = kept.map(
32784
+ (k) => `### ${CATEGORY_LABELS[k.category]}
32785
+ ${k.text}`
32786
+ );
32787
+ const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
32788
+ ${blocks.join("\n\n")}`;
32789
+ return {
32790
+ section,
32791
+ categoriesIncluded: kept.map((k) => k.category)
32792
+ };
32793
+ }
32794
+
32795
+ // src/composition/constants.ts
32796
+ var COMPOSITION_EVENT_TYPES = [
32797
+ "composition_receipt_packed",
32798
+ "composition_receipt_verified",
32799
+ "composition_mandate_verified",
32800
+ "composition_verascore_published",
32801
+ "composition_sidecar_spawned",
32802
+ "composition_sidecar_crashed",
32803
+ "composition_sidecar_recovered",
32804
+ "composition_degraded",
32805
+ "composition_recovered"
32806
+ ];
32807
+
32808
+ // src/chat/concierge-query-grammar.ts
32809
+ var CANONICAL_AUDIT_EVENT_CLASSES = [
32810
+ // Lifecycle / policy
32811
+ "policy_change",
32812
+ "approval_request",
32813
+ "audit_truncate",
32814
+ "lockdown",
32815
+ "unwrap",
32816
+ // Exit bundle (Tier 1)
32817
+ "exit_bundle_export",
32818
+ "exit_bundle_import_activate",
32819
+ "exit_bundle_rekey",
32820
+ // Cross-harness approval aggregator
32821
+ "cross_harness_approval_aggregated",
32822
+ "cross_harness_approval_resolved",
32823
+ "cross_harness_approval_deduped",
32824
+ "cross_harness_approval_payload_decrypted",
32825
+ "cross_harness_approval_audit_trail_viewed",
32826
+ "cross_harness_approval_replayed",
32827
+ // Composition (full set from constants.ts)
32828
+ ...COMPOSITION_EVENT_TYPES,
32829
+ // Operator chat / concierge (full set from OPERATOR_CHAT_OPS)
32830
+ OPERATOR_CHAT_OPS.CONCIERGE_CHAT,
32831
+ OPERATOR_CHAT_OPS.AGENT_INSPECT_PANEL_OPENED,
32832
+ OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ,
32833
+ OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED,
32834
+ OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED,
32835
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
32836
+ // Bridge / commitment
32837
+ "bridge_commit",
32838
+ "bridge_verify",
32839
+ "bridge_attest",
32840
+ "proof_commitment",
32841
+ "proof_reveal",
32842
+ // Reputation
32843
+ "reputation_export",
32844
+ "reputation_import",
32845
+ "reputation_publish",
32846
+ "reputation_record",
32847
+ "reputation_query"
32848
+ ];
32849
+ var EVENT_SYNONYMS = [
32850
+ { phrase: "approvals", canonical: ["approval_request", "cross_harness_approval_aggregated", "cross_harness_approval_resolved"] },
32851
+ { phrase: "approval", canonical: ["approval_request"] },
32852
+ { phrase: "policy changes", canonical: ["policy_change"] },
32853
+ { phrase: "policy change", canonical: ["policy_change"] },
32854
+ { phrase: "policy edits", canonical: ["policy_change"] },
32855
+ { phrase: "lockdowns", canonical: ["lockdown"] },
32856
+ { phrase: "exit bundles", canonical: ["exit_bundle_export", "exit_bundle_import_activate"] },
32857
+ { phrase: "exit bundle", canonical: ["exit_bundle_export"] },
32858
+ { phrase: "audit truncations", canonical: ["audit_truncate"] },
32859
+ { phrase: "audit truncation", canonical: ["audit_truncate"] },
32860
+ { phrase: "compositions", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32861
+ { phrase: "receipts", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
32862
+ { phrase: "receipt verifications", canonical: ["composition_receipt_verified"] },
32863
+ { phrase: "concierge chats", canonical: [OPERATOR_CHAT_OPS.CONCIERGE_CHAT] },
32864
+ { phrase: "cross harness approvals", canonical: ["cross_harness_approval_aggregated", "cross_harness_approval_resolved"] }
32865
+ ];
32866
+ var MS_PER_HOUR = 60 * 60 * 1e3;
32867
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
32868
+ var NUMBER_WORDS = {
32869
+ a: 1,
32870
+ an: 1,
32871
+ one: 1,
32872
+ two: 2,
32873
+ three: 3,
32874
+ four: 4,
32875
+ five: 5,
32876
+ six: 6,
32877
+ seven: 7,
32878
+ eight: 8,
32879
+ nine: 9,
32880
+ ten: 10,
32881
+ twelve: 12,
32882
+ twentyfour: 24
32883
+ };
32884
+ function resolveTimeRange(query, now) {
32885
+ const normalized = query.trim();
32886
+ const lower = normalized.toLowerCase();
32887
+ const fromTo = lower.match(
32888
+ /\b(?:from|between)\s+(.+?)\s+(?:to|and|-|until)\s+([\w:.\-+t /]+)/i
32889
+ );
32890
+ if (fromTo) {
32891
+ const aSlice = fromTo[1];
32892
+ const bSlice = fromTo[2];
32893
+ if (aSlice !== void 0 && bSlice !== void 0) {
32894
+ const a = parseInstant(aSlice, now);
32895
+ const b = parseInstant(bSlice, now);
32896
+ if (a && b) {
32897
+ const start = a.getTime() <= b.getTime() ? a : b;
32898
+ const end = a.getTime() <= b.getTime() ? b : a;
32899
+ return {
32900
+ range: { start, end },
32901
+ matchedSubstring: fromTo[0]
32902
+ };
32903
+ }
32904
+ }
32905
+ }
32906
+ const sinceMatch = lower.match(/\bsince\s+([\w:.\-+t /]+)/i);
32907
+ if (sinceMatch) {
32908
+ const slice = sinceMatch[1];
32909
+ if (slice !== void 0) {
32910
+ const start = parseInstant(slice, now);
32911
+ if (start) {
32912
+ return {
32913
+ range: { start, end: now },
32914
+ matchedSubstring: sinceMatch[0]
32915
+ };
32916
+ }
32917
+ }
32918
+ }
32919
+ if (/\byesterday\b/.test(lower)) {
32920
+ const startOfToday = startOfDay(now);
32921
+ const start = new Date(startOfToday.getTime() - MS_PER_DAY);
32922
+ const end = new Date(startOfToday.getTime() - 1);
32923
+ return {
32924
+ range: { start, end, relative_label: "yesterday" },
32925
+ matchedSubstring: "yesterday"
32926
+ };
32927
+ }
32928
+ if (/\btoday\b/.test(lower)) {
32929
+ return {
32930
+ range: {
32931
+ start: startOfDay(now),
32932
+ end: now,
32933
+ relative_label: "today"
32934
+ },
32935
+ matchedSubstring: "today"
32936
+ };
32937
+ }
32938
+ const compactHours = lower.match(/\blast\s+(\d+)\s*h\b/i);
32939
+ if (compactHours) {
32940
+ const tok = compactHours[1];
32941
+ if (tok !== void 0) {
32942
+ const n = Number.parseInt(tok, 10);
32943
+ if (Number.isFinite(n) && n > 0) {
32944
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32945
+ return {
32946
+ range: { start, end: now, relative_label: `last ${n}h` },
32947
+ matchedSubstring: compactHours[0]
32948
+ };
32949
+ }
32950
+ }
32951
+ }
32952
+ const hoursMatch = lower.match(
32953
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:hr\b|hrs\b|hour|hours)/i
32954
+ );
32955
+ if (hoursMatch) {
32956
+ const tok = hoursMatch[1];
32957
+ if (tok !== void 0) {
32958
+ const n = parseCount(tok);
32959
+ if (n !== null && n > 0) {
32960
+ const start = new Date(now.getTime() - n * MS_PER_HOUR);
32961
+ return {
32962
+ range: { start, end: now, relative_label: `past ${n} hour${n === 1 ? "" : "s"}` },
32963
+ matchedSubstring: hoursMatch[0]
32964
+ };
32965
+ }
32966
+ }
32967
+ }
32968
+ if (/\b(?:past|last)\s+hour\b/.test(lower)) {
32969
+ const start = new Date(now.getTime() - MS_PER_HOUR);
32970
+ return {
32971
+ range: { start, end: now, relative_label: "past hour" },
32972
+ matchedSubstring: lower.match(/\b(?:past|last)\s+hour\b/i)[0]
32973
+ };
32974
+ }
32975
+ const daysMatch = lower.match(
32976
+ /\b(?:past|last)\s+([\w]+|\d+)\s*(?:d\b|day|days)/i
32977
+ );
32978
+ if (daysMatch) {
32979
+ const tok = daysMatch[1];
32980
+ if (tok !== void 0) {
32981
+ const n = parseCount(tok);
32982
+ if (n !== null && n > 0) {
32983
+ const start = new Date(now.getTime() - n * MS_PER_DAY);
32984
+ return {
32985
+ range: { start, end: now, relative_label: `past ${n} day${n === 1 ? "" : "s"}` },
32986
+ matchedSubstring: daysMatch[0]
32987
+ };
32988
+ }
32989
+ }
32990
+ }
32991
+ if (/\b(?:past|last)\s+day\b/.test(lower)) {
32992
+ const start = new Date(now.getTime() - MS_PER_DAY);
32993
+ return {
32994
+ range: { start, end: now, relative_label: "past day" },
32995
+ matchedSubstring: lower.match(/\b(?:past|last)\s+day\b/i)[0]
32996
+ };
32997
+ }
32998
+ if (/\bthis\s+week\b/.test(lower)) {
32999
+ const start = startOfWeek(now);
33000
+ return {
33001
+ range: { start, end: now, relative_label: "this week" },
33002
+ matchedSubstring: lower.match(/\bthis\s+week\b/i)[0]
33003
+ };
33004
+ }
33005
+ if (/\b(?:past|last)\s+week\b/.test(lower)) {
33006
+ const start = new Date(now.getTime() - 7 * MS_PER_DAY);
33007
+ return {
33008
+ range: { start, end: now, relative_label: "past week" },
33009
+ matchedSubstring: lower.match(/\b(?:past|last)\s+week\b/i)[0]
33010
+ };
33011
+ }
33012
+ const isoMatch = normalized.match(
33013
+ /\b(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?)?)\b/
33014
+ );
33015
+ if (isoMatch) {
33016
+ const tok = isoMatch[1];
33017
+ if (tok !== void 0) {
33018
+ const parsed = parseInstant(tok, now);
33019
+ if (parsed) {
33020
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(tok);
33021
+ if (isDateOnly) {
33022
+ return {
33023
+ range: {
33024
+ start: parsed,
33025
+ end: new Date(parsed.getTime() + MS_PER_DAY - 1)
33026
+ },
33027
+ matchedSubstring: tok
33028
+ };
33029
+ }
33030
+ return {
33031
+ range: {
33032
+ start: new Date(parsed.getTime() - 30 * 60 * 1e3),
33033
+ end: new Date(parsed.getTime() + 30 * 60 * 1e3)
33034
+ },
33035
+ matchedSubstring: tok
33036
+ };
33037
+ }
33038
+ }
33039
+ }
33040
+ return null;
33041
+ }
33042
+ function parseInstant(token, now) {
33043
+ const trimmed = token.trim().replace(/[,.!?;]+$/g, "");
33044
+ if (!trimmed) return null;
33045
+ const lower = trimmed.toLowerCase();
33046
+ if (lower === "now") return now;
33047
+ if (lower === "today") return startOfDay(now);
33048
+ if (lower === "yesterday") {
33049
+ return new Date(startOfDay(now).getTime() - MS_PER_DAY);
33050
+ }
33051
+ const isoLike = trimmed.replace(" ", "T");
33052
+ const parsed = new Date(isoLike);
33053
+ if (!Number.isNaN(parsed.getTime())) return parsed;
33054
+ return null;
33055
+ }
33056
+ function parseCount(token) {
33057
+ const lower = token.toLowerCase();
33058
+ if (/^\d+$/.test(lower)) {
33059
+ const n = Number.parseInt(lower, 10);
33060
+ return Number.isFinite(n) ? n : null;
33061
+ }
33062
+ return NUMBER_WORDS[lower] ?? null;
33063
+ }
33064
+ function startOfDay(d) {
33065
+ const out = new Date(d);
33066
+ out.setHours(0, 0, 0, 0);
33067
+ return out;
33068
+ }
33069
+ function startOfWeek(d) {
33070
+ const out = startOfDay(d);
33071
+ const dayOfWeek = out.getDay();
33072
+ const offsetToMonday = (dayOfWeek + 6) % 7;
33073
+ out.setDate(out.getDate() - offsetToMonday);
33074
+ return out;
33075
+ }
33076
+ function listFromRegistry(registry) {
33077
+ if (!registry) return [];
33078
+ if (Array.isArray(registry)) return registry;
33079
+ if (typeof registry.list === "function") {
33080
+ return registry.list();
33081
+ }
33082
+ return [];
33083
+ }
33084
+ function extractAgentNames(query, registry) {
33085
+ const records = listFromRegistry(registry);
33086
+ if (records.length === 0) return { matched: [], flagged: false };
33087
+ const lowerQuery = query.toLowerCase();
33088
+ const compactQuery = lowerQuery.replace(/[\s_-]+/g, "");
33089
+ const matched = [];
33090
+ const seen = /* @__PURE__ */ new Set();
33091
+ for (const rec of records) {
33092
+ const id = rec.agent_id;
33093
+ if (!id || seen.has(id)) continue;
33094
+ const idLower = id.toLowerCase();
33095
+ if (idLower.length < 3) continue;
33096
+ const idCompact = idLower.replace(/[\s_-]+/g, "");
33097
+ const wordRe = new RegExp(
33098
+ `\\b${idLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
33099
+ "i"
33100
+ );
33101
+ if (wordRe.test(query)) {
33102
+ matched.push(id);
33103
+ seen.add(id);
33104
+ continue;
33105
+ }
33106
+ if (idCompact.length >= 4 && compactQuery.includes(idCompact)) {
33107
+ matched.push(id);
33108
+ seen.add(id);
33109
+ }
33110
+ }
33111
+ const agentMention = lowerQuery.match(/\bagent\s+([a-z0-9_-]{3,40})/i);
33112
+ const flagged = matched.length === 0 && agentMention !== null && agentMention[1] !== void 0 && !records.some((r) => r.agent_id.toLowerCase() === agentMention[1]?.toLowerCase());
33113
+ return { matched, flagged };
33114
+ }
33115
+ function escapeRegex(s) {
33116
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33117
+ }
33118
+ function extractEventTypes(query, enumValues) {
33119
+ const lower = query.toLowerCase();
33120
+ const matched = [];
33121
+ const seen = /* @__PURE__ */ new Set();
33122
+ for (const ev of enumValues) {
33123
+ if (seen.has(ev)) continue;
33124
+ const re = new RegExp(`\\b${escapeRegex(ev)}\\b`, "i");
33125
+ if (re.test(query)) {
33126
+ matched.push(ev);
33127
+ seen.add(ev);
33128
+ }
33129
+ }
33130
+ for (const syn of EVENT_SYNONYMS) {
33131
+ const re = new RegExp(
33132
+ `\\b${syn.phrase.split(/\s+/).map(escapeRegex).join("\\s+")}\\b`,
33133
+ "i"
33134
+ );
33135
+ if (re.test(query)) {
33136
+ for (const c of syn.canonical) {
33137
+ if (seen.has(c)) continue;
33138
+ if (!enumValues.includes(c)) continue;
33139
+ matched.push(c);
33140
+ seen.add(c);
33141
+ }
33142
+ }
33143
+ }
33144
+ const globMatches = lower.match(/\b([a-z_]+)_\*/g) ?? [];
33145
+ for (const glob of globMatches) {
33146
+ const prefix = glob.slice(0, -2);
33147
+ for (const ev of enumValues) {
33148
+ if (seen.has(ev)) continue;
33149
+ if (ev.startsWith(prefix)) {
33150
+ matched.push(ev);
33151
+ seen.add(ev);
33152
+ }
33153
+ }
33154
+ }
33155
+ const eventNounMention = /\b(?:event|events|class|classes)\b/i.test(query) && matched.length === 0;
33156
+ return { matched, flagged: eventNounMention };
33157
+ }
33158
+ function deriveIntentPhrase(query, stripTokens) {
33159
+ let out = query;
33160
+ for (const tok of stripTokens) {
33161
+ if (!tok) continue;
33162
+ const re = new RegExp(escapeRegex(tok), "gi");
33163
+ out = out.replace(re, " ");
33164
+ }
33165
+ return out.replace(/\s+/g, " ").trim();
33166
+ }
33167
+ function computeConfidence(parsed) {
33168
+ const dims = [
33169
+ { present: parsed.hasTimeMention, resolved: parsed.timeResolved },
33170
+ { present: parsed.hasAgentMention, resolved: parsed.agentResolved },
33171
+ { present: parsed.hasEventMention, resolved: parsed.eventResolved }
33172
+ ];
33173
+ const present = dims.filter((d) => d.present);
33174
+ let base;
33175
+ if (present.length === 0) {
33176
+ base = parsed.intentEmpty ? 0 : 0.3;
33177
+ } else {
33178
+ const resolved = present.filter((d) => d.resolved).length;
33179
+ base = resolved / present.length;
33180
+ }
33181
+ const adjusted = base - 0.15 * parsed.ambiguityCount;
33182
+ if (adjusted < 0) return 0;
33183
+ if (adjusted > 1) return 1;
33184
+ return adjusted;
33185
+ }
33186
+ 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;
33187
+ var AGENT_MENTION_PROBE = /\bagent[s]?\b/i;
33188
+ var EVENT_MENTION_PROBE = /\b(event|events|class|classes|approvals?|policy)\b/i;
33189
+ function parseQuery(query, opts) {
33190
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
33191
+ const enumValues = opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES;
33192
+ const original = query ?? "";
33193
+ const trimmed = original.trim();
33194
+ if (trimmed.length === 0) {
33195
+ return {
33196
+ time_range: null,
33197
+ agent_names: [],
33198
+ event_types: [],
33199
+ intent_phrase: "",
33200
+ ambiguity_flags: ["no_signal_extracted"],
33201
+ parse_confidence: 0
33202
+ };
33203
+ }
33204
+ const ambiguity_flags = /* @__PURE__ */ new Set();
33205
+ const timeMatch = resolveTimeRange(trimmed, now);
33206
+ const hasTimeMention = TIME_MENTION_PROBE.test(trimmed);
33207
+ if (hasTimeMention && !timeMatch) {
33208
+ ambiguity_flags.add("unknown_time_token");
33209
+ }
33210
+ const agentResult = extractAgentNames(trimmed, opts?.registry);
33211
+ if (agentResult.flagged) {
33212
+ ambiguity_flags.add("unknown_agent_token");
33213
+ }
33214
+ const hasAgentMention = AGENT_MENTION_PROBE.test(trimmed);
33215
+ const eventResult = extractEventTypes(trimmed, enumValues);
33216
+ const hasEventMention = EVENT_MENTION_PROBE.test(trimmed);
33217
+ if (eventResult.flagged) {
33218
+ ambiguity_flags.add("unknown_event_token");
33219
+ }
33220
+ const stripTokens = [];
33221
+ if (timeMatch) stripTokens.push(timeMatch.matchedSubstring);
33222
+ for (const name of agentResult.matched) stripTokens.push(name);
33223
+ for (const ev of eventResult.matched) {
33224
+ if (trimmed.toLowerCase().includes(ev.toLowerCase())) {
33225
+ stripTokens.push(ev);
33226
+ }
33227
+ }
33228
+ const intent_phrase = deriveIntentPhrase(trimmed, stripTokens);
33229
+ const parse_confidence = computeConfidence({
33230
+ hasTimeMention,
33231
+ timeResolved: timeMatch !== null,
33232
+ hasAgentMention,
33233
+ agentResolved: agentResult.matched.length > 0,
33234
+ hasEventMention,
33235
+ eventResolved: eventResult.matched.length > 0,
33236
+ intentEmpty: intent_phrase.length === 0,
33237
+ ambiguityCount: ambiguity_flags.size
33238
+ });
33239
+ if (timeMatch === null && agentResult.matched.length === 0 && eventResult.matched.length === 0 && intent_phrase.length === 0) {
33240
+ ambiguity_flags.add("no_signal_extracted");
33241
+ }
33242
+ return {
33243
+ time_range: timeMatch ? timeMatch.range : null,
33244
+ agent_names: agentResult.matched,
33245
+ event_types: eventResult.matched,
33246
+ intent_phrase,
33247
+ ambiguity_flags: Array.from(ambiguity_flags),
33248
+ parse_confidence
33249
+ };
33250
+ }
33251
+ var LLM_ASSIST_THRESHOLD = 0.5;
33252
+ function isLowConfidence(parsed) {
33253
+ return parsed.parse_confidence < LLM_ASSIST_THRESHOLD;
33254
+ }
33255
+ async function parseQueryWithLlmAssist(query, llmAssist, opts) {
33256
+ const parsed = parseQuery(query, opts);
33257
+ if (!llmAssist || !isLowConfidence(parsed)) return parsed;
33258
+ let completion;
33259
+ try {
33260
+ completion = await llmAssist(query, parsed);
33261
+ } catch {
33262
+ return parsed;
33263
+ }
33264
+ if (!completion || typeof completion !== "object") return parsed;
33265
+ const merged = { ...parsed };
33266
+ if (parsed.time_range === null && completion.time_range) {
33267
+ merged.time_range = completion.time_range;
33268
+ }
33269
+ if (parsed.agent_names.length === 0 && Array.isArray(completion.agent_names)) {
33270
+ merged.agent_names = completion.agent_names.filter(
33271
+ (s) => typeof s === "string" && s.length > 0
33272
+ );
33273
+ }
33274
+ if (parsed.event_types.length === 0 && Array.isArray(completion.event_types)) {
33275
+ const allowed = new Set(opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES);
33276
+ merged.event_types = completion.event_types.filter(
33277
+ (s) => typeof s === "string" && allowed.has(s)
33278
+ );
33279
+ }
33280
+ merged.parse_confidence = Math.max(
33281
+ parsed.parse_confidence,
33282
+ computeConfidence({
33283
+ hasTimeMention: TIME_MENTION_PROBE.test(query),
33284
+ timeResolved: merged.time_range !== null,
33285
+ hasAgentMention: AGENT_MENTION_PROBE.test(query),
33286
+ agentResolved: merged.agent_names.length > 0,
33287
+ hasEventMention: EVENT_MENTION_PROBE.test(query),
33288
+ eventResolved: merged.event_types.length > 0,
33289
+ intentEmpty: merged.intent_phrase.length === 0,
33290
+ ambiguityCount: merged.ambiguity_flags.length
33291
+ })
33292
+ );
33293
+ return merged;
33294
+ }
33295
+ function auditSafeSummary(parsed) {
33296
+ return {
33297
+ time_range: parsed.time_range ? {
33298
+ start_iso: parsed.time_range.start.toISOString(),
33299
+ end_iso: parsed.time_range.end.toISOString(),
33300
+ ...parsed.time_range.relative_label !== void 0 ? { relative_label: parsed.time_range.relative_label } : {}
33301
+ } : null,
33302
+ agent_names: [...parsed.agent_names],
33303
+ event_types: [...parsed.event_types],
33304
+ ambiguity_flags: [...parsed.ambiguity_flags],
33305
+ parse_confidence: parsed.parse_confidence
33306
+ };
33307
+ }
33308
+
31996
33309
  // src/chat/operator-chat-service.ts
31997
33310
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
31998
33311
  var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
31999
33312
  var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
32000
33313
  var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
32001
33314
  var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32002
- function approxTokenLen(text) {
33315
+ var DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
33316
+ function approxTokenLen2(text) {
32003
33317
  return Math.ceil(text.length / 4);
32004
33318
  }
32005
33319
  var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
@@ -32043,6 +33357,11 @@ var OperatorChatService = class {
32043
33357
  historyTokenBudget;
32044
33358
  sessionTtlMs;
32045
33359
  clock;
33360
+ contextFetchers;
33361
+ contextLlmAssist;
33362
+ dynamicContextBudget;
33363
+ agentRegistry;
33364
+ grammarLlmAssist;
32046
33365
  /**
32047
33366
  * In-memory thread_id assigned to the active concierge session.
32048
33367
  * The first sendConcierge call after construction allocates a fresh
@@ -32074,6 +33393,19 @@ var OperatorChatService = class {
32074
33393
  this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
32075
33394
  this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
32076
33395
  this.clock = deps.conciergeClock ?? (() => Date.now());
33396
+ if (deps.conciergeContextFetchers) {
33397
+ this.contextFetchers = deps.conciergeContextFetchers;
33398
+ }
33399
+ if (deps.conciergeContextLlmAssist) {
33400
+ this.contextLlmAssist = deps.conciergeContextLlmAssist;
33401
+ }
33402
+ this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
33403
+ if (deps.conciergeAgentRegistry) {
33404
+ this.agentRegistry = deps.conciergeAgentRegistry;
33405
+ }
33406
+ if (deps.conciergeGrammarLlmAssist) {
33407
+ this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
33408
+ }
32077
33409
  }
32078
33410
  // ── Concierge ─────────────────────────────────────────────────────────
32079
33411
  /**
@@ -32132,11 +33464,13 @@ var OperatorChatService = class {
32132
33464
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
32133
33465
  });
32134
33466
  }
33467
+ const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
32135
33468
  const start = Date.now();
32136
33469
  let conciergeBody;
32137
33470
  let servedBy = "disabled";
32138
33471
  let displayLabel = "Concierge: substrate not configured";
32139
33472
  let outcome = "substrate_disabled";
33473
+ let dynamicCategoriesIncluded = [];
32140
33474
  if (!this.substrateSelector) {
32141
33475
  conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
32142
33476
  } else {
@@ -32148,7 +33482,15 @@ var OperatorChatService = class {
32148
33482
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
32149
33483
  outcome = "substrate_disabled";
32150
33484
  } else {
32151
- const context = await this.assembleConciergeContext(priorTurns);
33485
+ const dynamicResult = await this.runDynamicContextFold(
33486
+ filterResult.filtered,
33487
+ parsedGrammar
33488
+ );
33489
+ dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
33490
+ const context = await this.assembleConciergeContext(
33491
+ priorTurns,
33492
+ dynamicResult.section
33493
+ );
32152
33494
  const response = await this.substrateSelector.invokeSummarize(
32153
33495
  "concierge",
32154
33496
  {
@@ -32212,7 +33554,9 @@ var OperatorChatService = class {
32212
33554
  ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
32213
33555
  ...this.memory ? {
32214
33556
  prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32215
- } : {}
33557
+ } : {},
33558
+ ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
33559
+ parsed_grammar: auditSafeSummary(parsedGrammar)
32216
33560
  };
32217
33561
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
32218
33562
  return {
@@ -32363,10 +33707,13 @@ var OperatorChatService = class {
32363
33707
  * ## Sanctuary reference
32364
33708
  * <static domain reference block>
32365
33709
  *
33710
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
33711
+ * ### <Category>
33712
+ * <fetcher payload>
33713
+ *
32366
33714
  * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
32367
33715
  * OPERATOR: ...
32368
33716
  * CONCIERGE: ...
32369
- * ---
32370
33717
  *
32371
33718
  * ## Recent activity
32372
33719
  * <recentActivity output>
@@ -32385,13 +33732,14 @@ var OperatorChatService = class {
32385
33732
  * if available; the v1.2 selector does not expose one, so structured
32386
33733
  * serialization is the canonical path for v1.3.
32387
33734
  */
32388
- async assembleConciergeContext(priorTurns = []) {
33735
+ async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
32389
33736
  const ref = `## Sanctuary reference
32390
33737
  ${SANCTUARY_DOMAIN_REFERENCE}`;
32391
33738
  const priorSection = this.formatPriorTurnsSection(priorTurns);
32392
33739
  if (!this.contextProviders) {
32393
33740
  return [
32394
33741
  ref,
33742
+ ...dynamicSection ? [dynamicSection] : [],
32395
33743
  ...priorSection ? [priorSection] : [],
32396
33744
  "## Recent activity\n(no providers wired)",
32397
33745
  "## Wrapped agents\n(no providers wired)",
@@ -32405,6 +33753,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
32405
33753
  ]);
32406
33754
  return [
32407
33755
  ref,
33756
+ ...dynamicSection ? [dynamicSection] : [],
32408
33757
  ...priorSection ? [priorSection] : [],
32409
33758
  `## Recent activity
32410
33759
  ${activity}`,
@@ -32414,6 +33763,69 @@ ${agents}`,
32414
33763
  ${inbox}`
32415
33764
  ].join("\n\n");
32416
33765
  }
33766
+ /**
33767
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
33768
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
33769
+ * an empty fold, fetcher failures emit a per-category audit event
33770
+ * and are omitted from the rendered section, an LLM-assist failure
33771
+ * proceeds with no fold. Returns the rendered section + the list of
33772
+ * categories whose data made it into the section (used for the
33773
+ * round-trip audit emission).
33774
+ *
33775
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
33776
+ * `parsed` opt to `foldContext`, so fetchers see the structured
33777
+ * `FetcherHints` derived from it.
33778
+ */
33779
+ async runDynamicContextFold(query, parsedGrammar) {
33780
+ if (!this.contextFetchers) {
33781
+ return { section: "", categoriesIncluded: [] };
33782
+ }
33783
+ const result = await foldContext(query, this.contextFetchers, {
33784
+ maxTokens: this.dynamicContextBudget,
33785
+ ...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
33786
+ onFetcherFailure: (category, error) => {
33787
+ this.emitContextFetcherFailed(category, classifyFetcherError(error));
33788
+ },
33789
+ parsed: parsedGrammar
33790
+ });
33791
+ return result;
33792
+ }
33793
+ /**
33794
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
33795
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
33796
+ * configured and the rule-based parse is below
33797
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
33798
+ * throws) so the audit emission can carry the result unconditionally.
33799
+ */
33800
+ async runGrammarParse(query) {
33801
+ return parseQueryWithLlmAssist(query, this.grammarLlmAssist, {
33802
+ ...this.agentRegistry !== void 0 ? { registry: this.agentRegistry } : {},
33803
+ eventClassEnum: CANONICAL_AUDIT_EVENT_CLASSES
33804
+ });
33805
+ }
33806
+ /**
33807
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
33808
+ * of the fold path so the dynamic-context handler stays readable.
33809
+ * Emits with `result: "failure"` since the named category dropped
33810
+ * from the rendered section for this round-trip.
33811
+ */
33812
+ emitContextFetcherFailed(category, failureReason) {
33813
+ const payload = {
33814
+ version: "1.2",
33815
+ event_id: makeEventId("conc-ctxfail"),
33816
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33817
+ identity_id: this.identityId,
33818
+ kind: "operator_concierge_context_fetcher_failed",
33819
+ surface: "concierge",
33820
+ category,
33821
+ failure_reason: failureReason
33822
+ };
33823
+ this.emit(
33824
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
33825
+ payload,
33826
+ "failure"
33827
+ );
33828
+ }
32417
33829
  /**
32418
33830
  * Render the prior-conversation section with token-budget enforcement
32419
33831
  * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
@@ -32424,14 +33836,14 @@ ${inbox}`
32424
33836
  if (turns.length === 0) return "";
32425
33837
  const HEADER = "## Prior conversation";
32426
33838
  const lines = turns.map(formatPriorTurnLine);
32427
- const headerTokens = approxTokenLen(`${HEADER}
33839
+ const headerTokens = approxTokenLen2(`${HEADER}
32428
33840
  `);
32429
- const sepTokens = approxTokenLen("\n");
33841
+ const sepTokens = approxTokenLen2("\n");
32430
33842
  let runningTokens = headerTokens;
32431
33843
  let runningLines = [];
32432
33844
  for (let i = lines.length - 1; i >= 0; i--) {
32433
33845
  const line = lines[i];
32434
- const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
33846
+ const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
32435
33847
  if (runningTokens + tokens > this.historyTokenBudget) break;
32436
33848
  runningTokens += tokens;
32437
33849
  runningLines.push(line);
@@ -32455,6 +33867,17 @@ ${runningLines.join("\n")}`;
32455
33867
  function makeEventId(prefix) {
32456
33868
  return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
32457
33869
  }
33870
+ function classifyFetcherError(error) {
33871
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
33872
+ if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
33873
+ if (msg.includes("schema") || msg.includes("invalid shape")) {
33874
+ return "schema_mismatch";
33875
+ }
33876
+ if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
33877
+ return "io_failed";
33878
+ }
33879
+ return "unknown";
33880
+ }
32458
33881
  function formatPriorTurnLine(turn) {
32459
33882
  const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
32460
33883
  return `${label}: ${turn.content}`;
@@ -32467,7 +33890,7 @@ function hashOf(input) {
32467
33890
  init_encryption();
32468
33891
  init_encoding();
32469
33892
  var OPERATOR_CHAT_NAMESPACE = "_chat";
32470
- var HKDF_INFO = "operator-chat-store-v1";
33893
+ var HKDF_INFO2 = "operator-chat-store-v1";
32471
33894
  function chatStorageKey(surface, threadKey) {
32472
33895
  return `${surface}.${threadKey}`;
32473
33896
  }
@@ -32476,7 +33899,7 @@ var OperatorChatStore = class {
32476
33899
  encryptionKey;
32477
33900
  constructor(storage, masterKey) {
32478
33901
  this.storage = storage;
32479
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO);
33902
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
32480
33903
  }
32481
33904
  /**
32482
33905
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -32560,9 +33983,9 @@ init_encryption();
32560
33983
  init_encoding();
32561
33984
  var CONCIERGE_MEMORY_NAMESPACE = "_chat";
32562
33985
  var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
32563
- var HKDF_INFO2 = "concierge-memory-store-v1";
33986
+ var HKDF_INFO3 = "concierge-memory-store-v1";
32564
33987
  var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
32565
- var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
33988
+ var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
32566
33989
  var ConciergeMemoryStore = class {
32567
33990
  storage;
32568
33991
  encryptionKey;
@@ -32571,7 +33994,7 @@ var ConciergeMemoryStore = class {
32571
33994
  locks;
32572
33995
  constructor(opts) {
32573
33996
  this.storage = opts.storage;
32574
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
33997
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
32575
33998
  this.fortressId = opts.fortressId;
32576
33999
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
32577
34000
  this.locks = /* @__PURE__ */ new Map();
@@ -32650,7 +34073,7 @@ var ConciergeMemoryStore = class {
32650
34073
  return { ok: false, reason: "io_failed" };
32651
34074
  }
32652
34075
  if (!raw) return { ok: true, turns: [] };
32653
- if (raw.length > MAX_BUNDLE_BYTES2) {
34076
+ if (raw.length > MAX_BUNDLE_BYTES3) {
32654
34077
  return { ok: false, reason: "oversize_bundle" };
32655
34078
  }
32656
34079
  let envelope;
@@ -32697,7 +34120,7 @@ var ConciergeMemoryStore = class {
32697
34120
  );
32698
34121
  const summaries = [];
32699
34122
  for (const meta of entries) {
32700
- const threadId = stripKeyPrefix(meta.key);
34123
+ const threadId = stripKeyPrefix2(meta.key);
32701
34124
  if (threadId === null) continue;
32702
34125
  const bundle = await this.loadBundle(threadId);
32703
34126
  if (!bundle || bundle.turns.length === 0) continue;
@@ -32750,7 +34173,7 @@ var ConciergeMemoryStore = class {
32750
34173
  );
32751
34174
  let pruned = 0;
32752
34175
  for (const meta of entries) {
32753
- const threadId = stripKeyPrefix(meta.key);
34176
+ const threadId = stripKeyPrefix2(meta.key);
32754
34177
  if (threadId === null) continue;
32755
34178
  pruned += await this.withLock(threadId, async () => {
32756
34179
  const bundle = await this.loadBundle(threadId);
@@ -32781,7 +34204,7 @@ var ConciergeMemoryStore = class {
32781
34204
  return null;
32782
34205
  }
32783
34206
  if (!raw) return null;
32784
- if (raw.length > MAX_BUNDLE_BYTES2) return null;
34207
+ if (raw.length > MAX_BUNDLE_BYTES3) return null;
32785
34208
  try {
32786
34209
  const envelope = JSON.parse(bytesToString(raw));
32787
34210
  const aad = stringToBytes(threadId);
@@ -32834,7 +34257,7 @@ var ConciergeMemoryStore = class {
32834
34257
  function bundleKey(threadId) {
32835
34258
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
32836
34259
  }
32837
- function stripKeyPrefix(key) {
34260
+ function stripKeyPrefix2(key) {
32838
34261
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
32839
34262
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
32840
34263
  }
@@ -32903,7 +34326,18 @@ function buildV11Bindings(inputs) {
32903
34326
  registry
32904
34327
  }),
32905
34328
  conciergePiiFilter: buildConciergePiiFilter(),
32906
- conciergeMemory
34329
+ conciergeMemory,
34330
+ conciergeContextFetchers: buildConciergeContextFetchers({
34331
+ auditLog: inputs.auditLog,
34332
+ identityId: inputs.identityId,
34333
+ registry
34334
+ }),
34335
+ ...inputs.intelligenceSelector ? {
34336
+ conciergeContextLlmAssist: buildConciergeContextLlmAssist({
34337
+ selector: inputs.intelligenceSelector,
34338
+ identityId: inputs.identityId
34339
+ })
34340
+ } : {}
32907
34341
  });
32908
34342
  }
32909
34343
  const hubService = new HubService({
@@ -32964,6 +34398,107 @@ function buildConciergeContextProviders(args) {
32964
34398
  }
32965
34399
  };
32966
34400
  }
34401
+ function buildConciergeContextFetchers(args) {
34402
+ const empty = async () => "";
34403
+ return {
34404
+ templates: async () => {
34405
+ const entries = listTemplates();
34406
+ if (entries.length === 0) return "(no templates installed)";
34407
+ const lines = entries.map((e) => {
34408
+ const m = e.metadata;
34409
+ return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
34410
+ });
34411
+ return lines.join("\n");
34412
+ },
34413
+ agent_state: async (agentNameHint) => {
34414
+ const records = args.registry.list({ identity_id: args.identityId });
34415
+ if (records.length === 0) return "(no wrapped agents)";
34416
+ const filtered = agentNameHint ? records.filter(
34417
+ (r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
34418
+ ) : records;
34419
+ const target = filtered.length > 0 ? filtered : records;
34420
+ const lines = target.slice(0, 20).map((r) => {
34421
+ const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
34422
+ return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
34423
+ });
34424
+ return lines.join("\n");
34425
+ },
34426
+ agent_activity: async (agentNameHint) => {
34427
+ const result = await args.auditLog.query({ limit: 50 });
34428
+ const owned = result.entries.filter(
34429
+ (e) => e.identity_id === args.identityId
34430
+ );
34431
+ const filtered = agentNameHint ? owned.filter((e) => {
34432
+ const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
34433
+ return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
34434
+ }) : owned;
34435
+ const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
34436
+ if (tail.length === 0) return "(no activity)";
34437
+ return tail.map((e) => {
34438
+ const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
34439
+ return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
34440
+ }).join("\n");
34441
+ },
34442
+ audit_log: async () => {
34443
+ const result = await args.auditLog.query({ limit: 30 });
34444
+ const owned = result.entries.filter(
34445
+ (e) => e.identity_id === args.identityId
34446
+ );
34447
+ if (owned.length === 0) return "(no audit log entries)";
34448
+ return owned.slice(-30).map(
34449
+ (e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
34450
+ ).join("\n");
34451
+ },
34452
+ sentinel_findings: empty,
34453
+ anomaly_alerts: empty,
34454
+ recent_receipts: async () => {
34455
+ const result = await args.auditLog.query({ limit: 100 });
34456
+ const owned = result.entries.filter(
34457
+ (e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
34458
+ );
34459
+ if (owned.length === 0) return "(no recent composition events)";
34460
+ return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
34461
+ },
34462
+ verascore_deltas: empty
34463
+ };
34464
+ }
34465
+ function buildConciergeContextLlmAssist(args) {
34466
+ return async (query, categories) => {
34467
+ const labelList = categories.map((c) => `- ${c}`).join("\n");
34468
+ const prompt = `You are a router. Classify the operator's query into one of the categories below or "none".
34469
+ Reply with exactly one token: one category name or "none".
34470
+
34471
+ Categories:
34472
+ ${labelList}
34473
+
34474
+ Query: ${query}
34475
+
34476
+ Category:`;
34477
+ try {
34478
+ const handle = await args.selector.getSubstrate("concierge");
34479
+ if (!handle.capability.summarize) return "none";
34480
+ const response = await args.selector.invokeSummarize("concierge", {
34481
+ kind: "summarize",
34482
+ context: prompt,
34483
+ query: "Output the single category token.",
34484
+ maxTokens: 16
34485
+ });
34486
+ if (response.failureClass || response.body.kind !== "summarize") {
34487
+ return "none";
34488
+ }
34489
+ const raw = response.body.text.trim().toLowerCase();
34490
+ const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
34491
+ const normalized = head.replace(/[^a-z_]/g, "");
34492
+ const known = categories;
34493
+ if (known.includes(normalized)) {
34494
+ return normalized;
34495
+ }
34496
+ return "none";
34497
+ } catch {
34498
+ return "none";
34499
+ }
34500
+ };
34501
+ }
32967
34502
  function buildConciergePiiFilter() {
32968
34503
  return {
32969
34504
  filter(input) {
@@ -33095,13 +34630,13 @@ init_encryption();
33095
34630
  init_encoding();
33096
34631
  var INTELLIGENCE_NAMESPACE = "_intelligence";
33097
34632
  var SUBSTRATE_CONFIG_KEY = "substrate-config";
33098
- var HKDF_INFO3 = "intelligence-substrate-config";
34633
+ var HKDF_INFO4 = "intelligence-substrate-config";
33099
34634
  var IntelligenceConfigStore = class {
33100
34635
  storage;
33101
34636
  encryptionKey;
33102
34637
  constructor(storage, masterKey) {
33103
34638
  this.storage = storage;
33104
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
34639
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
33105
34640
  }
33106
34641
  /**
33107
34642
  * Load the operator's substrate config from disk. Returns the config
@@ -37047,12 +38582,18 @@ ${err.message}
37047
38582
  } : void 0;
37048
38583
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37049
38584
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
38585
+ const aggregatorPayloadStore = new AggregatorPayloadStore({
38586
+ storage,
38587
+ masterKey,
38588
+ fortressId: fortressIdForAggregator
38589
+ });
37050
38590
  const approvalAggregator = new ApprovalAggregator({
37051
38591
  storage,
37052
38592
  masterKey,
37053
38593
  auditLog,
37054
38594
  identityId: aggregatorIdentityId,
37055
- fortressId: fortressIdForAggregator
38595
+ fortressId: fortressIdForAggregator,
38596
+ payloadStore: aggregatorPayloadStore
37056
38597
  });
37057
38598
  const wrappedApprovalChannel = new AggregatorBackedChannel({
37058
38599
  underlying: approvalChannel,