@sanctuary-framework/mcp-server 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -4457,9 +4457,35 @@ function validatePolicy(raw) {
4457
4457
  };
4458
4458
  delete merged.auto_deny;
4459
4459
  return merged;
4460
- })()
4460
+ })(),
4461
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4461
4462
  };
4462
4463
  }
4464
+ function parseApprovalRedirect(raw) {
4465
+ if (raw === void 0 || raw === null) {
4466
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4467
+ }
4468
+ if (typeof raw !== "object") {
4469
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4470
+ }
4471
+ const obj = raw;
4472
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4473
+ const modeRaw = obj.mode;
4474
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4475
+ if (modeRaw !== void 0) {
4476
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4477
+ throw new Error(
4478
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4479
+ );
4480
+ }
4481
+ mode = modeRaw;
4482
+ }
4483
+ const result = { enabled, mode };
4484
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4485
+ result.per_agent = obj.per_agent;
4486
+ }
4487
+ return result;
4488
+ }
4463
4489
  function generateDefaultPolicyYaml() {
4464
4490
  return `# Sanctuary Principal Policy v1
4465
4491
  # This file controls what your agent can do without asking.
@@ -4538,6 +4564,7 @@ tier3_always_allow:
4538
4564
  - handshake_status
4539
4565
  - handshake_exchange
4540
4566
  - handshake_verify_attestation
4567
+ - handshake_abort
4541
4568
  - reputation_query_weighted
4542
4569
  - federation_peers
4543
4570
  - federation_trust_evaluate
@@ -4572,6 +4599,21 @@ tier3_always_allow:
4572
4599
  approval_channel:
4573
4600
  type: stderr
4574
4601
  timeout_seconds: 300
4602
+
4603
+ # \u2500\u2500\u2500 Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4604
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4605
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4606
+ # of (or in addition to) the configured approval_channel above.
4607
+ #
4608
+ # mode:
4609
+ # replace: bypass the approval_channel entirely; the gate awaits a
4610
+ # decision from the inbox (default once enabled).
4611
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4612
+ # wins. Right shape for harnesses that cannot fully suppress
4613
+ # their local approval prompt (e.g. Mastra-class).
4614
+ approval_redirect:
4615
+ enabled: false
4616
+ mode: replace
4575
4617
  `;
4576
4618
  }
4577
4619
  async function loadPrincipalPolicy(storagePath) {
@@ -4608,7 +4650,7 @@ async function loadPrincipalPolicy(storagePath) {
4608
4650
  );
4609
4651
  }
4610
4652
  }
4611
- var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4653
+ var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4612
4654
  var init_loader = __esm({
4613
4655
  "src/principal-policy/loader.ts"() {
4614
4656
  DEFAULT_TIER2 = {
@@ -4625,6 +4667,10 @@ var init_loader = __esm({
4625
4667
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4626
4668
  // Field omitted intentionally — all channels hardcode deny on timeout.
4627
4669
  };
4670
+ DEFAULT_APPROVAL_REDIRECT = {
4671
+ enabled: false,
4672
+ mode: "replace"
4673
+ };
4628
4674
  DEFAULT_POLICY = {
4629
4675
  version: 1,
4630
4676
  tier1_always_approve: [
@@ -4698,6 +4744,7 @@ var init_loader = __esm({
4698
4744
  "handshake_status",
4699
4745
  "handshake_exchange",
4700
4746
  "handshake_verify_attestation",
4747
+ "handshake_abort",
4701
4748
  "reputation_query_weighted",
4702
4749
  "federation_peers",
4703
4750
  "federation_trust_evaluate",
@@ -4739,7 +4786,8 @@ var init_loader = __esm({
4739
4786
  "compliance_eu_ai_act_annex_iii_classify"
4740
4787
  // Read-only; rule-based Annex III classifier
4741
4788
  ],
4742
- approval_channel: DEFAULT_CHANNEL
4789
+ approval_channel: DEFAULT_CHANNEL,
4790
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4743
4791
  };
4744
4792
  MalformedPrincipalPolicyError = class extends Error {
4745
4793
  constructor(policyPath, reason) {
@@ -17170,6 +17218,27 @@ async function handleApprovalInboxRoute(deps, req, res) {
17170
17218
  await handleStream2(deps, res);
17171
17219
  return true;
17172
17220
  }
17221
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
17222
+ const limit = parseLimit2(
17223
+ url.searchParams.get("limit"),
17224
+ APPROVAL_INBOX_DEFAULT_LIMIT,
17225
+ APPROVAL_INBOX_MAX_LIMIT
17226
+ );
17227
+ const statusRaw = url.searchParams.get("status");
17228
+ const sinceTs = url.searchParams.get("since") ?? void 0;
17229
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
17230
+ const filterStatus = statusRaw && isStatusFilter(statusRaw) && statusRaw !== "pending" ? statusRaw : void 0;
17231
+ const entries = await deps.aggregator.getHistory(
17232
+ {
17233
+ limit,
17234
+ ...filterStatus !== void 0 ? { status: filterStatus } : {},
17235
+ ...sinceTs !== void 0 ? { sinceTs } : {}
17236
+ },
17237
+ operatorId
17238
+ );
17239
+ writeJSON4(res, 200, { ok: true, data: { entries } });
17240
+ return true;
17241
+ }
17173
17242
  if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
17174
17243
  const limit = parseLimit2(
17175
17244
  url.searchParams.get("limit"),
@@ -17192,11 +17261,39 @@ async function handleApprovalInboxRoute(deps, req, res) {
17192
17261
  writeJSON4(res, 404, { ok: false, error: "not_found", path });
17193
17262
  return true;
17194
17263
  }
17195
- if (method === "GET" && entryMatch.action === null) {
17196
- const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
17197
- const entry = entries.find(
17198
- (e) => e.aggregator_id === entryMatch.aggregatorId
17264
+ if (method === "GET" && entryMatch.action === "audit-trail") {
17265
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
17266
+ if (!entry) {
17267
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17268
+ return true;
17269
+ }
17270
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
17271
+ const trail = await deps.aggregator.getAuditTrail(
17272
+ entryMatch.aggregatorId,
17273
+ operatorId
17274
+ );
17275
+ writeJSON4(res, 200, { ok: true, data: { entry, audit_trail: trail } });
17276
+ return true;
17277
+ }
17278
+ if (method === "GET" && entryMatch.action === "payload") {
17279
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
17280
+ if (!entry) {
17281
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17282
+ return true;
17283
+ }
17284
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
17285
+ const payload = await deps.aggregator.getFullPayloadWithAudit(
17286
+ entryMatch.aggregatorId,
17287
+ operatorId
17199
17288
  );
17289
+ writeJSON4(res, 200, {
17290
+ ok: true,
17291
+ data: { entry, request_payload: payload }
17292
+ });
17293
+ return true;
17294
+ }
17295
+ if (method === "GET" && entryMatch.action === null) {
17296
+ const entry = await deps.aggregator.getEntry(entryMatch.aggregatorId);
17200
17297
  if (!entry) {
17201
17298
  writeJSON4(res, 404, { ok: false, error: "not_found" });
17202
17299
  return true;
@@ -20230,7 +20327,10 @@ var init_approval_aggregator = __esm({
20230
20327
  APPROVAL_AGGREGATOR_AUDIT_OPS = {
20231
20328
  AGGREGATED: "cross_harness_approval_aggregated",
20232
20329
  RESOLVED: "cross_harness_approval_resolved",
20233
- DEDUPED: "cross_harness_approval_deduped"
20330
+ DEDUPED: "cross_harness_approval_deduped",
20331
+ PAYLOAD_DECRYPTED: "cross_harness_approval_payload_decrypted",
20332
+ AUDIT_TRAIL_VIEWED: "cross_harness_approval_audit_trail_viewed",
20333
+ REPLAYED: "cross_harness_approval_replayed"
20234
20334
  };
20235
20335
  DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
20236
20336
  DEFAULT_MAX_LIST_LIMIT = 200;
@@ -20246,6 +20346,8 @@ var init_approval_aggregator = __esm({
20246
20346
  now;
20247
20347
  resolveSourceContext;
20248
20348
  resolveHubInboxItemId;
20349
+ payloadStore;
20350
+ resolveEnforcementChain;
20249
20351
  /** Cached entries by `aggregator_id`. */
20250
20352
  entries = /* @__PURE__ */ new Map();
20251
20353
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -20275,6 +20377,14 @@ var init_approval_aggregator = __esm({
20275
20377
  source_agent_id: this.fortressId
20276
20378
  }));
20277
20379
  this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
20380
+ this.payloadStore = deps.payloadStore ?? null;
20381
+ this.resolveEnforcementChain = deps.resolveEnforcementChain ?? ((event) => [
20382
+ {
20383
+ layer: "l2",
20384
+ event: `approval_required:${event.operation}`,
20385
+ timestamp: event.request_timestamp
20386
+ }
20387
+ ]);
20278
20388
  }
20279
20389
  /**
20280
20390
  * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
@@ -20325,13 +20435,152 @@ var init_approval_aggregator = __esm({
20325
20435
  }
20326
20436
  /**
20327
20437
  * Return the original (unhashed) request payload for the entry. Returns
20328
- * `null` when the entry is unknown or the payload was evicted (e.g. the
20329
- * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
20438
+ * `null` when the entry is unknown. When the in-memory payload map has
20439
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
20440
+ * provided, the at-rest bundle is decrypted and the in-memory map is
20441
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
20442
+ * accessor is silent so internal callers can read without polluting the
20443
+ * audit trail.
20330
20444
  */
20331
20445
  async getFullPayload(aggregatorId) {
20332
20446
  await this.hydrate();
20333
20447
  if (!this.entries.has(aggregatorId)) return null;
20334
- return this.fullPayloads.get(aggregatorId) ?? null;
20448
+ const cached = this.fullPayloads.get(aggregatorId);
20449
+ if (cached !== void 0) return cached;
20450
+ if (this.payloadStore) {
20451
+ try {
20452
+ const restored = await this.payloadStore.loadPayload(aggregatorId);
20453
+ if (restored !== null) {
20454
+ this.fullPayloads.set(aggregatorId, restored);
20455
+ return restored;
20456
+ }
20457
+ } catch {
20458
+ }
20459
+ }
20460
+ return null;
20461
+ }
20462
+ /**
20463
+ * Return the entry record for the given id, or null when unknown.
20464
+ * Idempotent. v1.3 Upsilon-3.
20465
+ */
20466
+ async getEntry(aggregatorId) {
20467
+ await this.hydrate();
20468
+ return this.entries.get(aggregatorId) ?? null;
20469
+ }
20470
+ /**
20471
+ * Audited variant of `getFullPayload`. Emits the
20472
+ * `cross_harness_approval_payload_decrypted` audit event before
20473
+ * returning. Used by the operator-facing /payload replay route.
20474
+ * v1.3 Upsilon-3.
20475
+ */
20476
+ async getFullPayloadWithAudit(aggregatorId, operatorId) {
20477
+ const payload = await this.getFullPayload(aggregatorId);
20478
+ if (payload === null) return null;
20479
+ const entry = this.entries.get(aggregatorId);
20480
+ this.auditLog.append(
20481
+ "l2",
20482
+ APPROVAL_AGGREGATOR_AUDIT_OPS.PAYLOAD_DECRYPTED,
20483
+ operatorId,
20484
+ {
20485
+ aggregator_id: aggregatorId,
20486
+ ...entry ? {
20487
+ source_harness: entry.source_harness,
20488
+ source_agent_id: entry.source_agent_id,
20489
+ entry_status: entry.status
20490
+ } : {}
20491
+ }
20492
+ );
20493
+ return payload;
20494
+ }
20495
+ /**
20496
+ * Return the audit-log entries that led to and surround this approval.
20497
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
20498
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
20499
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
20500
+ * aggregator id at v1.3, so they are matched via timestamp window
20501
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
20502
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
20503
+ * v1.3 Upsilon-3.
20504
+ */
20505
+ async getAuditTrail(aggregatorId, operatorId) {
20506
+ await this.hydrate();
20507
+ const entry = this.entries.get(aggregatorId);
20508
+ if (!entry) {
20509
+ return [];
20510
+ }
20511
+ const sinceMs = Date.parse(entry.created_at) - 1e3;
20512
+ const sinceIso = new Date(sinceMs).toISOString();
20513
+ const queried = await this.auditLog.query({ since: sinceIso, limit: 1e3 });
20514
+ const operationPart = entry.policy_rule_id.includes(":") ? entry.policy_rule_id.slice(entry.policy_rule_id.indexOf(":") + 1) : entry.policy_rule_id;
20515
+ const lifetimeStart = sinceMs;
20516
+ const lifetimeEnd = entry.resolved_at ? Date.parse(entry.resolved_at) + 1e3 : Date.parse(entry.expires_at) + 1e3;
20517
+ const matches = [];
20518
+ for (const audit of queried.entries) {
20519
+ const detailsId = audit.details !== void 0 ? audit.details["aggregator_id"] : void 0;
20520
+ if (detailsId === aggregatorId) {
20521
+ matches.push(audit);
20522
+ continue;
20523
+ }
20524
+ const auditMs = Date.parse(audit.timestamp);
20525
+ if (auditMs < lifetimeStart || auditMs > lifetimeEnd) continue;
20526
+ if (audit.operation.endsWith(`:${operationPart}`)) {
20527
+ matches.push(audit);
20528
+ }
20529
+ }
20530
+ matches.sort(
20531
+ (a, b) => a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
20532
+ );
20533
+ this.auditLog.append(
20534
+ "l2",
20535
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AUDIT_TRAIL_VIEWED,
20536
+ operatorId,
20537
+ {
20538
+ aggregator_id: aggregatorId,
20539
+ entry_status: entry.status,
20540
+ match_count: matches.length
20541
+ }
20542
+ );
20543
+ return matches;
20544
+ }
20545
+ /**
20546
+ * List historical (resolved) approvals. Excludes pending entries by
20547
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
20548
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
20549
+ * Upsilon-3.
20550
+ */
20551
+ async getHistory(opts, operatorId) {
20552
+ await this.hydrate();
20553
+ await this.expireStale();
20554
+ const limit = Math.min(
20555
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
20556
+ this.maxListLimit
20557
+ );
20558
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
20559
+ const matching = [];
20560
+ for (const entry of this.entries.values()) {
20561
+ if (entry.status === "pending") continue;
20562
+ if (opts?.status && entry.status !== opts.status) continue;
20563
+ const stamp = Date.parse(entry.resolved_at ?? entry.created_at);
20564
+ if (stamp < sinceMs) continue;
20565
+ matching.push(entry);
20566
+ }
20567
+ matching.sort((a, b) => {
20568
+ const aStamp = a.resolved_at ?? a.created_at;
20569
+ const bStamp = b.resolved_at ?? b.created_at;
20570
+ return bStamp.localeCompare(aStamp);
20571
+ });
20572
+ const sliced = matching.slice(0, limit);
20573
+ this.auditLog.append(
20574
+ "l2",
20575
+ APPROVAL_AGGREGATOR_AUDIT_OPS.REPLAYED,
20576
+ operatorId,
20577
+ {
20578
+ result_count: sliced.length,
20579
+ ...opts?.status !== void 0 ? { status_filter: opts.status } : {},
20580
+ ...opts?.sinceTs !== void 0 ? { since: opts.sinceTs } : {}
20581
+ }
20582
+ );
20583
+ return sliced;
20335
20584
  }
20336
20585
  /**
20337
20586
  * Resolve an entry. Used by both:
@@ -20405,6 +20654,7 @@ var init_approval_aggregator = __esm({
20405
20654
  const now = this.now();
20406
20655
  const expires = new Date(now.getTime() + this.pendingTtlMs);
20407
20656
  const hubInboxId = this.resolveHubInboxItemId(event);
20657
+ const enforcementChain = this.resolveEnforcementChain(event);
20408
20658
  const entry = {
20409
20659
  aggregator_id: id,
20410
20660
  source_harness: ctx.source_harness,
@@ -20416,13 +20666,20 @@ var init_approval_aggregator = __esm({
20416
20666
  status: "pending",
20417
20667
  created_at: now.toISOString(),
20418
20668
  expires_at: expires.toISOString(),
20419
- ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
20669
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
20670
+ ...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
20420
20671
  };
20421
20672
  this.entries.set(id, entry);
20422
20673
  this.dedupIndex.set(dedupKey, id);
20423
20674
  this.correlationIndex.set(event.correlation_id, id);
20424
20675
  this.fullPayloads.set(id, event.context);
20425
20676
  await this.persist(entry);
20677
+ if (this.payloadStore) {
20678
+ try {
20679
+ await this.payloadStore.savePayload(id, event.context);
20680
+ } catch {
20681
+ }
20682
+ }
20426
20683
  this.auditLog.append(
20427
20684
  "l2",
20428
20685
  APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
@@ -20570,6 +20827,317 @@ var init_approval_aggregator = __esm({
20570
20827
  }
20571
20828
  });
20572
20829
 
20830
+ // src/principal-policy/channels/aggregator-backed-channel.ts
20831
+ function auditEntryIdFor(request) {
20832
+ return `${request.timestamp}:${request.operation}`;
20833
+ }
20834
+ function statusToDecision(entry) {
20835
+ switch (entry.status) {
20836
+ case "approved":
20837
+ return {
20838
+ decision: "approve",
20839
+ decided_by: "human"
20840
+ };
20841
+ case "denied":
20842
+ return {
20843
+ decision: "deny",
20844
+ decided_by: "human"
20845
+ };
20846
+ case "timeout":
20847
+ case "expired":
20848
+ return {
20849
+ decision: "deny",
20850
+ decided_by: "timeout"
20851
+ };
20852
+ default:
20853
+ return null;
20854
+ }
20855
+ }
20856
+ function makeRedirectResolverFromPolicySupplier(supplier) {
20857
+ return (_request) => {
20858
+ const cfg = supplier().approval_redirect;
20859
+ if (!cfg || cfg.enabled !== true) {
20860
+ return { enabled: false, mode: "replace" };
20861
+ }
20862
+ return {
20863
+ enabled: true,
20864
+ mode: cfg.mode === "notify" ? "notify" : "replace"
20865
+ };
20866
+ };
20867
+ }
20868
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
20869
+ var init_aggregator_backed_channel = __esm({
20870
+ "src/principal-policy/channels/aggregator-backed-channel.ts"() {
20871
+ DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
20872
+ AggregatorBackedChannel = class {
20873
+ underlying;
20874
+ aggregator;
20875
+ resolveRedirect;
20876
+ replaceModeTimeoutMs;
20877
+ now;
20878
+ constructor(opts) {
20879
+ this.underlying = opts.underlying;
20880
+ this.aggregator = opts.aggregator;
20881
+ this.resolveRedirect = opts.resolveRedirect;
20882
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
20883
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
20884
+ }
20885
+ /** Expose underlying for tests / wire-up reuse. */
20886
+ getUnderlying() {
20887
+ return this.underlying;
20888
+ }
20889
+ async requestApproval(request) {
20890
+ const cfg = this.resolveRedirect(request);
20891
+ if (!cfg.enabled) {
20892
+ return this.underlying.requestApproval(request);
20893
+ }
20894
+ if (cfg.mode === "replace") {
20895
+ return this.awaitAggregatorDecision(request);
20896
+ }
20897
+ return this.notifyMode(request);
20898
+ }
20899
+ /**
20900
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
20901
+ * checking already-stored entries (avoids a race where the entry resolves
20902
+ * between list and subscribe). Match incoming events to this request by
20903
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
20904
+ */
20905
+ async awaitAggregatorDecision(request) {
20906
+ const auditId = auditEntryIdFor(request);
20907
+ return new Promise((resolveOuter) => {
20908
+ let settled = false;
20909
+ let unsubscribe = null;
20910
+ let timeoutHandle = null;
20911
+ const settle = (response) => {
20912
+ if (settled) return;
20913
+ settled = true;
20914
+ if (timeoutHandle) clearTimeout(timeoutHandle);
20915
+ if (unsubscribe) {
20916
+ try {
20917
+ unsubscribe();
20918
+ } catch {
20919
+ }
20920
+ }
20921
+ resolveOuter(response);
20922
+ };
20923
+ const onEvent = (emit) => {
20924
+ if (emit.type !== "resolved") return;
20925
+ if (emit.entry.audit_log_entry_id !== auditId) return;
20926
+ const mapped = statusToDecision(emit.entry);
20927
+ if (!mapped) return;
20928
+ settle({
20929
+ decision: mapped.decision,
20930
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
20931
+ decided_by: mapped.decided_by
20932
+ });
20933
+ };
20934
+ try {
20935
+ unsubscribe = this.aggregator.onEvent(onEvent);
20936
+ } catch (err) {
20937
+ settle({
20938
+ decision: "deny",
20939
+ decided_at: this.now().toISOString(),
20940
+ decided_by: "channel_failure"
20941
+ });
20942
+ throw err instanceof Error ? err : new Error(String(err));
20943
+ }
20944
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
20945
+ for (const entry of entries) {
20946
+ if (entry.audit_log_entry_id !== auditId) continue;
20947
+ const mapped = statusToDecision(entry);
20948
+ if (!mapped) return;
20949
+ settle({
20950
+ decision: mapped.decision,
20951
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
20952
+ decided_by: mapped.decided_by
20953
+ });
20954
+ return;
20955
+ }
20956
+ }).catch(() => {
20957
+ });
20958
+ timeoutHandle = setTimeout(() => {
20959
+ settle({
20960
+ decision: "deny",
20961
+ decided_at: this.now().toISOString(),
20962
+ decided_by: "timeout"
20963
+ });
20964
+ }, this.replaceModeTimeoutMs);
20965
+ });
20966
+ }
20967
+ /**
20968
+ * `notify` mode. Fire the underlying channel and listen on the
20969
+ * aggregator simultaneously; whichever resolves first wins. Both
20970
+ * paths produce identical `ApprovalResponse` shapes; the gate's
20971
+ * downstream audit logging is unchanged.
20972
+ *
20973
+ * On underlying-channel failure, fall through to the aggregator wait
20974
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
20975
+ * resolve from the inbox even if the dashboard/webhook is down.
20976
+ */
20977
+ async notifyMode(request) {
20978
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
20979
+ let underlyingPromise;
20980
+ try {
20981
+ underlyingPromise = this.underlying.requestApproval(request);
20982
+ } catch (err) {
20983
+ const response = await aggregatorPromise;
20984
+ return response;
20985
+ }
20986
+ return Promise.race([
20987
+ aggregatorPromise,
20988
+ underlyingPromise.catch(
20989
+ () => new Promise(() => {
20990
+ })
20991
+ )
20992
+ ]);
20993
+ }
20994
+ };
20995
+ }
20996
+ });
20997
+
20998
+ // src/principal-policy/aggregator-store.ts
20999
+ function payloadKey(aggregatorId) {
21000
+ return `${AGGREGATOR_PAYLOAD_KEY_PREFIX}${aggregatorId}`;
21001
+ }
21002
+ function stripKeyPrefix(key) {
21003
+ if (!key.startsWith(AGGREGATOR_PAYLOAD_KEY_PREFIX)) return null;
21004
+ return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
21005
+ }
21006
+ var AGGREGATOR_PAYLOAD_NAMESPACE, AGGREGATOR_PAYLOAD_KEY_PREFIX, HKDF_INFO, DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS, MAX_BUNDLE_BYTES2, AggregatorPayloadStore;
21007
+ var init_aggregator_store = __esm({
21008
+ "src/principal-policy/aggregator-store.ts"() {
21009
+ init_encryption();
21010
+ init_key_derivation();
21011
+ init_encoding();
21012
+ AGGREGATOR_PAYLOAD_NAMESPACE = "_approval_aggregator_payloads";
21013
+ AGGREGATOR_PAYLOAD_KEY_PREFIX = "payload.";
21014
+ HKDF_INFO = "l2-approval-aggregator-payload-v1";
21015
+ DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS = 30;
21016
+ MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
21017
+ AggregatorPayloadStore = class {
21018
+ storage;
21019
+ encryptionKey;
21020
+ fortressId;
21021
+ retentionDays;
21022
+ constructor(opts) {
21023
+ this.storage = opts.storage;
21024
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO);
21025
+ this.fortressId = opts.fortressId;
21026
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_AGGREGATOR_PAYLOAD_RETENTION_DAYS;
21027
+ }
21028
+ /**
21029
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
21030
+ * twice with the same id rewrites the bundle (retention_until is
21031
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
21032
+ * so callers can log it.
21033
+ */
21034
+ async savePayload(aggregatorId, payload) {
21035
+ const now = /* @__PURE__ */ new Date();
21036
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
21037
+ const retentionUntil = new Date(now.getTime() + retentionMs);
21038
+ const bundle = {
21039
+ version: 1,
21040
+ aggregator_id: aggregatorId,
21041
+ fortress_id: this.fortressId,
21042
+ created_at: now.toISOString(),
21043
+ retention_until: retentionUntil.toISOString(),
21044
+ payload
21045
+ };
21046
+ const aad = stringToBytes(aggregatorId);
21047
+ const plaintext = stringToBytes(JSON.stringify(bundle));
21048
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
21049
+ await this.storage.write(
21050
+ AGGREGATOR_PAYLOAD_NAMESPACE,
21051
+ payloadKey(aggregatorId),
21052
+ stringToBytes(JSON.stringify(envelope))
21053
+ );
21054
+ return bundle.retention_until;
21055
+ }
21056
+ /**
21057
+ * Read the persisted payload for the aggregator_id. Returns null if no
21058
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
21059
+ */
21060
+ async loadPayload(aggregatorId) {
21061
+ const key = payloadKey(aggregatorId);
21062
+ let raw;
21063
+ try {
21064
+ raw = await this.storage.read(AGGREGATOR_PAYLOAD_NAMESPACE, key);
21065
+ } catch {
21066
+ return null;
21067
+ }
21068
+ if (!raw) return null;
21069
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
21070
+ try {
21071
+ const envelope = JSON.parse(bytesToString(raw));
21072
+ const aad = stringToBytes(aggregatorId);
21073
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
21074
+ const parsed = JSON.parse(
21075
+ bytesToString(plaintext)
21076
+ );
21077
+ if (parsed.version !== 1) return null;
21078
+ if (parsed.aggregator_id !== aggregatorId) return null;
21079
+ return parsed.payload;
21080
+ } catch {
21081
+ return null;
21082
+ }
21083
+ }
21084
+ /**
21085
+ * Delete the persisted payload. Returns true when a bundle was removed,
21086
+ * false when none existed.
21087
+ */
21088
+ async deletePayload(aggregatorId) {
21089
+ const key = payloadKey(aggregatorId);
21090
+ const existed = await this.storage.exists(
21091
+ AGGREGATOR_PAYLOAD_NAMESPACE,
21092
+ key
21093
+ );
21094
+ if (!existed) return false;
21095
+ try {
21096
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, key);
21097
+ } catch {
21098
+ return false;
21099
+ }
21100
+ return true;
21101
+ }
21102
+ /**
21103
+ * Drop expired payload bundles. Returns the count of bundles pruned.
21104
+ * Caller wires this into the cocoon-unlock initialization path.
21105
+ */
21106
+ async pruneExpired(now) {
21107
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
21108
+ const entries = await this.storage.list(
21109
+ AGGREGATOR_PAYLOAD_NAMESPACE,
21110
+ AGGREGATOR_PAYLOAD_KEY_PREFIX
21111
+ );
21112
+ let pruned = 0;
21113
+ for (const meta of entries) {
21114
+ const aggregatorId = stripKeyPrefix(meta.key);
21115
+ if (aggregatorId === null) continue;
21116
+ const raw = await this.storage.read(
21117
+ AGGREGATOR_PAYLOAD_NAMESPACE,
21118
+ meta.key
21119
+ );
21120
+ if (!raw) continue;
21121
+ try {
21122
+ const envelope = JSON.parse(bytesToString(raw));
21123
+ const aad = stringToBytes(aggregatorId);
21124
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
21125
+ const parsed = JSON.parse(
21126
+ bytesToString(plaintext)
21127
+ );
21128
+ if (parsed.retention_until <= cutoff) {
21129
+ await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
21130
+ pruned += 1;
21131
+ }
21132
+ } catch {
21133
+ }
21134
+ }
21135
+ return { pruned };
21136
+ }
21137
+ };
21138
+ }
21139
+ });
21140
+
20573
21141
  // src/principal-policy/tools.ts
20574
21142
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
20575
21143
  return [
@@ -21473,6 +22041,76 @@ var init_attestation = __esm({
21473
22041
  }
21474
22042
  });
21475
22043
 
22044
+ // src/handshake/audit.ts
22045
+ function auditHandshakeInitiated(auditLog, ctx) {
22046
+ auditLog.append(
22047
+ "l4",
22048
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
22049
+ ctx.identity_id,
22050
+ detailsFromContext(ctx),
22051
+ "success"
22052
+ );
22053
+ }
22054
+ function auditHandshakeCompleted(auditLog, ctx) {
22055
+ const details = detailsFromContext(ctx);
22056
+ if (ctx.trust_tier !== void 0) {
22057
+ details.trust_tier = ctx.trust_tier;
22058
+ }
22059
+ auditLog.append(
22060
+ "l4",
22061
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
22062
+ ctx.identity_id,
22063
+ details,
22064
+ "success"
22065
+ );
22066
+ }
22067
+ function auditHandshakeFailed(auditLog, ctx) {
22068
+ const details = detailsFromContext(ctx);
22069
+ details.reason = ctx.reason;
22070
+ if (ctx.error !== void 0) {
22071
+ details.error = ctx.error;
22072
+ }
22073
+ auditLog.append(
22074
+ "l4",
22075
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
22076
+ ctx.identity_id,
22077
+ details,
22078
+ "failure"
22079
+ );
22080
+ }
22081
+ function auditHandshakeAborted(auditLog, ctx) {
22082
+ const details = detailsFromContext(ctx);
22083
+ details.reason = ctx.reason;
22084
+ auditLog.append(
22085
+ "l4",
22086
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
22087
+ ctx.identity_id,
22088
+ details,
22089
+ "failure"
22090
+ );
22091
+ }
22092
+ function detailsFromContext(ctx) {
22093
+ const details = {
22094
+ session_id: ctx.session_id,
22095
+ role: ctx.role
22096
+ };
22097
+ if (ctx.counterparty_id !== void 0) {
22098
+ details.counterparty_id = ctx.counterparty_id;
22099
+ }
22100
+ return details;
22101
+ }
22102
+ var HANDSHAKE_LIFECYCLE_OPS;
22103
+ var init_audit = __esm({
22104
+ "src/handshake/audit.ts"() {
22105
+ HANDSHAKE_LIFECYCLE_OPS = {
22106
+ INITIATED: "handshake_initiated",
22107
+ COMPLETED: "handshake_completed",
22108
+ FAILED: "handshake_failed",
22109
+ ABORTED: "handshake_aborted"
22110
+ };
22111
+ }
22112
+ });
22113
+
21476
22114
  // src/handshake/tools.ts
21477
22115
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
21478
22116
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -21506,6 +22144,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21506
22144
  const { challenge, session } = initiateHandshake(shr);
21507
22145
  sessions.set(session.session_id, session);
21508
22146
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
22147
+ auditHandshakeInitiated(auditLog, {
22148
+ session_id: session.session_id,
22149
+ role: "initiator",
22150
+ identity_id: shr.body.instance_id
22151
+ });
21509
22152
  return toolResult({
21510
22153
  session_id: session.session_id,
21511
22154
  challenge,
@@ -21545,10 +22188,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21545
22188
  );
21546
22189
  if ("error" in result) {
21547
22190
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
22191
+ auditHandshakeFailed(auditLog, {
22192
+ session_id: "unknown",
22193
+ role: "responder",
22194
+ identity_id: shr.body.instance_id,
22195
+ reason: classifyRespondFailure(result.error),
22196
+ error: result.error
22197
+ });
21548
22198
  return toolResult({ error: result.error });
21549
22199
  }
21550
22200
  sessions.set(result.session.session_id, result.session);
21551
22201
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
22202
+ auditHandshakeInitiated(auditLog, {
22203
+ session_id: result.session.session_id,
22204
+ role: "responder",
22205
+ identity_id: shr.body.instance_id,
22206
+ counterparty_id: challenge.shr.body.instance_id
22207
+ });
21552
22208
  let autoPublishResult;
21553
22209
  if (autoPublishHandshakes) {
21554
22210
  autoPublishResult = { attempted: true };
@@ -21656,9 +22312,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21656
22312
  const response = args.response;
21657
22313
  const session = sessions.get(sessionId);
21658
22314
  if (!session) {
22315
+ auditHandshakeFailed(auditLog, {
22316
+ session_id: sessionId,
22317
+ role: "initiator",
22318
+ identity_id: "unknown",
22319
+ reason: "session_unknown",
22320
+ error: `No handshake session found: ${sessionId}`
22321
+ });
21659
22322
  return toolResult({ error: `No handshake session found: ${sessionId}` });
21660
22323
  }
21661
22324
  if (session.state !== "initiated") {
22325
+ auditHandshakeFailed(auditLog, {
22326
+ session_id: sessionId,
22327
+ role: "initiator",
22328
+ identity_id: session.our_shr.body.instance_id,
22329
+ reason: "session_state_mismatch",
22330
+ error: `Session is in state '${session.state}', expected 'initiated'`
22331
+ });
21662
22332
  return toolResult({
21663
22333
  error: `Session is in state '${session.state}', expected 'initiated'`
21664
22334
  });
@@ -21672,6 +22342,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21672
22342
  if ("error" in result) {
21673
22343
  session.state = "failed";
21674
22344
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
22345
+ auditHandshakeFailed(auditLog, {
22346
+ session_id: sessionId,
22347
+ role: "initiator",
22348
+ identity_id: session.our_shr.body.instance_id,
22349
+ reason: classifyCompleteFailure(result.error),
22350
+ error: result.error
22351
+ });
21675
22352
  return toolResult({ error: result.error });
21676
22353
  }
21677
22354
  session.state = "completed";
@@ -21680,6 +22357,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21680
22357
  session.result = result.result;
21681
22358
  handshakeResults.set(result.result.counterparty_id, result.result);
21682
22359
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
22360
+ auditHandshakeCompleted(auditLog, {
22361
+ session_id: sessionId,
22362
+ role: "initiator",
22363
+ identity_id: session.our_shr.body.instance_id,
22364
+ counterparty_id: result.result.counterparty_id,
22365
+ trust_tier: result.result.trust_tier
22366
+ });
21683
22367
  return toolResult({
21684
22368
  completion: result.completion,
21685
22369
  result: result.result,
@@ -21727,6 +22411,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21727
22411
  void 0,
21728
22412
  result.verified ? "success" : "failure"
21729
22413
  );
22414
+ if (result.verified) {
22415
+ auditHandshakeCompleted(auditLog, {
22416
+ session_id: session.session_id,
22417
+ role: "responder",
22418
+ identity_id: session.our_shr.body.instance_id,
22419
+ counterparty_id: result.counterparty_id,
22420
+ trust_tier: result.trust_tier
22421
+ });
22422
+ } else {
22423
+ auditHandshakeFailed(auditLog, {
22424
+ session_id: session.session_id,
22425
+ role: "responder",
22426
+ identity_id: session.our_shr.body.instance_id,
22427
+ counterparty_id: result.counterparty_id,
22428
+ reason: classifyCompleteFailure(result.errors.join("; ")),
22429
+ error: result.errors.join("; ")
22430
+ });
22431
+ }
21730
22432
  return toolResult({ result });
21731
22433
  }
21732
22434
  return toolResult({
@@ -21834,10 +22536,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21834
22536
  _content_trust: "external"
21835
22537
  });
21836
22538
  }
22539
+ },
22540
+ {
22541
+ name: "handshake_abort",
22542
+ description: "Abort an in-flight handshake session. Drops the session record and appends a session-lifecycle audit entry (handshake_aborted) so the operator can distinguish operator-cancelled, timed-out, and dropped sessions from sessions that simply fell off the protocol path.",
22543
+ inputSchema: {
22544
+ type: "object",
22545
+ properties: {
22546
+ session_id: {
22547
+ type: "string",
22548
+ description: "Session ID returned from handshake_initiate / handshake_respond."
22549
+ },
22550
+ reason: {
22551
+ type: "string",
22552
+ enum: [
22553
+ "operator_cancelled",
22554
+ "session_timeout",
22555
+ "transport_dropped",
22556
+ "shutdown",
22557
+ "other"
22558
+ ],
22559
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
22560
+ }
22561
+ },
22562
+ required: ["session_id"]
22563
+ },
22564
+ handler: async (args) => {
22565
+ const sessionId = args.session_id;
22566
+ const reason = args.reason ?? "operator_cancelled";
22567
+ const session = sessions.get(sessionId);
22568
+ if (!session) {
22569
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
22570
+ }
22571
+ if (session.state === "completed") {
22572
+ return toolResult({
22573
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
22574
+ });
22575
+ }
22576
+ sessions.delete(sessionId);
22577
+ auditHandshakeAborted(auditLog, {
22578
+ session_id: sessionId,
22579
+ role: session.role,
22580
+ identity_id: session.our_shr.body.instance_id,
22581
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
22582
+ reason
22583
+ });
22584
+ return toolResult({
22585
+ aborted: true,
22586
+ session_id: sessionId,
22587
+ reason
22588
+ });
22589
+ }
21837
22590
  }
21838
22591
  ];
21839
22592
  return { tools, handshakeResults };
21840
22593
  }
22594
+ function classifyRespondFailure(error) {
22595
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22596
+ if (error.includes("SHR verification failed")) return "shr_invalid";
22597
+ if (error.includes("No identity available")) return "no_signing_identity";
22598
+ return "other";
22599
+ }
22600
+ function classifyCompleteFailure(error) {
22601
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22602
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
22603
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
22604
+ if (error.includes("No identity available")) return "no_signing_identity";
22605
+ return "other";
22606
+ }
21841
22607
  var init_tools6 = __esm({
21842
22608
  "src/handshake/tools.ts"() {
21843
22609
  init_router();
@@ -21847,6 +22613,7 @@ var init_tools6 = __esm({
21847
22613
  init_encoding();
21848
22614
  init_protocol();
21849
22615
  init_attestation();
22616
+ init_audit();
21850
22617
  init_verifier();
21851
22618
  }
21852
22619
  });
@@ -32982,7 +33749,21 @@ var init_operator_chat_audit_events = __esm({
32982
33749
  * successful thread removal. Body carries thread_id + turn_count of
32983
33750
  * the deleted bundle.
32984
33751
  */
32985
- CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
33752
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
33753
+ /**
33754
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
33755
+ * the multi-turn coherence fold cannot load the active thread's prior
33756
+ * turns; the concierge degrades to single-turn after emitting. Body
33757
+ * carries thread_id + a stable failure_reason enum.
33758
+ */
33759
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed",
33760
+ /**
33761
+ * Concierge dynamic-context fetcher failed (WP-V1.3-9 Tau-3). Emitted
33762
+ * when a category fetcher throws while assembling the dynamic context
33763
+ * fold. The concierge omits that category and continues; the user-
33764
+ * facing query is never broken. Body carries category + failure_reason.
33765
+ */
33766
+ CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
32986
33767
  };
32987
33768
  }
32988
33769
  });
@@ -32995,20 +33776,312 @@ var init_operator_chat_types = __esm({
32995
33776
  CONCIERGE_THREAD_KEY = "_fortress";
32996
33777
  }
32997
33778
  });
33779
+
33780
+ // src/chat/concierge-context-router.ts
33781
+ function phrasePattern(phrase) {
33782
+ const escaped = phrase.toLowerCase().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
33783
+ return { source: `\\b${escaped}\\b`, phrase };
33784
+ }
33785
+ function extractAgentNameHint(query) {
33786
+ const agentPattern = /\bagent\s+["']?([A-Za-z][\w-]{0,40})["']?/i;
33787
+ const m = query.match(agentPattern);
33788
+ if (m && m[1]) return m[1];
33789
+ const quoted = query.match(/["']([A-Za-z][\w-]{0,40})["']/);
33790
+ if (quoted && quoted[1]) return quoted[1];
33791
+ return null;
33792
+ }
33793
+ function isTrivialQuery(query) {
33794
+ const norm = query.trim().toLowerCase();
33795
+ if (norm.length === 0) return true;
33796
+ if (norm.length < 8) return true;
33797
+ return TRIVIAL_GREETINGS.has(norm);
33798
+ }
33799
+ function classifyQuery(query) {
33800
+ const normalized = query.toLowerCase();
33801
+ const matches = [];
33802
+ for (const spec of CATEGORY_KEYWORDS) {
33803
+ const matchedPhrases = [];
33804
+ for (const pattern of spec.patterns) {
33805
+ if (matchedPhrases.includes(pattern.phrase)) continue;
33806
+ const re = new RegExp(pattern.source, "i");
33807
+ if (re.test(normalized)) {
33808
+ matchedPhrases.push(pattern.phrase);
33809
+ }
33810
+ }
33811
+ if (matchedPhrases.length === 0) continue;
33812
+ const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
33813
+ matches.push({
33814
+ category: spec.category,
33815
+ confidence,
33816
+ matched_keywords: matchedPhrases,
33817
+ agent_name_hint: spec.category === "agent_state" || spec.category === "agent_activity" ? extractAgentNameHint(query) : null
33818
+ });
33819
+ }
33820
+ matches.sort((a, b) => {
33821
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
33822
+ return CONTEXT_CATEGORIES.indexOf(a.category) - CONTEXT_CATEGORIES.indexOf(b.category);
33823
+ });
33824
+ return matches;
33825
+ }
33826
+ function approxTokenLen(text) {
33827
+ return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
33828
+ }
33829
+ async function runFetcher(match, fetchers) {
33830
+ switch (match.category) {
33831
+ case "templates":
33832
+ return fetchers.templates();
33833
+ case "agent_state":
33834
+ return fetchers.agent_state(match.agent_name_hint);
33835
+ case "agent_activity":
33836
+ return fetchers.agent_activity(match.agent_name_hint);
33837
+ case "audit_log":
33838
+ return fetchers.audit_log();
33839
+ case "sentinel_findings":
33840
+ return fetchers.sentinel_findings();
33841
+ case "anomaly_alerts":
33842
+ return fetchers.anomaly_alerts();
33843
+ case "recent_receipts":
33844
+ return fetchers.recent_receipts();
33845
+ case "verascore_deltas":
33846
+ return fetchers.verascore_deltas();
33847
+ }
33848
+ }
33849
+ function trivialMatch(category) {
33850
+ return {
33851
+ category,
33852
+ confidence: 0.5,
33853
+ matched_keywords: ["llm-assist"],
33854
+ agent_name_hint: null
33855
+ };
33856
+ }
33857
+ async function foldContext(query, fetchers, opts) {
33858
+ const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
33859
+ let matches = classifyQuery(query);
33860
+ if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
33861
+ try {
33862
+ const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
33863
+ if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
33864
+ matches = [trivialMatch(picked)];
33865
+ }
33866
+ } catch {
33867
+ }
33868
+ }
33869
+ if (matches.length === 0) {
33870
+ return { section: "", categoriesIncluded: [] };
33871
+ }
33872
+ const attempts = [];
33873
+ for (const match of matches) {
33874
+ try {
33875
+ const text = await runFetcher(match, fetchers);
33876
+ const trimmed = text.trim();
33877
+ if (trimmed.length > 0) {
33878
+ attempts.push({ category: match.category, text: trimmed });
33879
+ }
33880
+ } catch (err) {
33881
+ opts?.onFetcherFailure?.(match.category, err);
33882
+ }
33883
+ }
33884
+ if (attempts.length === 0) {
33885
+ return { section: "", categoriesIncluded: [] };
33886
+ }
33887
+ const headerTokens = approxTokenLen(`${DYNAMIC_CONTEXT_SECTION_HEADER}
33888
+ `);
33889
+ const sepTokens = approxTokenLen("\n\n");
33890
+ let runningTokens = headerTokens;
33891
+ const kept = [];
33892
+ for (const attempt of attempts) {
33893
+ const block = `### ${CATEGORY_LABELS[attempt.category]}
33894
+ ${attempt.text}`;
33895
+ const tokens = approxTokenLen(block) + (kept.length > 0 ? sepTokens : 0);
33896
+ if (kept.length === 0) {
33897
+ kept.push(attempt);
33898
+ runningTokens += tokens;
33899
+ continue;
33900
+ }
33901
+ if (runningTokens + tokens > budget) break;
33902
+ kept.push(attempt);
33903
+ runningTokens += tokens;
33904
+ }
33905
+ const blocks = kept.map(
33906
+ (k) => `### ${CATEGORY_LABELS[k.category]}
33907
+ ${k.text}`
33908
+ );
33909
+ const section = `${DYNAMIC_CONTEXT_SECTION_HEADER}
33910
+ ${blocks.join("\n\n")}`;
33911
+ return {
33912
+ section,
33913
+ categoriesIncluded: kept.map((k) => k.category)
33914
+ };
33915
+ }
33916
+ var APPROX_CHARS_PER_TOKEN, DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET, DYNAMIC_CONTEXT_SECTION_HEADER, CONTEXT_CATEGORIES, CATEGORY_KEYWORDS, TRIVIAL_GREETINGS, CATEGORY_LABELS;
33917
+ var init_concierge_context_router = __esm({
33918
+ "src/chat/concierge-context-router.ts"() {
33919
+ APPROX_CHARS_PER_TOKEN = 4;
33920
+ DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET = 2e3;
33921
+ DYNAMIC_CONTEXT_SECTION_HEADER = "## Live fortress context";
33922
+ CONTEXT_CATEGORIES = [
33923
+ "templates",
33924
+ "agent_state",
33925
+ "agent_activity",
33926
+ "audit_log",
33927
+ "sentinel_findings",
33928
+ "anomaly_alerts",
33929
+ "recent_receipts",
33930
+ "verascore_deltas"
33931
+ ];
33932
+ CATEGORY_KEYWORDS = [
33933
+ {
33934
+ category: "templates",
33935
+ patterns: [
33936
+ "templates",
33937
+ "template",
33938
+ "channel templates",
33939
+ "channel template",
33940
+ "list templates",
33941
+ "available templates",
33942
+ "what templates"
33943
+ ].map(phrasePattern)
33944
+ },
33945
+ {
33946
+ category: "agent_state",
33947
+ patterns: [
33948
+ "state",
33949
+ "status",
33950
+ "agent state",
33951
+ "agent status",
33952
+ "status of agent",
33953
+ "status of agents",
33954
+ "state of",
33955
+ "doing"
33956
+ ].map(phrasePattern)
33957
+ },
33958
+ {
33959
+ category: "agent_activity",
33960
+ patterns: [
33961
+ "activity",
33962
+ "agent activity",
33963
+ "what did",
33964
+ "recent activity"
33965
+ ].map(phrasePattern)
33966
+ },
33967
+ {
33968
+ category: "audit_log",
33969
+ patterns: [
33970
+ "audit log",
33971
+ "audit",
33972
+ "log entry",
33973
+ "log entries",
33974
+ "what happened",
33975
+ "show me events",
33976
+ "event class"
33977
+ ].map(phrasePattern)
33978
+ },
33979
+ {
33980
+ category: "sentinel_findings",
33981
+ patterns: [
33982
+ "sentinel",
33983
+ "sentinels",
33984
+ "warning",
33985
+ "warnings",
33986
+ "alert",
33987
+ "alerts",
33988
+ "whats wrong",
33989
+ "what's wrong",
33990
+ "findings"
33991
+ ].map(phrasePattern)
33992
+ },
33993
+ {
33994
+ category: "anomaly_alerts",
33995
+ patterns: [
33996
+ "anomaly",
33997
+ "anomalies",
33998
+ "spike",
33999
+ "unusual",
34000
+ "outlier"
34001
+ ].map(phrasePattern)
34002
+ },
34003
+ {
34004
+ category: "recent_receipts",
34005
+ patterns: [
34006
+ "receipt",
34007
+ "receipts",
34008
+ "concordia",
34009
+ "commitment",
34010
+ "commitments",
34011
+ "chain",
34012
+ "chains"
34013
+ ].map(phrasePattern)
34014
+ },
34015
+ {
34016
+ category: "verascore_deltas",
34017
+ patterns: [
34018
+ "verascore",
34019
+ "vera score",
34020
+ "trust score",
34021
+ "reputation"
34022
+ ].map(phrasePattern)
34023
+ }
34024
+ ];
34025
+ TRIVIAL_GREETINGS = /* @__PURE__ */ new Set([
34026
+ "hi",
34027
+ "hello",
34028
+ "hey",
34029
+ "yo",
34030
+ "ok",
34031
+ "thanks",
34032
+ "thx",
34033
+ "thank you"
34034
+ ]);
34035
+ CATEGORY_LABELS = {
34036
+ templates: "Templates",
34037
+ agent_state: "Agent state",
34038
+ agent_activity: "Agent activity",
34039
+ audit_log: "Audit log",
34040
+ sentinel_findings: "Sentinel findings",
34041
+ anomaly_alerts: "Anomaly alerts",
34042
+ recent_receipts: "Recent receipts",
34043
+ verascore_deltas: "Verascore deltas"
34044
+ };
34045
+ }
34046
+ });
34047
+ function approxTokenLen2(text) {
34048
+ return Math.ceil(text.length / 4);
34049
+ }
32998
34050
  function makeEventId(prefix) {
32999
34051
  return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
33000
34052
  }
34053
+ function classifyFetcherError(error) {
34054
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
34055
+ if (msg.includes("timeout") || msg.includes("timed out")) return "timeout";
34056
+ if (msg.includes("schema") || msg.includes("invalid shape")) {
34057
+ return "schema_mismatch";
34058
+ }
34059
+ if (msg.includes("io") || msg.includes("read") || msg.includes("enoent") || msg.includes("eacces")) {
34060
+ return "io_failed";
34061
+ }
34062
+ return "unknown";
34063
+ }
34064
+ function formatPriorTurnLine(turn) {
34065
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
34066
+ return `${label}: ${turn.content}`;
34067
+ }
33001
34068
  function hashOf(input) {
33002
34069
  return hashToString(sha256.sha256(stringToBytes(input)));
33003
34070
  }
33004
- var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
34071
+ var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
33005
34072
  var init_operator_chat_service = __esm({
33006
34073
  "src/chat/operator-chat-service.ts"() {
33007
34074
  init_hashing();
33008
34075
  init_encoding();
33009
34076
  init_operator_chat_audit_events();
33010
34077
  init_operator_chat_types();
34078
+ init_concierge_context_router();
33011
34079
  DEFAULT_CONCIERGE_MAX_TOKENS = 512;
34080
+ DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
34081
+ DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
34082
+ DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
34083
+ DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
34084
+ DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
33012
34085
  SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
33013
34086
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
33014
34087
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -33045,6 +34118,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33045
34118
  piiFilter;
33046
34119
  conciergeMaxTokens;
33047
34120
  memory;
34121
+ historyWindowTurns;
34122
+ historyFreshnessMs;
34123
+ historyTokenBudget;
34124
+ sessionTtlMs;
34125
+ clock;
34126
+ contextFetchers;
34127
+ contextLlmAssist;
34128
+ dynamicContextBudget;
33048
34129
  /**
33049
34130
  * In-memory thread_id assigned to the active concierge session.
33050
34131
  * The first sendConcierge call after construction allocates a fresh
@@ -33052,6 +34133,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33052
34133
  * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
33053
34134
  */
33054
34135
  activeMemoryThreadId;
34136
+ /**
34137
+ * Wall-clock ms of the most recent sendConcierge that touched the
34138
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
34139
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
34140
+ * allocates a new thread_id even though the prior one is still
34141
+ * readable from the memory store.
34142
+ */
34143
+ lastInteractionAt;
33055
34144
  constructor(deps) {
33056
34145
  this.store = deps.store;
33057
34146
  this.auditLog = deps.auditLog;
@@ -33063,6 +34152,18 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33063
34152
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
33064
34153
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
33065
34154
  if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
34155
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
34156
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
34157
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
34158
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
34159
+ this.clock = deps.conciergeClock ?? (() => Date.now());
34160
+ if (deps.conciergeContextFetchers) {
34161
+ this.contextFetchers = deps.conciergeContextFetchers;
34162
+ }
34163
+ if (deps.conciergeContextLlmAssist) {
34164
+ this.contextLlmAssist = deps.conciergeContextLlmAssist;
34165
+ }
34166
+ this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
33066
34167
  }
33067
34168
  // ── Concierge ─────────────────────────────────────────────────────────
33068
34169
  /**
@@ -33081,6 +34182,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33081
34182
  throw new Error("concierge query must not be empty");
33082
34183
  }
33083
34184
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
34185
+ const nowMs = this.clock();
34186
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
34187
+ this.activeMemoryThreadId = void 0;
34188
+ }
33084
34189
  const operatorMessage = {
33085
34190
  message_id: crypto.randomUUID(),
33086
34191
  surface: "concierge",
@@ -33093,6 +34198,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33093
34198
  CONCIERGE_THREAD_KEY,
33094
34199
  operatorMessage
33095
34200
  );
34201
+ let priorTurns = [];
34202
+ let memoryReadFailureReason = null;
34203
+ let activeThreadIdForRound;
34204
+ if (this.memory) {
34205
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
34206
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
34207
+ if (result.ok) {
34208
+ const cutoff = nowMs - this.historyFreshnessMs;
34209
+ const fresh = result.turns.filter((t) => {
34210
+ const ts = Date.parse(t.created_at);
34211
+ return Number.isFinite(ts) && ts >= cutoff;
34212
+ });
34213
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
34214
+ priorTurns = recent;
34215
+ } else {
34216
+ memoryReadFailureReason = result.reason;
34217
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
34218
+ }
34219
+ }
33096
34220
  if (this.memory) {
33097
34221
  const threadId = this.ensureActiveMemoryThread();
33098
34222
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
@@ -33103,6 +34227,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33103
34227
  let servedBy = "disabled";
33104
34228
  let displayLabel = "Concierge: substrate not configured";
33105
34229
  let outcome = "substrate_disabled";
34230
+ let dynamicCategoriesIncluded = [];
33106
34231
  if (!this.substrateSelector) {
33107
34232
  conciergeBody = "Concierge unavailable. The substrate selector is not configured for this fortress. Pick a substrate in the Policy center to enable concierge replies.";
33108
34233
  } else {
@@ -33114,7 +34239,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33114
34239
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
33115
34240
  outcome = "substrate_disabled";
33116
34241
  } else {
33117
- const context = await this.assembleConciergeContext();
34242
+ const dynamicResult = await this.runDynamicContextFold(
34243
+ filterResult.filtered
34244
+ );
34245
+ dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
34246
+ const context = await this.assembleConciergeContext(
34247
+ priorTurns,
34248
+ dynamicResult.section
34249
+ );
33118
34250
  const response = await this.substrateSelector.invokeSummarize(
33119
34251
  "concierge",
33120
34252
  {
@@ -33153,10 +34285,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33153
34285
  CONCIERGE_THREAD_KEY,
33154
34286
  responseMessage
33155
34287
  );
34288
+ let assistantTurnId;
33156
34289
  if (this.memory) {
33157
34290
  const threadId = this.ensureActiveMemoryThread();
33158
- await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
33159
- });
34291
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
34292
+ if (persisted) assistantTurnId = persisted.turn_id;
34293
+ }
34294
+ if (this.memory && activeThreadIdForRound) {
34295
+ this.lastInteractionAt = nowMs;
33160
34296
  }
33161
34297
  const payload = {
33162
34298
  version: "1.2",
@@ -33169,7 +34305,13 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33169
34305
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
33170
34306
  substrate: servedBy,
33171
34307
  latency_ms: latencyMs,
33172
- outcome
34308
+ outcome,
34309
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
34310
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
34311
+ ...this.memory ? {
34312
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
34313
+ } : {},
34314
+ ...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
33173
34315
  };
33174
34316
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
33175
34317
  return {
@@ -33179,6 +34321,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33179
34321
  outcome
33180
34322
  };
33181
34323
  }
34324
+ /**
34325
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
34326
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
34327
+ * with `result: "failure"` since the concierge fell back to
34328
+ * single-turn mode for this round-trip.
34329
+ */
34330
+ emitMemoryReadFailed(threadId, reason) {
34331
+ const payload = {
34332
+ version: "1.2",
34333
+ event_id: makeEventId("conc-memfail"),
34334
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
34335
+ identity_id: this.identityId,
34336
+ kind: "operator_concierge_memory_read_failed",
34337
+ surface: "concierge",
34338
+ thread_id: threadId,
34339
+ failure_reason: reason
34340
+ };
34341
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
34342
+ }
33182
34343
  /**
33183
34344
  * Read the persisted concierge thread, oldest message first. Returns
33184
34345
  * an empty array when no thread exists yet.
@@ -33301,6 +34462,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33301
34462
  * ## Sanctuary reference
33302
34463
  * <static domain reference block>
33303
34464
  *
34465
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
34466
+ * ### <Category>
34467
+ * <fetcher payload>
34468
+ *
34469
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
34470
+ * OPERATOR: ...
34471
+ * CONCIERGE: ...
34472
+ *
33304
34473
  * ## Recent activity
33305
34474
  * <recentActivity output>
33306
34475
  *
@@ -33310,37 +34479,116 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33310
34479
  * ## Open inbox
33311
34480
  * <openInbox output>
33312
34481
  * ```
33313
- */
33314
- async assembleConciergeContext() {
34482
+ *
34483
+ * The substrate selector ships a `context: string` shape (not a
34484
+ * messages array), so multi-turn coherence is folded as a structured
34485
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
34486
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
34487
+ * if available; the v1.2 selector does not expose one, so structured
34488
+ * serialization is the canonical path for v1.3.
34489
+ */
34490
+ async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
33315
34491
  const ref = `## Sanctuary reference
33316
34492
  ${SANCTUARY_DOMAIN_REFERENCE}`;
34493
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
33317
34494
  if (!this.contextProviders) {
33318
- return `${ref}
33319
-
33320
- ## Recent activity
33321
- (no providers wired)
33322
-
33323
- ## Wrapped agents
33324
- (no providers wired)
33325
-
33326
- ## Open inbox
33327
- (no providers wired)`;
34495
+ return [
34496
+ ref,
34497
+ ...dynamicSection ? [dynamicSection] : [],
34498
+ ...priorSection ? [priorSection] : [],
34499
+ "## Recent activity\n(no providers wired)",
34500
+ "## Wrapped agents\n(no providers wired)",
34501
+ "## Open inbox\n(no providers wired)"
34502
+ ].join("\n\n");
33328
34503
  }
33329
34504
  const [activity, agents, inbox] = await Promise.all([
33330
34505
  this.contextProviders.recentActivity(),
33331
34506
  this.contextProviders.agentInventory(),
33332
34507
  this.contextProviders.openInbox()
33333
34508
  ]);
33334
- return `${ref}
33335
-
33336
- ## Recent activity
33337
- ${activity}
33338
-
33339
- ## Wrapped agents
33340
- ${agents}
33341
-
33342
- ## Open inbox
33343
- ${inbox}`;
34509
+ return [
34510
+ ref,
34511
+ ...dynamicSection ? [dynamicSection] : [],
34512
+ ...priorSection ? [priorSection] : [],
34513
+ `## Recent activity
34514
+ ${activity}`,
34515
+ `## Wrapped agents
34516
+ ${agents}`,
34517
+ `## Open inbox
34518
+ ${inbox}`
34519
+ ].join("\n\n");
34520
+ }
34521
+ /**
34522
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
34523
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
34524
+ * an empty fold, fetcher failures emit a per-category audit event
34525
+ * and are omitted from the rendered section, an LLM-assist failure
34526
+ * proceeds with no fold. Returns the rendered section + the list of
34527
+ * categories whose data made it into the section (used for the
34528
+ * round-trip audit emission).
34529
+ */
34530
+ async runDynamicContextFold(query) {
34531
+ if (!this.contextFetchers) {
34532
+ return { section: "", categoriesIncluded: [] };
34533
+ }
34534
+ const result = await foldContext(query, this.contextFetchers, {
34535
+ maxTokens: this.dynamicContextBudget,
34536
+ ...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
34537
+ onFetcherFailure: (category, error) => {
34538
+ this.emitContextFetcherFailed(category, classifyFetcherError(error));
34539
+ }
34540
+ });
34541
+ return result;
34542
+ }
34543
+ /**
34544
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
34545
+ * of the fold path so the dynamic-context handler stays readable.
34546
+ * Emits with `result: "failure"` since the named category dropped
34547
+ * from the rendered section for this round-trip.
34548
+ */
34549
+ emitContextFetcherFailed(category, failureReason) {
34550
+ const payload = {
34551
+ version: "1.2",
34552
+ event_id: makeEventId("conc-ctxfail"),
34553
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
34554
+ identity_id: this.identityId,
34555
+ kind: "operator_concierge_context_fetcher_failed",
34556
+ surface: "concierge",
34557
+ category,
34558
+ failure_reason: failureReason
34559
+ };
34560
+ this.emit(
34561
+ OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
34562
+ payload,
34563
+ "failure"
34564
+ );
34565
+ }
34566
+ /**
34567
+ * Render the prior-conversation section with token-budget enforcement
34568
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
34569
+ * section exceeds `historyTokenBudget`. Returns an empty string when
34570
+ * the input is empty or when the budget excludes every turn.
34571
+ */
34572
+ formatPriorTurnsSection(turns) {
34573
+ if (turns.length === 0) return "";
34574
+ const HEADER = "## Prior conversation";
34575
+ const lines = turns.map(formatPriorTurnLine);
34576
+ const headerTokens = approxTokenLen2(`${HEADER}
34577
+ `);
34578
+ const sepTokens = approxTokenLen2("\n");
34579
+ let runningTokens = headerTokens;
34580
+ let runningLines = [];
34581
+ for (let i = lines.length - 1; i >= 0; i--) {
34582
+ const line = lines[i];
34583
+ const tokens = approxTokenLen2(line) + (runningLines.length > 0 ? sepTokens : 0);
34584
+ if (runningTokens + tokens > this.historyTokenBudget) break;
34585
+ runningTokens += tokens;
34586
+ runningLines.push(line);
34587
+ }
34588
+ if (runningLines.length === 0) return "";
34589
+ runningLines = runningLines.reverse();
34590
+ return `${HEADER}
34591
+ ${runningLines.join("\n")}`;
33344
34592
  }
33345
34593
  // ── audit helpers ────────────────────────────────────────────────────
33346
34594
  emit(operation, payload, result) {
@@ -33360,7 +34608,7 @@ ${inbox}`;
33360
34608
  function chatStorageKey(surface, threadKey) {
33361
34609
  return `${surface}.${threadKey}`;
33362
34610
  }
33363
- var OPERATOR_CHAT_NAMESPACE, HKDF_INFO, OperatorChatStore;
34611
+ var OPERATOR_CHAT_NAMESPACE, HKDF_INFO2, OperatorChatStore;
33364
34612
  var init_operator_chat_store = __esm({
33365
34613
  "src/chat/operator-chat-store.ts"() {
33366
34614
  init_encryption();
@@ -33368,13 +34616,13 @@ var init_operator_chat_store = __esm({
33368
34616
  init_encoding();
33369
34617
  init_operator_chat_types();
33370
34618
  OPERATOR_CHAT_NAMESPACE = "_chat";
33371
- HKDF_INFO = "operator-chat-store-v1";
34619
+ HKDF_INFO2 = "operator-chat-store-v1";
33372
34620
  OperatorChatStore = class {
33373
34621
  storage;
33374
34622
  encryptionKey;
33375
34623
  constructor(storage, masterKey) {
33376
34624
  this.storage = storage;
33377
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO);
34625
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
33378
34626
  }
33379
34627
  /**
33380
34628
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -33459,7 +34707,7 @@ var init_operator_chat_store = __esm({
33459
34707
  function bundleKey(threadId) {
33460
34708
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
33461
34709
  }
33462
- function stripKeyPrefix(key) {
34710
+ function stripKeyPrefix2(key) {
33463
34711
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
33464
34712
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
33465
34713
  }
@@ -33470,7 +34718,7 @@ function lastTurnId(bundle) {
33470
34718
  }
33471
34719
  return max;
33472
34720
  }
33473
- var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
34721
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO3, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
33474
34722
  var init_concierge_memory_store = __esm({
33475
34723
  "src/chat/concierge-memory-store.ts"() {
33476
34724
  init_encryption();
@@ -33478,9 +34726,9 @@ var init_concierge_memory_store = __esm({
33478
34726
  init_encoding();
33479
34727
  CONCIERGE_MEMORY_NAMESPACE = "_chat";
33480
34728
  CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
33481
- HKDF_INFO2 = "concierge-memory-store-v1";
34729
+ HKDF_INFO3 = "concierge-memory-store-v1";
33482
34730
  DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
33483
- MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
34731
+ MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
33484
34732
  ConciergeMemoryStore = class {
33485
34733
  storage;
33486
34734
  encryptionKey;
@@ -33489,7 +34737,7 @@ var init_concierge_memory_store = __esm({
33489
34737
  locks;
33490
34738
  constructor(opts) {
33491
34739
  this.storage = opts.storage;
33492
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
34740
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
33493
34741
  this.fortressId = opts.fortressId;
33494
34742
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
33495
34743
  this.locks = /* @__PURE__ */ new Map();
@@ -33545,6 +34793,65 @@ var init_concierge_memory_store = __esm({
33545
34793
  }
33546
34794
  return turns;
33547
34795
  }
34796
+ /**
34797
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
34798
+ * `readThread` collapses every failure mode to an empty array, this
34799
+ * variant returns a discriminated result so the multi-turn fold path
34800
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
34801
+ * with a concrete cause.
34802
+ *
34803
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
34804
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
34805
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
34806
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
34807
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
34808
+ * - Storage IO error → `io_failed`.
34809
+ */
34810
+ async readThreadStrict(threadId, opts) {
34811
+ const key = bundleKey(threadId);
34812
+ let raw;
34813
+ try {
34814
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
34815
+ } catch {
34816
+ return { ok: false, reason: "io_failed" };
34817
+ }
34818
+ if (!raw) return { ok: true, turns: [] };
34819
+ if (raw.length > MAX_BUNDLE_BYTES3) {
34820
+ return { ok: false, reason: "oversize_bundle" };
34821
+ }
34822
+ let envelope;
34823
+ try {
34824
+ envelope = JSON.parse(bytesToString(raw));
34825
+ } catch {
34826
+ return { ok: false, reason: "schema_mismatch" };
34827
+ }
34828
+ let plaintext;
34829
+ try {
34830
+ const aad = stringToBytes(threadId);
34831
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
34832
+ } catch {
34833
+ return { ok: false, reason: "decrypt_failed" };
34834
+ }
34835
+ let parsed;
34836
+ try {
34837
+ parsed = JSON.parse(bytesToString(plaintext));
34838
+ } catch {
34839
+ return { ok: false, reason: "schema_mismatch" };
34840
+ }
34841
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
34842
+ if (parsed.thread_id !== threadId) {
34843
+ return { ok: false, reason: "schema_mismatch" };
34844
+ }
34845
+ let turns = parsed.turns;
34846
+ if (opts?.sinceTurnId !== void 0) {
34847
+ const cutoff = opts.sinceTurnId;
34848
+ turns = turns.filter((t) => t.turn_id > cutoff);
34849
+ }
34850
+ if (opts?.limit !== void 0) {
34851
+ turns = turns.slice(0, opts.limit);
34852
+ }
34853
+ return { ok: true, turns };
34854
+ }
33548
34855
  /**
33549
34856
  * Enumerate concierge threads in this fortress with summary metadata.
33550
34857
  * Sorted newest-first by last_turn_at.
@@ -33556,7 +34863,7 @@ var init_concierge_memory_store = __esm({
33556
34863
  );
33557
34864
  const summaries = [];
33558
34865
  for (const meta of entries) {
33559
- const threadId = stripKeyPrefix(meta.key);
34866
+ const threadId = stripKeyPrefix2(meta.key);
33560
34867
  if (threadId === null) continue;
33561
34868
  const bundle = await this.loadBundle(threadId);
33562
34869
  if (!bundle || bundle.turns.length === 0) continue;
@@ -33609,7 +34916,7 @@ var init_concierge_memory_store = __esm({
33609
34916
  );
33610
34917
  let pruned = 0;
33611
34918
  for (const meta of entries) {
33612
- const threadId = stripKeyPrefix(meta.key);
34919
+ const threadId = stripKeyPrefix2(meta.key);
33613
34920
  if (threadId === null) continue;
33614
34921
  pruned += await this.withLock(threadId, async () => {
33615
34922
  const bundle = await this.loadBundle(threadId);
@@ -33640,7 +34947,7 @@ var init_concierge_memory_store = __esm({
33640
34947
  return null;
33641
34948
  }
33642
34949
  if (!raw) return null;
33643
- if (raw.length > MAX_BUNDLE_BYTES2) return null;
34950
+ if (raw.length > MAX_BUNDLE_BYTES3) return null;
33644
34951
  try {
33645
34952
  const envelope = JSON.parse(bytesToString(raw));
33646
34953
  const aad = stringToBytes(threadId);
@@ -33731,7 +35038,18 @@ function buildV11Bindings(inputs) {
33731
35038
  registry
33732
35039
  }),
33733
35040
  conciergePiiFilter: buildConciergePiiFilter(),
33734
- conciergeMemory
35041
+ conciergeMemory,
35042
+ conciergeContextFetchers: buildConciergeContextFetchers({
35043
+ auditLog: inputs.auditLog,
35044
+ identityId: inputs.identityId,
35045
+ registry
35046
+ }),
35047
+ ...inputs.intelligenceSelector ? {
35048
+ conciergeContextLlmAssist: buildConciergeContextLlmAssist({
35049
+ selector: inputs.intelligenceSelector,
35050
+ identityId: inputs.identityId
35051
+ })
35052
+ } : {}
33735
35053
  });
33736
35054
  }
33737
35055
  const hubService = new HubService({
@@ -33792,6 +35110,107 @@ function buildConciergeContextProviders(args) {
33792
35110
  }
33793
35111
  };
33794
35112
  }
35113
+ function buildConciergeContextFetchers(args) {
35114
+ const empty = async () => "";
35115
+ return {
35116
+ templates: async () => {
35117
+ const entries = listTemplates();
35118
+ if (entries.length === 0) return "(no templates installed)";
35119
+ const lines = entries.map((e) => {
35120
+ const m = e.metadata;
35121
+ return `${m.name} (tier ${m.tier}, channel ${m.channel}, target ${m.target_archetype})`;
35122
+ });
35123
+ return lines.join("\n");
35124
+ },
35125
+ agent_state: async (agentNameHint) => {
35126
+ const records = args.registry.list({ identity_id: args.identityId });
35127
+ if (records.length === 0) return "(no wrapped agents)";
35128
+ const filtered = agentNameHint ? records.filter(
35129
+ (r) => r.agent_id.toLowerCase().includes(agentNameHint.toLowerCase()) || r.harness.toLowerCase().includes(agentNameHint.toLowerCase())
35130
+ ) : records;
35131
+ const target = filtered.length > 0 ? filtered : records;
35132
+ const lines = target.slice(0, 20).map((r) => {
35133
+ const tmpl = typeof r.channel_template_id === "string" ? r.channel_template_id : "no_template";
35134
+ return `${r.agent_id} harness=${r.harness} status=${r.status} template=${tmpl}`;
35135
+ });
35136
+ return lines.join("\n");
35137
+ },
35138
+ agent_activity: async (agentNameHint) => {
35139
+ const result = await args.auditLog.query({ limit: 50 });
35140
+ const owned = result.entries.filter(
35141
+ (e) => e.identity_id === args.identityId
35142
+ );
35143
+ const filtered = agentNameHint ? owned.filter((e) => {
35144
+ const agentId = e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : "";
35145
+ return agentId.toLowerCase().includes(agentNameHint.toLowerCase());
35146
+ }) : owned;
35147
+ const tail = (filtered.length > 0 ? filtered : owned).slice(-20);
35148
+ if (tail.length === 0) return "(no activity)";
35149
+ return tail.map((e) => {
35150
+ const agentId = (e.details && typeof e.details["agent_id"] === "string" ? e.details["agent_id"] : null) ?? "_fortress";
35151
+ return `${e.timestamp} ${e.layer}.${e.operation} agent=${agentId} result=${e.result}`;
35152
+ }).join("\n");
35153
+ },
35154
+ audit_log: async () => {
35155
+ const result = await args.auditLog.query({ limit: 30 });
35156
+ const owned = result.entries.filter(
35157
+ (e) => e.identity_id === args.identityId
35158
+ );
35159
+ if (owned.length === 0) return "(no audit log entries)";
35160
+ return owned.slice(-30).map(
35161
+ (e) => `${e.timestamp} ${e.layer}.${e.operation} result=${e.result}`
35162
+ ).join("\n");
35163
+ },
35164
+ sentinel_findings: empty,
35165
+ anomaly_alerts: empty,
35166
+ recent_receipts: async () => {
35167
+ const result = await args.auditLog.query({ limit: 100 });
35168
+ const owned = result.entries.filter(
35169
+ (e) => e.identity_id === args.identityId && e.operation.startsWith("composition_")
35170
+ );
35171
+ if (owned.length === 0) return "(no recent composition events)";
35172
+ return owned.slice(-15).map((e) => `${e.timestamp} ${e.operation} result=${e.result}`).join("\n");
35173
+ },
35174
+ verascore_deltas: empty
35175
+ };
35176
+ }
35177
+ function buildConciergeContextLlmAssist(args) {
35178
+ return async (query, categories) => {
35179
+ const labelList = categories.map((c) => `- ${c}`).join("\n");
35180
+ const prompt2 = `You are a router. Classify the operator's query into one of the categories below or "none".
35181
+ Reply with exactly one token: one category name or "none".
35182
+
35183
+ Categories:
35184
+ ${labelList}
35185
+
35186
+ Query: ${query}
35187
+
35188
+ Category:`;
35189
+ try {
35190
+ const handle = await args.selector.getSubstrate("concierge");
35191
+ if (!handle.capability.summarize) return "none";
35192
+ const response = await args.selector.invokeSummarize("concierge", {
35193
+ kind: "summarize",
35194
+ context: prompt2,
35195
+ query: "Output the single category token.",
35196
+ maxTokens: 16
35197
+ });
35198
+ if (response.failureClass || response.body.kind !== "summarize") {
35199
+ return "none";
35200
+ }
35201
+ const raw = response.body.text.trim().toLowerCase();
35202
+ const head = raw.split(/\s|[.,!?:;]/)[0] ?? "";
35203
+ const normalized = head.replace(/[^a-z_]/g, "");
35204
+ const known = categories;
35205
+ if (known.includes(normalized)) {
35206
+ return normalized;
35207
+ }
35208
+ return "none";
35209
+ } catch {
35210
+ return "none";
35211
+ }
35212
+ };
35213
+ }
33795
35214
  function buildConciergePiiFilter() {
33796
35215
  return {
33797
35216
  filter(input) {
@@ -33819,6 +35238,7 @@ var init_wiring = __esm({
33819
35238
  init_agent_registry_persistence();
33820
35239
  init_operator_chat_index();
33821
35240
  init_privacy_filter();
35241
+ init_registry();
33822
35242
  CapabilityErrorAgentController = class {
33823
35243
  fail(action) {
33824
35244
  throw new HubCapabilityError(
@@ -33966,7 +35386,7 @@ var init_defaults = __esm({
33966
35386
  });
33967
35387
 
33968
35388
  // src/intelligence/policy-store.ts
33969
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
35389
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO4, IntelligenceConfigStore;
33970
35390
  var init_policy_store = __esm({
33971
35391
  "src/intelligence/policy-store.ts"() {
33972
35392
  init_encryption();
@@ -33975,13 +35395,13 @@ var init_policy_store = __esm({
33975
35395
  init_defaults();
33976
35396
  INTELLIGENCE_NAMESPACE = "_intelligence";
33977
35397
  SUBSTRATE_CONFIG_KEY = "substrate-config";
33978
- HKDF_INFO3 = "intelligence-substrate-config";
35398
+ HKDF_INFO4 = "intelligence-substrate-config";
33979
35399
  IntelligenceConfigStore = class {
33980
35400
  storage;
33981
35401
  encryptionKey;
33982
35402
  constructor(storage, masterKey) {
33983
35403
  this.storage = storage;
33984
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
35404
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
33985
35405
  }
33986
35406
  /**
33987
35407
  * Load the operator's substrate config from disk. Returns the config
@@ -36442,7 +37862,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
36442
37862
  }
36443
37863
  return null;
36444
37864
  }
36445
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
37865
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
36446
37866
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
36447
37867
  if (!destinationSigner) {
36448
37868
  return {
@@ -36504,8 +37924,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36504
37924
  }
36505
37925
  }
36506
37926
  }
37927
+ let plaintext;
36507
37928
  try {
36508
- const plaintext = decrypt(
37929
+ plaintext = decrypt(
36509
37930
  item.entry.payload,
36510
37931
  deriveNamespaceKey(sourceMasterKey, item.namespace)
36511
37932
  );
@@ -36514,28 +37935,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36514
37935
  skipped++;
36515
37936
  continue;
36516
37937
  }
36517
- await stateStore.write(
36518
- item.namespace,
36519
- item.key,
36520
- bytesToString(plaintext),
36521
- destinationSigner.identity_id,
36522
- destinationSigner.encrypted_private_key,
36523
- identityEncryptionKey,
36524
- {
36525
- content_type: item.entry.metadata.content_type,
36526
- ttl_seconds: item.entry.metadata.ttl_seconds,
36527
- tags: [
36528
- ...item.entry.metadata.tags ?? [],
36529
- "exit-import",
36530
- `source:${item.entry.kid}`
36531
- ]
36532
- }
36533
- );
36534
- imported++;
36535
37938
  } catch {
36536
37939
  skippedInvalidSig++;
36537
37940
  skipped++;
37941
+ continue;
36538
37942
  }
37943
+ await stateStore.write(
37944
+ item.namespace,
37945
+ item.key,
37946
+ bytesToString(plaintext),
37947
+ destinationSigner.identity_id,
37948
+ destinationSigner.encrypted_private_key,
37949
+ identityEncryptionKey,
37950
+ {
37951
+ content_type: item.entry.metadata.content_type,
37952
+ ttl_seconds: item.entry.metadata.ttl_seconds,
37953
+ tags: [
37954
+ ...item.entry.metadata.tags ?? [],
37955
+ "exit-import",
37956
+ `source:${item.entry.kid}`
37957
+ ]
37958
+ }
37959
+ );
37960
+ imported++;
37961
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
36539
37962
  }
36540
37963
  return {
36541
37964
  status: "rekeyed",
@@ -36546,6 +37969,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36546
37969
  conflicts
36547
37970
  };
36548
37971
  }
37972
+ async function cleanupStagedPaths(storage, staged) {
37973
+ let removed = 0;
37974
+ const failed = [];
37975
+ for (const loc of staged) {
37976
+ try {
37977
+ const ok2 = await storage.delete(loc.namespace, loc.key);
37978
+ if (ok2) {
37979
+ removed++;
37980
+ } else {
37981
+ failed.push(loc);
37982
+ }
37983
+ } catch {
37984
+ failed.push(loc);
37985
+ }
37986
+ }
37987
+ return { removed, failed };
37988
+ }
36549
37989
  async function stageArtifact(storage, namespace, key, value) {
36550
37990
  await storage.write(namespace, key, jsonBytes(value));
36551
37991
  }
@@ -36670,6 +38110,8 @@ async function importExitBundle(opts) {
36670
38110
  }
36671
38111
  const importId = importIdForManifest(manifest);
36672
38112
  const stagedArtifacts = [];
38113
+ const stagedLocations = [];
38114
+ const importedRekeyEntries = [];
36673
38115
  if (identityArtifact) {
36674
38116
  await stageArtifact(
36675
38117
  opts.storage,
@@ -36678,10 +38120,15 @@ async function importExitBundle(opts) {
36678
38120
  identityArtifact.json
36679
38121
  );
36680
38122
  stagedArtifacts.push("public_identity");
38123
+ stagedLocations.push({
38124
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
38125
+ key: identityArtifact.json.bundle.identity_id
38126
+ });
36681
38127
  }
36682
38128
  if (policySet) {
36683
38129
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
36684
38130
  stagedArtifacts.push("policy_set");
38131
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
36685
38132
  }
36686
38133
  if (auditReceipts) {
36687
38134
  await stageArtifact(
@@ -36691,10 +38138,12 @@ async function importExitBundle(opts) {
36691
38138
  auditReceipts.json
36692
38139
  );
36693
38140
  stagedArtifacts.push("audit_receipts");
38141
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
36694
38142
  }
36695
38143
  if (commitments) {
36696
38144
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
36697
38145
  stagedArtifacts.push("commitments");
38146
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
36698
38147
  }
36699
38148
  if (placeholderMetadata) {
36700
38149
  await stageArtifact(
@@ -36704,12 +38153,17 @@ async function importExitBundle(opts) {
36704
38153
  placeholderMetadata.json
36705
38154
  );
36706
38155
  stagedArtifacts.push("placeholder_vault_metadata");
38156
+ stagedLocations.push({
38157
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
38158
+ key: importId
38159
+ });
36707
38160
  }
36708
38161
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
36709
38162
  manifest: manifest.body,
36710
38163
  verified_at: verification.verified_at,
36711
38164
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
36712
38165
  });
38166
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
36713
38167
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
36714
38168
  let reputationResult = {
36715
38169
  imported_attestations: 0,
@@ -36734,26 +38188,57 @@ async function importExitBundle(opts) {
36734
38188
  encryptedState?.json ?? null,
36735
38189
  opts
36736
38190
  );
36737
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36738
- encryptedState.json,
36739
- opts,
36740
- sourceMasterKey,
36741
- publicKeys.byIdentityId
36742
- ) : {
36743
- status: "staged_requires_source_key",
36744
- imported_keys: 0,
36745
- skipped_keys: encryptedState.json.entries.length,
36746
- skipped_invalid_sig: 0,
36747
- skipped_unknown_kid: 0,
36748
- conflicts: conflicts.state_conflicts.length
36749
- } : {
36750
- status: "not_requested",
36751
- imported_keys: 0,
36752
- skipped_keys: 0,
36753
- skipped_invalid_sig: 0,
36754
- skipped_unknown_kid: 0,
36755
- conflicts: 0
36756
- };
38191
+ let stateResult;
38192
+ try {
38193
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
38194
+ encryptedState.json,
38195
+ opts,
38196
+ sourceMasterKey,
38197
+ publicKeys.byIdentityId,
38198
+ importedRekeyEntries
38199
+ ) : {
38200
+ status: "staged_requires_source_key",
38201
+ imported_keys: 0,
38202
+ skipped_keys: encryptedState.json.entries.length,
38203
+ skipped_invalid_sig: 0,
38204
+ skipped_unknown_kid: 0,
38205
+ conflicts: conflicts.state_conflicts.length
38206
+ } : {
38207
+ status: "not_requested",
38208
+ imported_keys: 0,
38209
+ skipped_keys: 0,
38210
+ skipped_invalid_sig: 0,
38211
+ skipped_unknown_kid: 0,
38212
+ conflicts: 0
38213
+ };
38214
+ } catch (err) {
38215
+ const toCleanup = [
38216
+ ...importedRekeyEntries,
38217
+ ...stagedLocations
38218
+ ];
38219
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
38220
+ opts.auditLog.append(
38221
+ "l1",
38222
+ "exit_bundle_rekey_failed_cleanup",
38223
+ manifest.body.identity_binding.identity_id,
38224
+ {
38225
+ import_id: importId,
38226
+ manifest_version: manifest.body.manifest_version,
38227
+ rekey_entries_removed: importedRekeyEntries.length,
38228
+ staged_artifacts_removed: stagedLocations.length,
38229
+ removed_total: cleanup.removed,
38230
+ cleanup_failed_count: cleanup.failed.length,
38231
+ original_error: err instanceof Error ? err.message : String(err)
38232
+ },
38233
+ "failure"
38234
+ );
38235
+ await opts.auditLog.flush();
38236
+ const originalMessage = err instanceof Error ? err.message : String(err);
38237
+ throw new ExitBundleImportError(
38238
+ "REKEY_FAILED_AND_CLEANED",
38239
+ `Exit-bundle re-key failed: ${originalMessage}. Cleanup removed ${cleanup.removed} of ${toCleanup.length} staged paths (${importedRekeyEntries.length} re-keyed entries plus ${stagedLocations.length} staged artifacts; ${cleanup.failed.length} cleanup deletes failed).`
38240
+ );
38241
+ }
36757
38242
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
36758
38243
  import_id: importId,
36759
38244
  manifest_version: manifest.body.manifest_version,
@@ -37832,16 +39317,35 @@ ${err.message}
37832
39317
  timestamp: alert.timestamp
37833
39318
  });
37834
39319
  } : void 0;
37835
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37836
39320
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37837
39321
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
39322
+ const aggregatorPayloadStore = new AggregatorPayloadStore({
39323
+ storage,
39324
+ masterKey,
39325
+ fortressId: fortressIdForAggregator
39326
+ });
37838
39327
  const approvalAggregator = new ApprovalAggregator({
37839
39328
  storage,
37840
39329
  masterKey,
37841
39330
  auditLog,
37842
39331
  identityId: aggregatorIdentityId,
37843
- fortressId: fortressIdForAggregator
39332
+ fortressId: fortressIdForAggregator,
39333
+ payloadStore: aggregatorPayloadStore
39334
+ });
39335
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
39336
+ underlying: approvalChannel,
39337
+ aggregator: approvalAggregator,
39338
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
39339
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
37844
39340
  });
39341
+ const gate = new ApprovalGate(
39342
+ policy,
39343
+ baseline,
39344
+ wrappedApprovalChannel,
39345
+ auditLog,
39346
+ injectionDetector,
39347
+ onInjectionAlert
39348
+ );
37845
39349
  gate.setApprovalEventCallback((event) => {
37846
39350
  void approvalAggregator.ingest(event);
37847
39351
  });
@@ -38039,6 +39543,8 @@ var init_src = __esm({
38039
39543
  init_webhook();
38040
39544
  init_gate();
38041
39545
  init_approval_aggregator();
39546
+ init_aggregator_backed_channel();
39547
+ init_aggregator_store();
38042
39548
  init_tools4();
38043
39549
  init_router();
38044
39550
  init_router();
@@ -41003,6 +42509,22 @@ var init_broker = __esm({
41003
42509
  auditLog;
41004
42510
  issuer;
41005
42511
  principalIdentityId;
42512
+ /**
42513
+ * Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
42514
+ * addSecret() / rotateSecret() / deleteSecret() calls on the same name
42515
+ * MUST serialize cleanly. The keychain backend's `find-then-add` and
42516
+ * `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
42517
+ * .rotateSecret) are not atomic against another caller racing the same
42518
+ * service-name; without serialization the second caller can observe a
42519
+ * stale "exists" check and either drop the new value or leave a
42520
+ * duplicate keychain entry.
42521
+ *
42522
+ * Implementation: an in-memory promise chain per name. Subsequent
42523
+ * callers `await` the chain tail and append their own work; failures
42524
+ * propagate to the failing caller without poisoning the chain for
42525
+ * later callers.
42526
+ */
42527
+ nameLocks = /* @__PURE__ */ new Map();
41006
42528
  constructor(opts) {
41007
42529
  this.backend = opts.backend;
41008
42530
  this.auditLog = opts.auditLog;
@@ -41013,6 +42535,40 @@ var init_broker = __esm({
41013
42535
  grants: opts.grants
41014
42536
  });
41015
42537
  }
42538
+ /**
42539
+ * Serialize `op` against any other in-flight write to the same secret
42540
+ * `name`. Per-name fairness only, distinct names run in parallel.
42541
+ * The current chain tail is used as the acceptance gate; we then
42542
+ * publish a new tail that swallows the operation's outcome so a
42543
+ * thrown error does not poison the next caller's wait.
42544
+ */
42545
+ async withNameLock(name, op) {
42546
+ const previous = this.nameLocks.get(name) ?? Promise.resolve();
42547
+ let release = () => {
42548
+ };
42549
+ const next = new Promise((resolve8) => {
42550
+ release = resolve8;
42551
+ });
42552
+ this.nameLocks.set(name, next);
42553
+ try {
42554
+ await previous.catch(() => {
42555
+ });
42556
+ return await op();
42557
+ } finally {
42558
+ release();
42559
+ if (this.nameLocks.get(name) === next) {
42560
+ this.nameLocks.delete(name);
42561
+ }
42562
+ }
42563
+ }
42564
+ /**
42565
+ * Diagnostic-only: visible for tests so they can assert that distinct
42566
+ * names do not contend on a shared lock. Not part of the public broker
42567
+ * contract; do not consume from production code.
42568
+ */
42569
+ __nameLockCountForTests() {
42570
+ return this.nameLocks.size;
42571
+ }
41016
42572
  /** Ensure backend is initialized and unlocked. Audits the unlock. */
41017
42573
  async ensureUnlocked(passphrase) {
41018
42574
  await this.backend.ensureInitialized(passphrase);
@@ -41025,31 +42581,37 @@ var init_broker = __esm({
41025
42581
  );
41026
42582
  }
41027
42583
  async addSecret(name, value) {
41028
- await this.backend.addSecret(name, value);
41029
- this.auditLog.append(
41030
- "l3",
41031
- BROKER_OPS.SECRET_ADDED,
41032
- this.principalIdentityId,
41033
- { secret: name }
41034
- );
42584
+ await this.withNameLock(name, async () => {
42585
+ await this.backend.addSecret(name, value);
42586
+ this.auditLog.append(
42587
+ "l3",
42588
+ BROKER_OPS.SECRET_ADDED,
42589
+ this.principalIdentityId,
42590
+ { secret: name }
42591
+ );
42592
+ });
41035
42593
  }
41036
42594
  async rotateSecret(name, newValue) {
41037
- await this.backend.rotateSecret(name, newValue);
41038
- this.auditLog.append(
41039
- "l3",
41040
- BROKER_OPS.SECRET_ROTATED,
41041
- this.principalIdentityId,
41042
- { secret: name }
41043
- );
42595
+ await this.withNameLock(name, async () => {
42596
+ await this.backend.rotateSecret(name, newValue);
42597
+ this.auditLog.append(
42598
+ "l3",
42599
+ BROKER_OPS.SECRET_ROTATED,
42600
+ this.principalIdentityId,
42601
+ { secret: name }
42602
+ );
42603
+ });
41044
42604
  }
41045
42605
  async deleteSecret(name) {
41046
- await this.backend.deleteSecret(name);
41047
- this.auditLog.append(
41048
- "l3",
41049
- BROKER_OPS.SECRET_DELETED,
41050
- this.principalIdentityId,
41051
- { secret: name }
41052
- );
42606
+ await this.withNameLock(name, async () => {
42607
+ await this.backend.deleteSecret(name);
42608
+ this.auditLog.append(
42609
+ "l3",
42610
+ BROKER_OPS.SECRET_DELETED,
42611
+ this.principalIdentityId,
42612
+ { secret: name }
42613
+ );
42614
+ });
41053
42615
  }
41054
42616
  async listSecretNames() {
41055
42617
  return this.backend.listSecretNames();
@@ -41089,6 +42651,19 @@ var init_broker = __esm({
41089
42651
  liveTokenCount() {
41090
42652
  return this.issuer.liveTokenCount();
41091
42653
  }
42654
+ /**
42655
+ * Drop expired tokens from the in-memory issuer map. Hardening wave 6
42656
+ * finding #86: previously expiry pruning depended on opportunistic
42657
+ * `pruneExpired()` calls; now the cocoon-unlock initialization path
42658
+ * (openBroker -> after backend.ensureInitialized -> after Broker
42659
+ * construction) fires this once so each cocoon-unlock cycle drops
42660
+ * stale bindings before any operator interaction.
42661
+ *
42662
+ * Returns the number of tokens removed. Safe to call repeatedly; idempotent.
42663
+ */
42664
+ pruneExpiredTokens() {
42665
+ return this.issuer.pruneExpired();
42666
+ }
41092
42667
  /**
41093
42668
  * Audit query restricted to broker-scoped operations. Returns entries
41094
42669
  * with their timestamps, op, and result (never the secret value).
@@ -41247,6 +42822,7 @@ async function openBroker(opts = {}) {
41247
42822
  grants,
41248
42823
  principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
41249
42824
  });
42825
+ broker.pruneExpiredTokens();
41250
42826
  return {
41251
42827
  broker,
41252
42828
  close: async () => {
@@ -42115,8 +43691,6 @@ var init_health = __esm({
42115
43691
  DEFAULT_TIMEOUT_MS4 = 500;
42116
43692
  }
42117
43693
  });
42118
-
42119
- // src/cli/agents/cli.ts
42120
43694
  function resolveCtx(args) {
42121
43695
  const env = args.env ?? process.env;
42122
43696
  const discoverOpts = {
@@ -42154,6 +43728,8 @@ async function runAgentsCommand(args) {
42154
43728
  return await cmdShow2(rest, ctx);
42155
43729
  case "status":
42156
43730
  return await cmdStatus(rest, ctx);
43731
+ case "config":
43732
+ return await cmdConfig(rest, ctx);
42157
43733
  default:
42158
43734
  ctx.err.write(`Unknown subcommand: ${sub}
42159
43735
  `);
@@ -42169,10 +43745,18 @@ async function runAgentsCommand(args) {
42169
43745
  }
42170
43746
  function printUsage4(s) {
42171
43747
  s.write(`Usage: sanctuary agents <command> [flags]
43748
+ sanctuary agent <command> [flags] (alias)
42172
43749
 
42173
43750
  list [--json] List every tenant visible on this host.
42174
- show <tenant> [--json] Show details for one tenant.
43751
+ show <tenant> [--json] Show details for one tenant (includes
43752
+ approval-redirect state).
42175
43753
  status [--json] One-line-per-tenant running/stopped summary.
43754
+ config <tenant> [opts] Write tenant principal-policy.yaml fields.
43755
+ --approval-redirect=<bool> Toggle cross-harness inbox redirect.
43756
+ --approval-redirect-mode=<replace|notify>
43757
+ Pick replace (bypass underlying channel)
43758
+ or notify (race both paths). Default
43759
+ replace when toggled on.
42176
43760
 
42177
43761
  Options:
42178
43762
  --fortress <path> Scope discovery to a specific storage path
@@ -42274,6 +43858,7 @@ async function cmdShow2(argv, ctx) {
42274
43858
  return 1;
42275
43859
  }
42276
43860
  const probe = await ctx.probe(tenant);
43861
+ const approvalRedirect = await readApprovalRedirectState(tenant);
42277
43862
  const payload = {
42278
43863
  name: tenant.name,
42279
43864
  storage_path: tenant.storage_path,
@@ -42287,7 +43872,8 @@ async function cmdShow2(argv, ctx) {
42287
43872
  running: probe.running,
42288
43873
  status: probe.status,
42289
43874
  reason: probe.reason
42290
- }
43875
+ },
43876
+ approval_redirect: approvalRedirect
42291
43877
  };
42292
43878
  if (hasJsonFlag(argv)) {
42293
43879
  ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
@@ -42331,10 +43917,173 @@ async function cmdShow2(argv, ctx) {
42331
43917
  }
42332
43918
  ctx.out.write(
42333
43919
  `probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
43920
+ `
43921
+ );
43922
+ ctx.out.write(
43923
+ `approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
42334
43924
  `
42335
43925
  );
42336
43926
  return 0;
42337
43927
  }
43928
+ async function readApprovalRedirectState(tenant) {
43929
+ const policyPath = path.join(tenant.storage_path, "principal-policy.yaml");
43930
+ try {
43931
+ const content = await promises.readFile(policyPath, "utf-8");
43932
+ const parsed = parsePolicy(content);
43933
+ const cfg = parsed.approval_redirect;
43934
+ if (!cfg) return { enabled: false, mode: "replace" };
43935
+ return {
43936
+ enabled: !!cfg.enabled,
43937
+ mode: cfg.mode === "notify" ? "notify" : "replace"
43938
+ };
43939
+ } catch {
43940
+ return { enabled: false, mode: "replace" };
43941
+ }
43942
+ }
43943
+ function parseBoolFlag(raw) {
43944
+ if (raw === void 0) return null;
43945
+ const v = raw.toLowerCase();
43946
+ if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
43947
+ if (v === "false" || v === "no" || v === "off" || v === "0") return false;
43948
+ return null;
43949
+ }
43950
+ function findFlagValue(argv, name) {
43951
+ for (let i = 0; i < argv.length; i++) {
43952
+ const a = argv[i];
43953
+ if (a === name) {
43954
+ return argv[i + 1];
43955
+ }
43956
+ const eq = `${name}=`;
43957
+ if (a.startsWith(eq)) {
43958
+ return a.slice(eq.length);
43959
+ }
43960
+ }
43961
+ return void 0;
43962
+ }
43963
+ async function cmdConfig(argv, ctx) {
43964
+ const positional = argv.find((a) => !a.startsWith("--"));
43965
+ if (!positional) {
43966
+ ctx.err.write(
43967
+ "Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
43968
+ );
43969
+ return 2;
43970
+ }
43971
+ const tenant = await findTenant(positional, ctx.discoverOpts);
43972
+ if (!tenant) {
43973
+ ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
43974
+ `);
43975
+ return 1;
43976
+ }
43977
+ const redirectFlag = parseBoolFlag(
43978
+ findFlagValue(argv, "--approval-redirect")
43979
+ );
43980
+ const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
43981
+ if (redirectFlag === null && modeFlag === void 0) {
43982
+ ctx.err.write(
43983
+ "sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
43984
+ );
43985
+ return 2;
43986
+ }
43987
+ if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
43988
+ ctx.err.write(
43989
+ `sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
43990
+ `
43991
+ );
43992
+ return 2;
43993
+ }
43994
+ const current = await readApprovalRedirectState(tenant);
43995
+ const next = {
43996
+ enabled: redirectFlag !== null ? redirectFlag : current.enabled,
43997
+ mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
43998
+ };
43999
+ await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
44000
+ if (hasJsonFlag(argv)) {
44001
+ ctx.out.write(
44002
+ JSON.stringify(
44003
+ {
44004
+ tenant: tenant.name,
44005
+ approval_redirect: next
44006
+ },
44007
+ null,
44008
+ 2
44009
+ ) + "\n"
44010
+ );
44011
+ } else {
44012
+ ctx.out.write(
44013
+ `sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
44014
+ `
44015
+ );
44016
+ ctx.out.write(
44017
+ ` Takes effect on the next gate request for the running server.
44018
+ `
44019
+ );
44020
+ }
44021
+ return 0;
44022
+ }
44023
+ async function writeApprovalRedirectToPolicyFile(storagePath, state) {
44024
+ const policyPath = path.join(storagePath, "principal-policy.yaml");
44025
+ let content;
44026
+ try {
44027
+ content = await promises.readFile(policyPath, "utf-8");
44028
+ } catch (err) {
44029
+ const code = err?.code;
44030
+ if (code !== "ENOENT") throw err;
44031
+ content = await defaultPolicyTextForBootstrap();
44032
+ }
44033
+ const block = renderApprovalRedirectBlock(state);
44034
+ const updated = upsertApprovalRedirectBlock(content, block);
44035
+ await promises.writeFile(policyPath, updated, "utf-8");
44036
+ await promises.chmod(policyPath, 384);
44037
+ }
44038
+ function renderApprovalRedirectBlock(state) {
44039
+ return [
44040
+ "# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
44041
+ "approval_redirect:",
44042
+ ` enabled: ${state.enabled ? "true" : "false"}`,
44043
+ ` mode: ${state.mode}`
44044
+ ].join("\n");
44045
+ }
44046
+ function upsertApprovalRedirectBlock(content, block) {
44047
+ const lines = content.split("\n");
44048
+ const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
44049
+ if (startIdx === -1) {
44050
+ const trimmed = content.endsWith("\n") ? content : content + "\n";
44051
+ return trimmed + "\n" + block + "\n";
44052
+ }
44053
+ let blockStart = startIdx;
44054
+ if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
44055
+ blockStart = blockStart - 1;
44056
+ }
44057
+ let blockEnd = startIdx + 1;
44058
+ while (blockEnd < lines.length) {
44059
+ const l = lines[blockEnd];
44060
+ if (l === "") {
44061
+ blockEnd++;
44062
+ continue;
44063
+ }
44064
+ if (/^[A-Za-z0-9#]/.test(l)) {
44065
+ break;
44066
+ }
44067
+ blockEnd++;
44068
+ }
44069
+ const before = lines.slice(0, blockStart);
44070
+ const after = lines.slice(blockEnd);
44071
+ const replaced = [...before, ...block.split("\n"), ...after].join("\n");
44072
+ return replaced.endsWith("\n") ? replaced : replaced + "\n";
44073
+ }
44074
+ async function defaultPolicyTextForBootstrap() {
44075
+ return [
44076
+ "version: 1",
44077
+ "tier1_always_approve:",
44078
+ " - state_export",
44079
+ " - state_import",
44080
+ " - state_delete",
44081
+ "approval_channel:",
44082
+ " type: stderr",
44083
+ " timeout_seconds: 300",
44084
+ ""
44085
+ ].join("\n");
44086
+ }
42338
44087
  async function cmdStatus(argv, ctx) {
42339
44088
  const tenants = await discoverTenants(ctx.discoverOpts);
42340
44089
  const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
@@ -42375,6 +44124,7 @@ var init_cli5 = __esm({
42375
44124
  "src/cli/agents/cli.ts"() {
42376
44125
  init_discovery();
42377
44126
  init_health();
44127
+ init_loader();
42378
44128
  }
42379
44129
  });
42380
44130
 
@@ -43803,7 +45553,7 @@ async function main() {
43803
45553
  const code = await runIdentityCommand2({ argv: args.slice(1) });
43804
45554
  process.exit(code);
43805
45555
  }
43806
- if (args[0] === "agents") {
45556
+ if (args[0] === "agents" || args[0] === "agent") {
43807
45557
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
43808
45558
  const code = await runAgentsCommand2({ argv: args.slice(1) });
43809
45559
  process.exit(code);