@sanctuary-framework/mcp-server 1.2.3 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4254,6 +4254,10 @@ var DEFAULT_CHANNEL = {
4254
4254
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4255
4255
  // Field omitted intentionally — all channels hardcode deny on timeout.
4256
4256
  };
4257
+ var DEFAULT_APPROVAL_REDIRECT = {
4258
+ enabled: false,
4259
+ mode: "replace"
4260
+ };
4257
4261
  var DEFAULT_POLICY = {
4258
4262
  version: 1,
4259
4263
  tier1_always_approve: [
@@ -4327,6 +4331,7 @@ var DEFAULT_POLICY = {
4327
4331
  "handshake_status",
4328
4332
  "handshake_exchange",
4329
4333
  "handshake_verify_attestation",
4334
+ "handshake_abort",
4330
4335
  "reputation_query_weighted",
4331
4336
  "federation_peers",
4332
4337
  "federation_trust_evaluate",
@@ -4368,7 +4373,8 @@ var DEFAULT_POLICY = {
4368
4373
  "compliance_eu_ai_act_annex_iii_classify"
4369
4374
  // Read-only; rule-based Annex III classifier
4370
4375
  ],
4371
- approval_channel: DEFAULT_CHANNEL
4376
+ approval_channel: DEFAULT_CHANNEL,
4377
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4372
4378
  };
4373
4379
  function extractOperationName(toolName) {
4374
4380
  if (toolName.startsWith("proxy/")) {
@@ -4465,9 +4471,35 @@ function validatePolicy(raw) {
4465
4471
  };
4466
4472
  delete merged.auto_deny;
4467
4473
  return merged;
4468
- })()
4474
+ })(),
4475
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4469
4476
  };
4470
4477
  }
4478
+ function parseApprovalRedirect(raw) {
4479
+ if (raw === void 0 || raw === null) {
4480
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4481
+ }
4482
+ if (typeof raw !== "object") {
4483
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4484
+ }
4485
+ const obj = raw;
4486
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4487
+ const modeRaw = obj.mode;
4488
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4489
+ if (modeRaw !== void 0) {
4490
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4491
+ throw new Error(
4492
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4493
+ );
4494
+ }
4495
+ mode = modeRaw;
4496
+ }
4497
+ const result = { enabled, mode };
4498
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4499
+ result.per_agent = obj.per_agent;
4500
+ }
4501
+ return result;
4502
+ }
4471
4503
  function generateDefaultPolicyYaml() {
4472
4504
  return `# Sanctuary Principal Policy v1
4473
4505
  # This file controls what your agent can do without asking.
@@ -4546,6 +4578,7 @@ tier3_always_allow:
4546
4578
  - handshake_status
4547
4579
  - handshake_exchange
4548
4580
  - handshake_verify_attestation
4581
+ - handshake_abort
4549
4582
  - reputation_query_weighted
4550
4583
  - federation_peers
4551
4584
  - federation_trust_evaluate
@@ -4580,22 +4613,69 @@ tier3_always_allow:
4580
4613
  approval_channel:
4581
4614
  type: stderr
4582
4615
  timeout_seconds: 300
4616
+
4617
+ # \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
4618
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4619
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4620
+ # of (or in addition to) the configured approval_channel above.
4621
+ #
4622
+ # mode:
4623
+ # replace: bypass the approval_channel entirely; the gate awaits a
4624
+ # decision from the inbox (default once enabled).
4625
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4626
+ # wins. Right shape for harnesses that cannot fully suppress
4627
+ # their local approval prompt (e.g. Mastra-class).
4628
+ approval_redirect:
4629
+ enabled: false
4630
+ mode: replace
4583
4631
  `;
4584
4632
  }
4633
+ var MalformedPrincipalPolicyError = class extends Error {
4634
+ constructor(policyPath, reason) {
4635
+ super(
4636
+ `Principal policy at ${policyPath} is malformed and cannot be loaded.
4637
+ Reason: ${reason}
4638
+ Sanctuary refuses to substitute a default policy when an existing file is present, to avoid silently overriding operator intent. Fix the file or delete it to regenerate the default.`
4639
+ );
4640
+ this.policyPath = policyPath;
4641
+ this.reason = reason;
4642
+ this.name = "MalformedPrincipalPolicyError";
4643
+ }
4644
+ policyPath;
4645
+ reason;
4646
+ };
4585
4647
  async function loadPrincipalPolicy(storagePath) {
4586
4648
  const policyPath = join(storagePath, "principal-policy.yaml");
4649
+ let content;
4650
+ try {
4651
+ content = await readFile(policyPath, "utf-8");
4652
+ } catch (err) {
4653
+ const code = err?.code;
4654
+ if (code === "ENOENT") {
4655
+ const defaultYaml = generateDefaultPolicyYaml();
4656
+ try {
4657
+ await writeFile(policyPath, defaultYaml, "utf-8");
4658
+ await chmod(policyPath, 384);
4659
+ } catch (writeErr) {
4660
+ console.warn(
4661
+ `Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
4662
+ );
4663
+ }
4664
+ return Object.freeze({ ...DEFAULT_POLICY });
4665
+ }
4666
+ throw new MalformedPrincipalPolicyError(
4667
+ policyPath,
4668
+ `read failed: ${err.message}`
4669
+ );
4670
+ }
4587
4671
  try {
4588
- const content = await readFile(policyPath, "utf-8");
4589
4672
  const policy = parsePolicy(content);
4590
4673
  return Object.freeze(policy);
4591
- } catch {
4592
- const defaultYaml = generateDefaultPolicyYaml();
4593
- try {
4594
- await writeFile(policyPath, defaultYaml, "utf-8");
4595
- await chmod(policyPath, 384);
4596
- } catch {
4597
- }
4598
- return Object.freeze({ ...DEFAULT_POLICY });
4674
+ } catch (parseErr) {
4675
+ throw new MalformedPrincipalPolicyError(
4676
+ policyPath,
4677
+ parseErr.message
4678
+ );
4599
4679
  }
4600
4680
  }
4601
4681
 
@@ -4822,7 +4902,7 @@ function deepSortKeys(obj) {
4822
4902
  return sorted;
4823
4903
  }
4824
4904
  function canonicalizeForSigning(body) {
4825
- return JSON.stringify(deepSortKeys(body));
4905
+ return JSON.stringify(deepSortKeys(body)).normalize("NFC");
4826
4906
  }
4827
4907
 
4828
4908
  // src/shr/generator.ts
@@ -11644,6 +11724,16 @@ var HUB_ROUTES = {
11644
11724
  */
11645
11725
  CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
11646
11726
  CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
11727
+ /**
11728
+ * Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
11729
+ * scrollback, and operator-initiated thread delete. Distinct from the
11730
+ * v1.2 `/history` route, which surfaces the active in-session thread
11731
+ * shape; the new routes target persisted multi-thread memory used by
11732
+ * v1.3 conversational sovereignty depth.
11733
+ */
11734
+ CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
11735
+ CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
11736
+ CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
11647
11737
  /**
11648
11738
  * Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
11649
11739
  * recent activity feed, pending Tier 1 approvals routed through this
@@ -11667,6 +11757,10 @@ var HUB_TIER_1_AGENT_CONTROL_ACTIONS = [
11667
11757
  ];
11668
11758
  var HUB_ACTIVITY_DEFAULT_LIMIT = 50;
11669
11759
  var HUB_ACTIVITY_MAX_LIMIT = 500;
11760
+ var HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
11761
+ var HUB_CHAT_THREADS_MAX_LIMIT = 500;
11762
+ var HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
11763
+ var HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
11670
11764
  var HUB_INBOX_DEFAULT_LIMIT = 100;
11671
11765
  var HUB_INBOX_MAX_LIMIT = 500;
11672
11766
  var HUB_AGENTS_DEFAULT_LIMIT = 100;
@@ -11838,6 +11932,23 @@ function checkChatMessage(value) {
11838
11932
  }
11839
11933
  return trimmed;
11840
11934
  }
11935
+ function matchConciergeThreadRoute(path) {
11936
+ const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
11937
+ if (!path.startsWith(prefix)) return null;
11938
+ const rest = path.slice(prefix.length);
11939
+ if (rest.length === 0 || rest.includes("/")) return null;
11940
+ const decoded = decodeURIComponent(rest);
11941
+ if (decoded.length === 0) return null;
11942
+ return { threadId: decoded };
11943
+ }
11944
+ function parseSince(raw) {
11945
+ if (raw === null || raw === "") return void 0;
11946
+ const parsed = Number.parseInt(raw, 10);
11947
+ if (Number.isNaN(parsed) || parsed < 0) {
11948
+ throw new HubValidationError("since must be a non-negative integer");
11949
+ }
11950
+ return parsed;
11951
+ }
11841
11952
  function matchInboxRoute(path) {
11842
11953
  const prefix = `${HUB_API_PREFIX}/inbox/`;
11843
11954
  if (!path.startsWith(prefix)) return null;
@@ -12021,6 +12132,47 @@ async function handleHubRoute(deps, req, res) {
12021
12132
  writeJSON2(res, 200, { ok: true, data: { messages } });
12022
12133
  return true;
12023
12134
  }
12135
+ if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
12136
+ const limit = parseLimit(
12137
+ url.searchParams.get("limit"),
12138
+ HUB_CHAT_THREADS_DEFAULT_LIMIT,
12139
+ HUB_CHAT_THREADS_MAX_LIMIT
12140
+ );
12141
+ const threads = await deps.service.listConciergeMemoryThreads({ limit });
12142
+ writeJSON2(res, 200, { ok: true, data: { threads } });
12143
+ return true;
12144
+ }
12145
+ {
12146
+ const threadMatch = matchConciergeThreadRoute(path);
12147
+ if (threadMatch) {
12148
+ if (method === "GET") {
12149
+ const since = parseSince(url.searchParams.get("since"));
12150
+ const limit = parseLimit(
12151
+ url.searchParams.get("limit"),
12152
+ HUB_CHAT_TURNS_DEFAULT_LIMIT,
12153
+ HUB_CHAT_TURNS_MAX_LIMIT
12154
+ );
12155
+ const readOpts = { limit };
12156
+ if (since !== void 0) readOpts.sinceTurnId = since;
12157
+ const turns = await deps.service.readConciergeMemoryThread(
12158
+ threadMatch.threadId,
12159
+ readOpts
12160
+ );
12161
+ writeJSON2(res, 200, { ok: true, data: { turns } });
12162
+ return true;
12163
+ }
12164
+ if (method === "DELETE") {
12165
+ const removed = await deps.service.deleteConciergeMemoryThread(
12166
+ threadMatch.threadId
12167
+ );
12168
+ writeJSON2(res, removed ? 200 : 404, {
12169
+ ok: removed,
12170
+ data: { thread_id: threadMatch.threadId, removed }
12171
+ });
12172
+ return true;
12173
+ }
12174
+ }
12175
+ }
12024
12176
  writeJSON2(res, 404, { ok: false, error: "not_found", path });
12025
12177
  return true;
12026
12178
  } catch (err) {
@@ -16070,6 +16222,162 @@ async function dispatchV11Request(inputs, req, res, url, method) {
16070
16222
  return false;
16071
16223
  }
16072
16224
 
16225
+ // src/principal-policy/approval-aggregator-routes.ts
16226
+ var APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
16227
+ var APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
16228
+ var APPROVAL_INBOX_DEFAULT_LIMIT = 50;
16229
+ var APPROVAL_INBOX_MAX_LIMIT = 200;
16230
+ function writeJSON4(res, status, payload) {
16231
+ res.writeHead(status, {
16232
+ "Content-Type": "application/json",
16233
+ "Cache-Control": "no-store"
16234
+ });
16235
+ res.end(JSON.stringify(payload));
16236
+ }
16237
+ function parseLimit2(raw, defaultValue, max) {
16238
+ if (raw === null || raw === "") return defaultValue;
16239
+ const parsed = Number.parseInt(raw, 10);
16240
+ if (Number.isNaN(parsed) || parsed < 0) {
16241
+ return defaultValue;
16242
+ }
16243
+ return Math.min(parsed, max);
16244
+ }
16245
+ function isStatusFilter(value) {
16246
+ return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
16247
+ }
16248
+ function matchEntryRoute(path) {
16249
+ const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
16250
+ if (!path.startsWith(prefix)) return null;
16251
+ const rest = path.slice(prefix.length);
16252
+ if (rest.length === 0) return null;
16253
+ const slash = rest.indexOf("/");
16254
+ if (slash === -1) {
16255
+ return { aggregatorId: decodeURIComponent(rest), action: null };
16256
+ }
16257
+ return {
16258
+ aggregatorId: decodeURIComponent(rest.slice(0, slash)),
16259
+ action: rest.slice(slash + 1)
16260
+ };
16261
+ }
16262
+ async function handleStream2(deps, res) {
16263
+ res.writeHead(200, {
16264
+ "Content-Type": "text/event-stream",
16265
+ "Cache-Control": "no-cache, no-transform",
16266
+ Connection: "keep-alive",
16267
+ "X-Accel-Buffering": "no"
16268
+ });
16269
+ const initial = await deps.aggregator.list({ status: "pending" });
16270
+ res.write(
16271
+ `event: approval_inbox_snapshot
16272
+ data: ${JSON.stringify({ entries: initial })}
16273
+
16274
+ `
16275
+ );
16276
+ const unsubscribe = deps.aggregator.onEvent((event) => {
16277
+ try {
16278
+ res.write(
16279
+ `event: approval_inbox_${event.type}
16280
+ data: ${JSON.stringify(event.entry)}
16281
+
16282
+ `
16283
+ );
16284
+ } catch {
16285
+ }
16286
+ });
16287
+ const keepAlive = setInterval(() => {
16288
+ try {
16289
+ res.write(": keepalive\n\n");
16290
+ } catch {
16291
+ }
16292
+ }, 25e3);
16293
+ const cleanup = () => {
16294
+ clearInterval(keepAlive);
16295
+ unsubscribe();
16296
+ };
16297
+ res.on("close", cleanup);
16298
+ res.on("error", cleanup);
16299
+ }
16300
+ async function handleApprovalInboxRoute(deps, req, res) {
16301
+ const host = req.headers.host || "localhost";
16302
+ const url = new URL(req.url ?? "/", `http://${host}`);
16303
+ const method = (req.method ?? "GET").toUpperCase();
16304
+ const path = url.pathname;
16305
+ if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
16306
+ return false;
16307
+ }
16308
+ const checkAuth = authMiddleware(deps.authConfig);
16309
+ if (!checkAuth(req, res, url)) return true;
16310
+ try {
16311
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
16312
+ await handleStream2(deps, res);
16313
+ return true;
16314
+ }
16315
+ if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
16316
+ const limit = parseLimit2(
16317
+ url.searchParams.get("limit"),
16318
+ APPROVAL_INBOX_DEFAULT_LIMIT,
16319
+ APPROVAL_INBOX_MAX_LIMIT
16320
+ );
16321
+ const statusRaw = url.searchParams.get("status");
16322
+ const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
16323
+ const sinceTs = url.searchParams.get("since") ?? void 0;
16324
+ const entries = await deps.aggregator.list({
16325
+ status,
16326
+ limit,
16327
+ ...sinceTs !== void 0 ? { sinceTs } : {}
16328
+ });
16329
+ writeJSON4(res, 200, { ok: true, data: { entries } });
16330
+ return true;
16331
+ }
16332
+ const entryMatch = matchEntryRoute(path);
16333
+ if (entryMatch === null) {
16334
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
16335
+ return true;
16336
+ }
16337
+ if (method === "GET" && entryMatch.action === null) {
16338
+ const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
16339
+ const entry = entries.find(
16340
+ (e) => e.aggregator_id === entryMatch.aggregatorId
16341
+ );
16342
+ if (!entry) {
16343
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16344
+ return true;
16345
+ }
16346
+ const payload = await deps.aggregator.getFullPayload(
16347
+ entryMatch.aggregatorId
16348
+ );
16349
+ writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
16350
+ return true;
16351
+ }
16352
+ if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
16353
+ const decision = entryMatch.action === "approve" ? "approved" : "denied";
16354
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
16355
+ try {
16356
+ const entry = await deps.aggregator.resolve(
16357
+ entryMatch.aggregatorId,
16358
+ decision,
16359
+ operatorId
16360
+ );
16361
+ writeJSON4(res, 200, { ok: true, data: { entry } });
16362
+ } catch (err) {
16363
+ const msg = err instanceof Error ? err.message : String(err);
16364
+ if (msg === "approval-aggregator: not_found") {
16365
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
16366
+ } else {
16367
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
16368
+ }
16369
+ }
16370
+ return true;
16371
+ }
16372
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
16373
+ return true;
16374
+ } catch (err) {
16375
+ const msg = err instanceof Error ? err.message : String(err);
16376
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
16377
+ return true;
16378
+ }
16379
+ }
16380
+
16073
16381
  // src/principal-policy/dashboard.ts
16074
16382
  var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
16075
16383
  var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
@@ -16134,6 +16442,14 @@ var DashboardApprovalChannel = class {
16134
16442
  * regardless. Default route flip is deferred to v1.2.
16135
16443
  */
16136
16444
  v11Bindings = null;
16445
+ /**
16446
+ * v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
16447
+ * additively at `/api/approval-inbox/*` when set. Legacy approval
16448
+ * routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
16449
+ * aggregator is a passive subscriber to the gate; the routes here are
16450
+ * the operator-facing query / decision surface.
16451
+ */
16452
+ approvalAggregator = null;
16137
16453
  constructor(config) {
16138
16454
  this.config = config;
16139
16455
  this.authToken = config.auth_token;
@@ -16184,6 +16500,34 @@ var DashboardApprovalChannel = class {
16184
16500
  setV11Bindings(bindings) {
16185
16501
  this.v11Bindings = bindings;
16186
16502
  }
16503
+ /**
16504
+ * v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
16505
+ * aggregator. Once set, requests to `/api/approval-inbox/*` route
16506
+ * through `handleApprovalInboxRoute`. Pass `null` to detach (used by
16507
+ * tests + during shutdown).
16508
+ */
16509
+ setApprovalAggregator(aggregator) {
16510
+ this.approvalAggregator = aggregator;
16511
+ }
16512
+ /**
16513
+ * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
16514
+ * before the legacy approval route table. Returns true when served.
16515
+ */
16516
+ async dispatchApprovalInbox(req, res) {
16517
+ if (!this.approvalAggregator) return false;
16518
+ return handleApprovalInboxRoute(
16519
+ {
16520
+ authConfig: {
16521
+ loopbackAutoAuth: this._autoAuthLocalhost,
16522
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
16523
+ },
16524
+ aggregator: this.approvalAggregator,
16525
+ operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
16526
+ },
16527
+ req,
16528
+ res
16529
+ );
16530
+ }
16187
16531
  /**
16188
16532
  * v1.1 dispatch entry point. Called from `handleRequest` before the
16189
16533
  * legacy route table. Returns true when the request was served by v1.1
@@ -16559,6 +16903,18 @@ var DashboardApprovalChannel = class {
16559
16903
  res.end();
16560
16904
  return;
16561
16905
  }
16906
+ if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
16907
+ this.dispatchApprovalInbox(req, res).then((handled) => {
16908
+ if (handled) return;
16909
+ this.handleLegacyRequest(req, res, url, method);
16910
+ }).catch(() => {
16911
+ if (!res.headersSent) {
16912
+ res.writeHead(500, { "Content-Type": "application/json" });
16913
+ res.end(JSON.stringify({ error: "Internal server error" }));
16914
+ }
16915
+ });
16916
+ return;
16917
+ }
16562
16918
  if (this.v11Bindings) {
16563
16919
  this.dispatchV11(req, res, url, method).then((handled) => {
16564
16920
  if (handled) return;
@@ -18577,14 +18933,25 @@ var ApprovalGate = class {
18577
18933
  auditLog;
18578
18934
  injectionDetector;
18579
18935
  onInjectionAlert;
18936
+ onApprovalEvent;
18580
18937
  proxyTierResolver;
18581
- constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
18938
+ constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
18582
18939
  this.policy = policy;
18583
18940
  this.baseline = baseline;
18584
18941
  this.channel = channel;
18585
18942
  this.auditLog = auditLog;
18586
18943
  this.injectionDetector = injectionDetector ?? new InjectionDetector();
18587
18944
  this.onInjectionAlert = onInjectionAlert;
18945
+ this.onApprovalEvent = onApprovalEvent;
18946
+ }
18947
+ /**
18948
+ * Set the approval-event callback after construction. Used by the
18949
+ * Upsilon-1 wire-up when the aggregator is constructed alongside the
18950
+ * gate. The aggregator subscribes through this setter rather than the
18951
+ * constructor so existing call sites continue to work unchanged.
18952
+ */
18953
+ setApprovalEventCallback(cb) {
18954
+ this.onApprovalEvent = cb;
18588
18955
  }
18589
18956
  /**
18590
18957
  * Set the proxy tier resolver. Called after the proxy router is initialized.
@@ -18818,21 +19185,105 @@ var ApprovalGate = class {
18818
19185
  }
18819
19186
  /**
18820
19187
  * Request approval from the human principal.
19188
+ *
19189
+ * Fail-closed contract (full-sweep #49): if the channel throws (network
19190
+ * down, callback unreachable, dashboard SSE peer dropped, webhook DNS
19191
+ * failure, etc.), the gate denies the operation and audit-logs the cause.
19192
+ * Channel-internal timeouts already resolve with decision: "deny" per
19193
+ * SEC-002; this catch covers the remaining "channel raised" path so an
19194
+ * unhandled rejection cannot turn into an indeterminate state at the gate.
18821
19195
  */
18822
19196
  async requestApproval(operation, tier, reason, context) {
19197
+ const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
18823
19198
  const request = {
18824
19199
  operation,
18825
19200
  tier,
18826
19201
  reason,
18827
19202
  context,
18828
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
19203
+ timestamp: requestTimestamp
18829
19204
  };
18830
- const response = await this.channel.requestApproval(request);
19205
+ const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
19206
+ if (this.onApprovalEvent) {
19207
+ try {
19208
+ this.onApprovalEvent({
19209
+ phase: "requested",
19210
+ operation,
19211
+ tier,
19212
+ reason,
19213
+ context,
19214
+ request_timestamp: requestTimestamp,
19215
+ correlation_id: correlationId
19216
+ });
19217
+ } catch {
19218
+ }
19219
+ }
19220
+ let response;
19221
+ try {
19222
+ response = await this.channel.requestApproval(request);
19223
+ } catch (err) {
19224
+ const errMessage = err instanceof Error ? err.message : String(err);
19225
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
19226
+ this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
19227
+ tier,
19228
+ reason,
19229
+ decided_by: "channel_failure",
19230
+ channel_error: errMessage
19231
+ });
19232
+ if (this.onApprovalEvent) {
19233
+ try {
19234
+ this.onApprovalEvent({
19235
+ phase: "resolved",
19236
+ operation,
19237
+ tier,
19238
+ reason,
19239
+ context,
19240
+ request_timestamp: requestTimestamp,
19241
+ resolution: {
19242
+ decision: "deny",
19243
+ decided_at: decidedAt,
19244
+ decided_by: "channel_failure"
19245
+ },
19246
+ correlation_id: correlationId
19247
+ });
19248
+ } catch {
19249
+ }
19250
+ }
19251
+ return {
19252
+ allowed: false,
19253
+ tier,
19254
+ reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
19255
+ approval_required: true,
19256
+ approval_response: {
19257
+ decision: "deny",
19258
+ decided_at: decidedAt,
19259
+ decided_by: "channel_failure"
19260
+ }
19261
+ };
19262
+ }
18831
19263
  this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
18832
19264
  tier,
18833
19265
  reason,
18834
19266
  decided_by: response.decided_by
18835
19267
  });
19268
+ if (this.onApprovalEvent) {
19269
+ try {
19270
+ this.onApprovalEvent({
19271
+ phase: "resolved",
19272
+ operation,
19273
+ tier,
19274
+ reason,
19275
+ context,
19276
+ request_timestamp: requestTimestamp,
19277
+ resolution: {
19278
+ decision: response.decision,
19279
+ decided_at: response.decided_at,
19280
+ decided_by: response.decided_by
19281
+ },
19282
+ correlation_id: correlationId
19283
+ });
19284
+ } catch {
19285
+ }
19286
+ }
18836
19287
  return {
18837
19288
  allowed: response.decision === "approve",
18838
19289
  tier,
@@ -18866,50 +19317,559 @@ var ApprovalGate = class {
18866
19317
  }
18867
19318
  };
18868
19319
 
18869
- // src/principal-policy/tools.ts
18870
- function createPrincipalPolicyTools(policy, baseline, auditLog) {
18871
- return [
18872
- {
18873
- name: "principal_policy_view",
18874
- description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
18875
- inputSchema: {
18876
- type: "object",
18877
- properties: {
18878
- include_defaults: {
18879
- type: "boolean",
18880
- description: "Include tier3_always_allow list (can be long)",
18881
- default: false
18882
- }
18883
- }
18884
- },
18885
- handler: async (args) => {
18886
- const includeDefaults = args.include_defaults ?? false;
18887
- const view = {
18888
- version: policy.version,
18889
- tier1_always_approve: policy.tier1_always_approve,
18890
- tier2_anomaly: policy.tier2_anomaly,
18891
- approval_channel: {
18892
- type: policy.approval_channel.type,
18893
- timeout_seconds: policy.approval_channel.timeout_seconds,
18894
- auto_deny: true
18895
- // SEC-002: hardcoded, not configurable
18896
- }
18897
- };
18898
- if (includeDefaults) {
18899
- view.tier3_always_allow = policy.tier3_always_allow;
18900
- } else {
18901
- view.tier3_always_allow_count = policy.tier3_always_allow.length;
18902
- view.note = "Pass include_defaults: true to see the full tier3_always_allow list";
18903
- }
18904
- auditLog.append("l2", "principal_policy_view", "system", {
18905
- include_defaults: includeDefaults
18906
- });
18907
- return toolResult(view);
18908
- }
18909
- },
18910
- {
18911
- name: "principal_baseline_view",
18912
- description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
19320
+ // src/principal-policy/approval-aggregator.ts
19321
+ init_encryption();
19322
+ init_encoding();
19323
+ var APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
19324
+ var APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
19325
+ var APPROVAL_AGGREGATOR_AUDIT_OPS = {
19326
+ AGGREGATED: "cross_harness_approval_aggregated",
19327
+ RESOLVED: "cross_harness_approval_resolved",
19328
+ DEDUPED: "cross_harness_approval_deduped"
19329
+ };
19330
+ var DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
19331
+ var DEFAULT_MAX_LIST_LIMIT = 200;
19332
+ var DEFAULT_LIST_PAGE_SIZE = 50;
19333
+ var ApprovalAggregator = class {
19334
+ storage;
19335
+ encryptionKey;
19336
+ auditLog;
19337
+ identityId;
19338
+ fortressId;
19339
+ pendingTtlMs;
19340
+ maxListLimit;
19341
+ now;
19342
+ resolveSourceContext;
19343
+ resolveHubInboxItemId;
19344
+ /** Cached entries by `aggregator_id`. */
19345
+ entries = /* @__PURE__ */ new Map();
19346
+ /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
19347
+ dedupIndex = /* @__PURE__ */ new Map();
19348
+ /** Correlation index: gate `correlation_id` -> aggregator_id. */
19349
+ correlationIndex = /* @__PURE__ */ new Map();
19350
+ /** Original request payloads kept in-memory for `getFullPayload()`. */
19351
+ fullPayloads = /* @__PURE__ */ new Map();
19352
+ /** Has the aggregator hydrated persisted entries on this process? */
19353
+ hydrated = false;
19354
+ /** Active SSE listeners. */
19355
+ listeners = /* @__PURE__ */ new Set();
19356
+ constructor(deps) {
19357
+ this.storage = deps.storage;
19358
+ this.encryptionKey = derivePurposeKey(
19359
+ deps.masterKey,
19360
+ APPROVAL_AGGREGATOR_HKDF_INFO
19361
+ );
19362
+ this.auditLog = deps.auditLog;
19363
+ this.identityId = deps.identityId;
19364
+ this.fortressId = deps.fortressId;
19365
+ this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
19366
+ this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
19367
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
19368
+ this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
19369
+ source_harness: this.fortressId,
19370
+ source_agent_id: this.fortressId
19371
+ }));
19372
+ this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
19373
+ }
19374
+ /**
19375
+ * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
19376
+ * use this to forward aggregator emissions to the dashboard.
19377
+ */
19378
+ onEvent(listener) {
19379
+ this.listeners.add(listener);
19380
+ return () => this.listeners.delete(listener);
19381
+ }
19382
+ /**
19383
+ * Ingest a gate event. Returns the aggregator entry on first sight,
19384
+ * `null` when deduped. Resolution events update the existing record;
19385
+ * unmatched resolutions are dropped silently (caller's gate emitted a
19386
+ * resolved-without-requested pair, which the aggregator does not invent
19387
+ * a record for).
19388
+ */
19389
+ async ingest(event) {
19390
+ await this.hydrate();
19391
+ if (event.phase === "requested") {
19392
+ return this.ingestRequested(event);
19393
+ }
19394
+ if (event.phase === "resolved") {
19395
+ return this.ingestResolved(event);
19396
+ }
19397
+ return null;
19398
+ }
19399
+ /**
19400
+ * List pending or recently resolved entries. Pending entries past TTL
19401
+ * are lazily transitioned to `expired` and persisted before the list
19402
+ * snapshot is returned.
19403
+ */
19404
+ async list(opts) {
19405
+ await this.hydrate();
19406
+ await this.expireStale();
19407
+ const limit = Math.min(
19408
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
19409
+ this.maxListLimit
19410
+ );
19411
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
19412
+ const matching = [];
19413
+ for (const entry of this.entries.values()) {
19414
+ if (opts?.status && entry.status !== opts.status) continue;
19415
+ if (Date.parse(entry.created_at) < sinceMs) continue;
19416
+ matching.push(entry);
19417
+ }
19418
+ matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
19419
+ return matching.slice(0, limit);
19420
+ }
19421
+ /**
19422
+ * Return the original (unhashed) request payload for the entry. Returns
19423
+ * `null` when the entry is unknown or the payload was evicted (e.g. the
19424
+ * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
19425
+ */
19426
+ async getFullPayload(aggregatorId) {
19427
+ await this.hydrate();
19428
+ if (!this.entries.has(aggregatorId)) return null;
19429
+ return this.fullPayloads.get(aggregatorId) ?? null;
19430
+ }
19431
+ /**
19432
+ * Resolve an entry. Used by both:
19433
+ * 1. The gate wire-up on channel-decision return.
19434
+ * 2. The HTTP `approve`/`deny` routes when an operator clicks.
19435
+ *
19436
+ * Idempotent: resolving an already-resolved entry is a no-op (the record
19437
+ * keeps its first decision and the audit log is not double-fired).
19438
+ * Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
19439
+ * routes return 404.
19440
+ */
19441
+ async resolve(aggregatorId, decision, operatorId) {
19442
+ await this.hydrate();
19443
+ const entry = this.entries.get(aggregatorId);
19444
+ if (!entry) {
19445
+ throw new Error("approval-aggregator: not_found");
19446
+ }
19447
+ if (entry.status !== "pending") {
19448
+ return entry;
19449
+ }
19450
+ entry.status = decision;
19451
+ entry.resolved_at = this.now().toISOString();
19452
+ entry.resolved_by = operatorId;
19453
+ await this.persist(entry);
19454
+ this.auditLog.append(
19455
+ "l2",
19456
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
19457
+ this.identityId,
19458
+ {
19459
+ aggregator_id: entry.aggregator_id,
19460
+ source_harness: entry.source_harness,
19461
+ source_agent_id: entry.source_agent_id,
19462
+ audit_log_entry_id: entry.audit_log_entry_id,
19463
+ policy_rule_id: entry.policy_rule_id,
19464
+ decision,
19465
+ decided_by: operatorId,
19466
+ decided_at: entry.resolved_at
19467
+ }
19468
+ );
19469
+ this.emit({ type: "resolved", entry: { ...entry } });
19470
+ return entry;
19471
+ }
19472
+ // ── Internal: ingest paths ─────────────────────────────────────────────
19473
+ async ingestRequested(event) {
19474
+ const ctx = this.resolveSourceContext(event);
19475
+ const auditId = this.auditEntryIdForEvent(event);
19476
+ const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
19477
+ const existing = this.dedupIndex.get(dedupKey);
19478
+ if (existing) {
19479
+ const existingEntry = this.entries.get(existing);
19480
+ if (existingEntry) {
19481
+ this.correlationIndex.set(event.correlation_id, existing);
19482
+ this.auditLog.append(
19483
+ "l2",
19484
+ APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
19485
+ this.identityId,
19486
+ {
19487
+ aggregator_id: existing,
19488
+ source_harness: ctx.source_harness,
19489
+ source_agent_id: ctx.source_agent_id,
19490
+ audit_log_entry_id: auditId,
19491
+ policy_rule_id: this.derivePolicyRuleId(event),
19492
+ correlation_id: event.correlation_id
19493
+ }
19494
+ );
19495
+ this.emit({ type: "deduped", entry: { ...existingEntry } });
19496
+ return null;
19497
+ }
19498
+ }
19499
+ const id = randomUUID();
19500
+ const now = this.now();
19501
+ const expires = new Date(now.getTime() + this.pendingTtlMs);
19502
+ const hubInboxId = this.resolveHubInboxItemId(event);
19503
+ const entry = {
19504
+ aggregator_id: id,
19505
+ source_harness: ctx.source_harness,
19506
+ source_agent_id: ctx.source_agent_id,
19507
+ audit_log_entry_id: auditId,
19508
+ policy_rule_id: this.derivePolicyRuleId(event),
19509
+ action_summary: this.deriveActionSummary(event),
19510
+ request_payload_hash: this.hashPayload(event.context),
19511
+ status: "pending",
19512
+ created_at: now.toISOString(),
19513
+ expires_at: expires.toISOString(),
19514
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
19515
+ };
19516
+ this.entries.set(id, entry);
19517
+ this.dedupIndex.set(dedupKey, id);
19518
+ this.correlationIndex.set(event.correlation_id, id);
19519
+ this.fullPayloads.set(id, event.context);
19520
+ await this.persist(entry);
19521
+ this.auditLog.append(
19522
+ "l2",
19523
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
19524
+ this.identityId,
19525
+ {
19526
+ aggregator_id: id,
19527
+ source_harness: ctx.source_harness,
19528
+ source_agent_id: ctx.source_agent_id,
19529
+ audit_log_entry_id: auditId,
19530
+ policy_rule_id: entry.policy_rule_id,
19531
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
19532
+ }
19533
+ );
19534
+ this.emit({ type: "aggregated", entry: { ...entry } });
19535
+ return entry;
19536
+ }
19537
+ async ingestResolved(event) {
19538
+ const id = this.correlationIndex.get(event.correlation_id);
19539
+ if (!id) return null;
19540
+ const entry = this.entries.get(id);
19541
+ if (!entry) return null;
19542
+ if (entry.status !== "pending") return entry;
19543
+ if (!event.resolution) return entry;
19544
+ const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
19545
+ const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
19546
+ entry.status = status;
19547
+ entry.resolved_at = event.resolution.decided_at;
19548
+ entry.resolved_by = event.resolution.decided_by;
19549
+ await this.persist(entry);
19550
+ this.auditLog.append(
19551
+ "l2",
19552
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
19553
+ this.identityId,
19554
+ {
19555
+ aggregator_id: id,
19556
+ source_harness: entry.source_harness,
19557
+ source_agent_id: entry.source_agent_id,
19558
+ audit_log_entry_id: entry.audit_log_entry_id,
19559
+ policy_rule_id: entry.policy_rule_id,
19560
+ decision: status,
19561
+ decided_by: entry.resolved_by,
19562
+ decided_at: entry.resolved_at,
19563
+ fail_closed: failClosed
19564
+ }
19565
+ );
19566
+ this.emit({ type: "resolved", entry: { ...entry } });
19567
+ return entry;
19568
+ }
19569
+ // ── Internal: helpers ──────────────────────────────────────────────────
19570
+ /**
19571
+ * Audit-log entry id for the dedup tuple. The audit log itself does not
19572
+ * surface a stable per-entry id (counter-prefixed keys are internal); the
19573
+ * aggregator uses the request timestamp + operation, which together pin
19574
+ * the audit entry the gate appended on the same call.
19575
+ */
19576
+ auditEntryIdForEvent(event) {
19577
+ return `${event.request_timestamp}:${event.operation}`;
19578
+ }
19579
+ derivePolicyRuleId(event) {
19580
+ return `tier${event.tier}:${event.operation}`;
19581
+ }
19582
+ deriveActionSummary(event) {
19583
+ return `${event.operation} (tier ${event.tier})`;
19584
+ }
19585
+ /**
19586
+ * Canonical SHA-256 of the request context. Sorted-keys serialization so
19587
+ * identical payloads always hash the same, even when key insertion order
19588
+ * varies. Defends against payload-replay smuggling (the aggregator can
19589
+ * tell the same payload was seen twice without storing it cleartext).
19590
+ */
19591
+ hashPayload(payload) {
19592
+ const canonical = JSON.stringify(payload, Object.keys(payload).sort());
19593
+ return createHash("sha256").update(canonical).digest("hex");
19594
+ }
19595
+ emit(event) {
19596
+ for (const listener of this.listeners) {
19597
+ try {
19598
+ listener(event);
19599
+ } catch {
19600
+ }
19601
+ }
19602
+ }
19603
+ async expireStale() {
19604
+ const nowMs = this.now().getTime();
19605
+ for (const entry of this.entries.values()) {
19606
+ if (entry.status !== "pending") continue;
19607
+ if (Date.parse(entry.expires_at) > nowMs) continue;
19608
+ entry.status = "expired";
19609
+ entry.resolved_at = this.now().toISOString();
19610
+ entry.resolved_by = "system_ttl";
19611
+ await this.persist(entry);
19612
+ this.auditLog.append(
19613
+ "l2",
19614
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
19615
+ this.identityId,
19616
+ {
19617
+ aggregator_id: entry.aggregator_id,
19618
+ source_harness: entry.source_harness,
19619
+ source_agent_id: entry.source_agent_id,
19620
+ audit_log_entry_id: entry.audit_log_entry_id,
19621
+ policy_rule_id: entry.policy_rule_id,
19622
+ decision: "expired",
19623
+ decided_by: "system_ttl",
19624
+ decided_at: entry.resolved_at
19625
+ }
19626
+ );
19627
+ this.emit({ type: "resolved", entry: { ...entry } });
19628
+ }
19629
+ }
19630
+ async persist(entry) {
19631
+ const serialized = stringToBytes(JSON.stringify(entry));
19632
+ const encrypted = encrypt(serialized, this.encryptionKey);
19633
+ await this.storage.write(
19634
+ APPROVAL_AGGREGATOR_NAMESPACE,
19635
+ entry.aggregator_id,
19636
+ stringToBytes(JSON.stringify(encrypted))
19637
+ );
19638
+ }
19639
+ async hydrate() {
19640
+ if (this.hydrated) return;
19641
+ this.hydrated = true;
19642
+ try {
19643
+ const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
19644
+ for (const meta of metas) {
19645
+ const raw = await this.storage.read(
19646
+ APPROVAL_AGGREGATOR_NAMESPACE,
19647
+ meta.key
19648
+ );
19649
+ if (!raw) continue;
19650
+ try {
19651
+ const encrypted = JSON.parse(bytesToString(raw));
19652
+ const decrypted = decrypt(encrypted, this.encryptionKey);
19653
+ const entry = JSON.parse(bytesToString(decrypted));
19654
+ this.entries.set(entry.aggregator_id, entry);
19655
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
19656
+ this.dedupIndex.set(dedupKey, entry.aggregator_id);
19657
+ } catch {
19658
+ }
19659
+ }
19660
+ } catch {
19661
+ this.hydrated = false;
19662
+ }
19663
+ }
19664
+ };
19665
+
19666
+ // src/principal-policy/channels/aggregator-backed-channel.ts
19667
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
19668
+ function auditEntryIdFor(request) {
19669
+ return `${request.timestamp}:${request.operation}`;
19670
+ }
19671
+ function statusToDecision(entry) {
19672
+ switch (entry.status) {
19673
+ case "approved":
19674
+ return {
19675
+ decision: "approve",
19676
+ decided_by: "human"
19677
+ };
19678
+ case "denied":
19679
+ return {
19680
+ decision: "deny",
19681
+ decided_by: "human"
19682
+ };
19683
+ case "timeout":
19684
+ case "expired":
19685
+ return {
19686
+ decision: "deny",
19687
+ decided_by: "timeout"
19688
+ };
19689
+ default:
19690
+ return null;
19691
+ }
19692
+ }
19693
+ var AggregatorBackedChannel = class {
19694
+ underlying;
19695
+ aggregator;
19696
+ resolveRedirect;
19697
+ replaceModeTimeoutMs;
19698
+ now;
19699
+ constructor(opts) {
19700
+ this.underlying = opts.underlying;
19701
+ this.aggregator = opts.aggregator;
19702
+ this.resolveRedirect = opts.resolveRedirect;
19703
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
19704
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
19705
+ }
19706
+ /** Expose underlying for tests / wire-up reuse. */
19707
+ getUnderlying() {
19708
+ return this.underlying;
19709
+ }
19710
+ async requestApproval(request) {
19711
+ const cfg = this.resolveRedirect(request);
19712
+ if (!cfg.enabled) {
19713
+ return this.underlying.requestApproval(request);
19714
+ }
19715
+ if (cfg.mode === "replace") {
19716
+ return this.awaitAggregatorDecision(request);
19717
+ }
19718
+ return this.notifyMode(request);
19719
+ }
19720
+ /**
19721
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
19722
+ * checking already-stored entries (avoids a race where the entry resolves
19723
+ * between list and subscribe). Match incoming events to this request by
19724
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
19725
+ */
19726
+ async awaitAggregatorDecision(request) {
19727
+ const auditId = auditEntryIdFor(request);
19728
+ return new Promise((resolveOuter) => {
19729
+ let settled = false;
19730
+ let unsubscribe = null;
19731
+ let timeoutHandle = null;
19732
+ const settle = (response) => {
19733
+ if (settled) return;
19734
+ settled = true;
19735
+ if (timeoutHandle) clearTimeout(timeoutHandle);
19736
+ if (unsubscribe) {
19737
+ try {
19738
+ unsubscribe();
19739
+ } catch {
19740
+ }
19741
+ }
19742
+ resolveOuter(response);
19743
+ };
19744
+ const onEvent = (emit) => {
19745
+ if (emit.type !== "resolved") return;
19746
+ if (emit.entry.audit_log_entry_id !== auditId) return;
19747
+ const mapped = statusToDecision(emit.entry);
19748
+ if (!mapped) return;
19749
+ settle({
19750
+ decision: mapped.decision,
19751
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
19752
+ decided_by: mapped.decided_by
19753
+ });
19754
+ };
19755
+ try {
19756
+ unsubscribe = this.aggregator.onEvent(onEvent);
19757
+ } catch (err) {
19758
+ settle({
19759
+ decision: "deny",
19760
+ decided_at: this.now().toISOString(),
19761
+ decided_by: "channel_failure"
19762
+ });
19763
+ throw err instanceof Error ? err : new Error(String(err));
19764
+ }
19765
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
19766
+ for (const entry of entries) {
19767
+ if (entry.audit_log_entry_id !== auditId) continue;
19768
+ const mapped = statusToDecision(entry);
19769
+ if (!mapped) return;
19770
+ settle({
19771
+ decision: mapped.decision,
19772
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
19773
+ decided_by: mapped.decided_by
19774
+ });
19775
+ return;
19776
+ }
19777
+ }).catch(() => {
19778
+ });
19779
+ timeoutHandle = setTimeout(() => {
19780
+ settle({
19781
+ decision: "deny",
19782
+ decided_at: this.now().toISOString(),
19783
+ decided_by: "timeout"
19784
+ });
19785
+ }, this.replaceModeTimeoutMs);
19786
+ });
19787
+ }
19788
+ /**
19789
+ * `notify` mode. Fire the underlying channel and listen on the
19790
+ * aggregator simultaneously; whichever resolves first wins. Both
19791
+ * paths produce identical `ApprovalResponse` shapes; the gate's
19792
+ * downstream audit logging is unchanged.
19793
+ *
19794
+ * On underlying-channel failure, fall through to the aggregator wait
19795
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
19796
+ * resolve from the inbox even if the dashboard/webhook is down.
19797
+ */
19798
+ async notifyMode(request) {
19799
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
19800
+ let underlyingPromise;
19801
+ try {
19802
+ underlyingPromise = this.underlying.requestApproval(request);
19803
+ } catch (err) {
19804
+ const response = await aggregatorPromise;
19805
+ return response;
19806
+ }
19807
+ return Promise.race([
19808
+ aggregatorPromise,
19809
+ underlyingPromise.catch(
19810
+ () => new Promise(() => {
19811
+ })
19812
+ )
19813
+ ]);
19814
+ }
19815
+ };
19816
+ function makeRedirectResolverFromPolicySupplier(supplier) {
19817
+ return (_request) => {
19818
+ const cfg = supplier().approval_redirect;
19819
+ if (!cfg || cfg.enabled !== true) {
19820
+ return { enabled: false, mode: "replace" };
19821
+ }
19822
+ return {
19823
+ enabled: true,
19824
+ mode: cfg.mode === "notify" ? "notify" : "replace"
19825
+ };
19826
+ };
19827
+ }
19828
+
19829
+ // src/principal-policy/tools.ts
19830
+ function createPrincipalPolicyTools(policy, baseline, auditLog) {
19831
+ return [
19832
+ {
19833
+ name: "principal_policy_view",
19834
+ description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
19835
+ inputSchema: {
19836
+ type: "object",
19837
+ properties: {
19838
+ include_defaults: {
19839
+ type: "boolean",
19840
+ description: "Include tier3_always_allow list (can be long)",
19841
+ default: false
19842
+ }
19843
+ }
19844
+ },
19845
+ handler: async (args) => {
19846
+ const includeDefaults = args.include_defaults ?? false;
19847
+ const view = {
19848
+ version: policy.version,
19849
+ tier1_always_approve: policy.tier1_always_approve,
19850
+ tier2_anomaly: policy.tier2_anomaly,
19851
+ approval_channel: {
19852
+ type: policy.approval_channel.type,
19853
+ timeout_seconds: policy.approval_channel.timeout_seconds,
19854
+ auto_deny: true
19855
+ // SEC-002: hardcoded, not configurable
19856
+ }
19857
+ };
19858
+ if (includeDefaults) {
19859
+ view.tier3_always_allow = policy.tier3_always_allow;
19860
+ } else {
19861
+ view.tier3_always_allow_count = policy.tier3_always_allow.length;
19862
+ view.note = "Pass include_defaults: true to see the full tier3_always_allow list";
19863
+ }
19864
+ auditLog.append("l2", "principal_policy_view", "system", {
19865
+ include_defaults: includeDefaults
19866
+ });
19867
+ return toolResult(view);
19868
+ }
19869
+ },
19870
+ {
19871
+ name: "principal_baseline_view",
19872
+ description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
18913
19873
  inputSchema: {
18914
19874
  type: "object",
18915
19875
  properties: {}
@@ -19736,6 +20696,71 @@ function verifyAttestation(attestation, now) {
19736
20696
  };
19737
20697
  }
19738
20698
 
20699
+ // src/handshake/audit.ts
20700
+ var HANDSHAKE_LIFECYCLE_OPS = {
20701
+ INITIATED: "handshake_initiated",
20702
+ COMPLETED: "handshake_completed",
20703
+ FAILED: "handshake_failed",
20704
+ ABORTED: "handshake_aborted"
20705
+ };
20706
+ function auditHandshakeInitiated(auditLog, ctx) {
20707
+ auditLog.append(
20708
+ "l4",
20709
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
20710
+ ctx.identity_id,
20711
+ detailsFromContext(ctx),
20712
+ "success"
20713
+ );
20714
+ }
20715
+ function auditHandshakeCompleted(auditLog, ctx) {
20716
+ const details = detailsFromContext(ctx);
20717
+ if (ctx.trust_tier !== void 0) {
20718
+ details.trust_tier = ctx.trust_tier;
20719
+ }
20720
+ auditLog.append(
20721
+ "l4",
20722
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
20723
+ ctx.identity_id,
20724
+ details,
20725
+ "success"
20726
+ );
20727
+ }
20728
+ function auditHandshakeFailed(auditLog, ctx) {
20729
+ const details = detailsFromContext(ctx);
20730
+ details.reason = ctx.reason;
20731
+ if (ctx.error !== void 0) {
20732
+ details.error = ctx.error;
20733
+ }
20734
+ auditLog.append(
20735
+ "l4",
20736
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
20737
+ ctx.identity_id,
20738
+ details,
20739
+ "failure"
20740
+ );
20741
+ }
20742
+ function auditHandshakeAborted(auditLog, ctx) {
20743
+ const details = detailsFromContext(ctx);
20744
+ details.reason = ctx.reason;
20745
+ auditLog.append(
20746
+ "l4",
20747
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
20748
+ ctx.identity_id,
20749
+ details,
20750
+ "failure"
20751
+ );
20752
+ }
20753
+ function detailsFromContext(ctx) {
20754
+ const details = {
20755
+ session_id: ctx.session_id,
20756
+ role: ctx.role
20757
+ };
20758
+ if (ctx.counterparty_id !== void 0) {
20759
+ details.counterparty_id = ctx.counterparty_id;
20760
+ }
20761
+ return details;
20762
+ }
20763
+
19739
20764
  // src/handshake/tools.ts
19740
20765
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
19741
20766
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -19769,6 +20794,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19769
20794
  const { challenge, session } = initiateHandshake(shr);
19770
20795
  sessions.set(session.session_id, session);
19771
20796
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
20797
+ auditHandshakeInitiated(auditLog, {
20798
+ session_id: session.session_id,
20799
+ role: "initiator",
20800
+ identity_id: shr.body.instance_id
20801
+ });
19772
20802
  return toolResult({
19773
20803
  session_id: session.session_id,
19774
20804
  challenge,
@@ -19808,10 +20838,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19808
20838
  );
19809
20839
  if ("error" in result) {
19810
20840
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
20841
+ auditHandshakeFailed(auditLog, {
20842
+ session_id: "unknown",
20843
+ role: "responder",
20844
+ identity_id: shr.body.instance_id,
20845
+ reason: classifyRespondFailure(result.error),
20846
+ error: result.error
20847
+ });
19811
20848
  return toolResult({ error: result.error });
19812
20849
  }
19813
20850
  sessions.set(result.session.session_id, result.session);
19814
20851
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
20852
+ auditHandshakeInitiated(auditLog, {
20853
+ session_id: result.session.session_id,
20854
+ role: "responder",
20855
+ identity_id: shr.body.instance_id,
20856
+ counterparty_id: challenge.shr.body.instance_id
20857
+ });
19815
20858
  let autoPublishResult;
19816
20859
  if (autoPublishHandshakes) {
19817
20860
  autoPublishResult = { attempted: true };
@@ -19919,9 +20962,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19919
20962
  const response = args.response;
19920
20963
  const session = sessions.get(sessionId);
19921
20964
  if (!session) {
20965
+ auditHandshakeFailed(auditLog, {
20966
+ session_id: sessionId,
20967
+ role: "initiator",
20968
+ identity_id: "unknown",
20969
+ reason: "session_unknown",
20970
+ error: `No handshake session found: ${sessionId}`
20971
+ });
19922
20972
  return toolResult({ error: `No handshake session found: ${sessionId}` });
19923
20973
  }
19924
20974
  if (session.state !== "initiated") {
20975
+ auditHandshakeFailed(auditLog, {
20976
+ session_id: sessionId,
20977
+ role: "initiator",
20978
+ identity_id: session.our_shr.body.instance_id,
20979
+ reason: "session_state_mismatch",
20980
+ error: `Session is in state '${session.state}', expected 'initiated'`
20981
+ });
19925
20982
  return toolResult({
19926
20983
  error: `Session is in state '${session.state}', expected 'initiated'`
19927
20984
  });
@@ -19935,6 +20992,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19935
20992
  if ("error" in result) {
19936
20993
  session.state = "failed";
19937
20994
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
20995
+ auditHandshakeFailed(auditLog, {
20996
+ session_id: sessionId,
20997
+ role: "initiator",
20998
+ identity_id: session.our_shr.body.instance_id,
20999
+ reason: classifyCompleteFailure(result.error),
21000
+ error: result.error
21001
+ });
19938
21002
  return toolResult({ error: result.error });
19939
21003
  }
19940
21004
  session.state = "completed";
@@ -19943,6 +21007,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19943
21007
  session.result = result.result;
19944
21008
  handshakeResults.set(result.result.counterparty_id, result.result);
19945
21009
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
21010
+ auditHandshakeCompleted(auditLog, {
21011
+ session_id: sessionId,
21012
+ role: "initiator",
21013
+ identity_id: session.our_shr.body.instance_id,
21014
+ counterparty_id: result.result.counterparty_id,
21015
+ trust_tier: result.result.trust_tier
21016
+ });
19946
21017
  return toolResult({
19947
21018
  completion: result.completion,
19948
21019
  result: result.result,
@@ -19990,6 +21061,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
19990
21061
  void 0,
19991
21062
  result.verified ? "success" : "failure"
19992
21063
  );
21064
+ if (result.verified) {
21065
+ auditHandshakeCompleted(auditLog, {
21066
+ session_id: session.session_id,
21067
+ role: "responder",
21068
+ identity_id: session.our_shr.body.instance_id,
21069
+ counterparty_id: result.counterparty_id,
21070
+ trust_tier: result.trust_tier
21071
+ });
21072
+ } else {
21073
+ auditHandshakeFailed(auditLog, {
21074
+ session_id: session.session_id,
21075
+ role: "responder",
21076
+ identity_id: session.our_shr.body.instance_id,
21077
+ counterparty_id: result.counterparty_id,
21078
+ reason: classifyCompleteFailure(result.errors.join("; ")),
21079
+ error: result.errors.join("; ")
21080
+ });
21081
+ }
19993
21082
  return toolResult({ result });
19994
21083
  }
19995
21084
  return toolResult({
@@ -20097,10 +21186,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20097
21186
  _content_trust: "external"
20098
21187
  });
20099
21188
  }
21189
+ },
21190
+ {
21191
+ name: "handshake_abort",
21192
+ 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.",
21193
+ inputSchema: {
21194
+ type: "object",
21195
+ properties: {
21196
+ session_id: {
21197
+ type: "string",
21198
+ description: "Session ID returned from handshake_initiate / handshake_respond."
21199
+ },
21200
+ reason: {
21201
+ type: "string",
21202
+ enum: [
21203
+ "operator_cancelled",
21204
+ "session_timeout",
21205
+ "transport_dropped",
21206
+ "shutdown",
21207
+ "other"
21208
+ ],
21209
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
21210
+ }
21211
+ },
21212
+ required: ["session_id"]
21213
+ },
21214
+ handler: async (args) => {
21215
+ const sessionId = args.session_id;
21216
+ const reason = args.reason ?? "operator_cancelled";
21217
+ const session = sessions.get(sessionId);
21218
+ if (!session) {
21219
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
21220
+ }
21221
+ if (session.state === "completed") {
21222
+ return toolResult({
21223
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
21224
+ });
21225
+ }
21226
+ sessions.delete(sessionId);
21227
+ auditHandshakeAborted(auditLog, {
21228
+ session_id: sessionId,
21229
+ role: session.role,
21230
+ identity_id: session.our_shr.body.instance_id,
21231
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
21232
+ reason
21233
+ });
21234
+ return toolResult({
21235
+ aborted: true,
21236
+ session_id: sessionId,
21237
+ reason
21238
+ });
21239
+ }
20100
21240
  }
20101
21241
  ];
20102
21242
  return { tools, handshakeResults };
20103
21243
  }
21244
+ function classifyRespondFailure(error) {
21245
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21246
+ if (error.includes("SHR verification failed")) return "shr_invalid";
21247
+ if (error.includes("No identity available")) return "no_signing_identity";
21248
+ return "other";
21249
+ }
21250
+ function classifyCompleteFailure(error) {
21251
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21252
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
21253
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
21254
+ if (error.includes("No identity available")) return "no_signing_identity";
21255
+ return "other";
21256
+ }
20104
21257
 
20105
21258
  // src/federation/registry.ts
20106
21259
  var DEFAULT_CAPABILITIES = {
@@ -21718,6 +22871,12 @@ function typed(markerPath, lineNumber, field, expected) {
21718
22871
  }
21719
22872
  async function consumeResetHistoryMarker(options) {
21720
22873
  const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
22874
+ const consumedPath = markerPath + ".consumed";
22875
+ if (await fileExists3(consumedPath)) {
22876
+ await rm(markerPath, { force: true });
22877
+ await rm(consumedPath, { force: true });
22878
+ return { emitted: 0, markerPath };
22879
+ }
21721
22880
  if (!await fileExists3(markerPath)) {
21722
22881
  return { emitted: 0, markerPath };
21723
22882
  }
@@ -21742,7 +22901,9 @@ async function consumeResetHistoryMarker(options) {
21742
22901
  });
21743
22902
  }
21744
22903
  await options.auditLog.flush();
22904
+ await writeFile(consumedPath, "", "utf-8");
21745
22905
  await rm(markerPath, { force: true });
22906
+ await rm(consumedPath, { force: true });
21746
22907
  return { emitted: markers.length, markerHash, markerPath };
21747
22908
  }
21748
22909
  async function fileExists3(path) {
@@ -30686,7 +31847,37 @@ var HubService = class {
30686
31847
  */
30687
31848
  async getConciergeHistory() {
30688
31849
  const chat = this.requireOperatorChat();
30689
- return chat.getConciergeHistory();
31850
+ return chat.getConciergeHistory();
31851
+ }
31852
+ // ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
31853
+ /**
31854
+ * Whether the operator-chat service has the WP-V1.3-9 memory store
31855
+ * wired. Routes use this to 503 cleanly when the foundation memory
31856
+ * surface is unavailable on a given fortress.
31857
+ */
31858
+ hasConciergeMemory() {
31859
+ return Boolean(this.deps.operatorChat?.hasConciergeMemory());
31860
+ }
31861
+ async listConciergeMemoryThreads(opts) {
31862
+ const chat = this.requireOperatorChat();
31863
+ if (!chat.hasConciergeMemory()) {
31864
+ throw new HubCapabilityError("concierge_memory_not_wired");
31865
+ }
31866
+ return chat.listConciergeMemoryThreads(opts);
31867
+ }
31868
+ async readConciergeMemoryThread(threadId, opts) {
31869
+ const chat = this.requireOperatorChat();
31870
+ if (!chat.hasConciergeMemory()) {
31871
+ throw new HubCapabilityError("concierge_memory_not_wired");
31872
+ }
31873
+ return chat.readConciergeMemoryThread(threadId, opts);
31874
+ }
31875
+ async deleteConciergeMemoryThread(threadId) {
31876
+ const chat = this.requireOperatorChat();
31877
+ if (!chat.hasConciergeMemory()) {
31878
+ throw new HubCapabilityError("concierge_memory_not_wired");
31879
+ }
31880
+ return chat.deleteConciergeMemoryThread(threadId);
30690
31881
  }
30691
31882
  /**
30692
31883
  * Open the click-to-inspect/approve panel for a wrapped agent. The
@@ -30775,7 +31966,28 @@ init_encoding();
30775
31966
 
30776
31967
  // src/chat/operator-chat-audit-events.ts
30777
31968
  var OPERATOR_CHAT_OPS = {
30778
- CONCIERGE_CHAT: "operator_concierge_chat"};
31969
+ CONCIERGE_CHAT: "operator_concierge_chat",
31970
+ /**
31971
+ * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
31972
+ * when the operator hits the list-threads or read-thread route. Body
31973
+ * carries the thread_id (or `*` for the list endpoint) and a count;
31974
+ * raw turn content never crosses the audit surface.
31975
+ */
31976
+ CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
31977
+ /**
31978
+ * Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
31979
+ * successful thread removal. Body carries thread_id + turn_count of
31980
+ * the deleted bundle.
31981
+ */
31982
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
31983
+ /**
31984
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
31985
+ * the multi-turn coherence fold cannot load the active thread's prior
31986
+ * turns; the concierge degrades to single-turn after emitting. Body
31987
+ * carries thread_id + a stable failure_reason enum.
31988
+ */
31989
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
31990
+ };
30779
31991
 
30780
31992
  // src/chat/operator-chat-types.ts
30781
31993
  var OPERATOR_CHAT_MAX_THREAD_LENGTH = 500;
@@ -30783,6 +31995,13 @@ var CONCIERGE_THREAD_KEY = "_fortress";
30783
31995
 
30784
31996
  // src/chat/operator-chat-service.ts
30785
31997
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
31998
+ var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
31999
+ var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
32000
+ var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
32001
+ var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32002
+ function approxTokenLen(text) {
32003
+ return Math.ceil(text.length / 4);
32004
+ }
30786
32005
  var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
30787
32006
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
30788
32007
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -30818,6 +32037,27 @@ var OperatorChatService = class {
30818
32037
  contextProviders;
30819
32038
  piiFilter;
30820
32039
  conciergeMaxTokens;
32040
+ memory;
32041
+ historyWindowTurns;
32042
+ historyFreshnessMs;
32043
+ historyTokenBudget;
32044
+ sessionTtlMs;
32045
+ clock;
32046
+ /**
32047
+ * In-memory thread_id assigned to the active concierge session.
32048
+ * The first sendConcierge call after construction allocates a fresh
32049
+ * UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
32050
+ * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
32051
+ */
32052
+ activeMemoryThreadId;
32053
+ /**
32054
+ * Wall-clock ms of the most recent sendConcierge that touched the
32055
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
32056
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
32057
+ * allocates a new thread_id even though the prior one is still
32058
+ * readable from the memory store.
32059
+ */
32060
+ lastInteractionAt;
30821
32061
  constructor(deps) {
30822
32062
  this.store = deps.store;
30823
32063
  this.auditLog = deps.auditLog;
@@ -30828,6 +32068,12 @@ var OperatorChatService = class {
30828
32068
  }
30829
32069
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
30830
32070
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
32071
+ if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
32072
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
32073
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
32074
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
32075
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
32076
+ this.clock = deps.conciergeClock ?? (() => Date.now());
30831
32077
  }
30832
32078
  // ── Concierge ─────────────────────────────────────────────────────────
30833
32079
  /**
@@ -30846,6 +32092,10 @@ var OperatorChatService = class {
30846
32092
  throw new Error("concierge query must not be empty");
30847
32093
  }
30848
32094
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
32095
+ const nowMs = this.clock();
32096
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
32097
+ this.activeMemoryThreadId = void 0;
32098
+ }
30849
32099
  const operatorMessage = {
30850
32100
  message_id: randomUUID(),
30851
32101
  surface: "concierge",
@@ -30858,6 +32108,30 @@ var OperatorChatService = class {
30858
32108
  CONCIERGE_THREAD_KEY,
30859
32109
  operatorMessage
30860
32110
  );
32111
+ let priorTurns = [];
32112
+ let memoryReadFailureReason = null;
32113
+ let activeThreadIdForRound;
32114
+ if (this.memory) {
32115
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
32116
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
32117
+ if (result.ok) {
32118
+ const cutoff = nowMs - this.historyFreshnessMs;
32119
+ const fresh = result.turns.filter((t) => {
32120
+ const ts = Date.parse(t.created_at);
32121
+ return Number.isFinite(ts) && ts >= cutoff;
32122
+ });
32123
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
32124
+ priorTurns = recent;
32125
+ } else {
32126
+ memoryReadFailureReason = result.reason;
32127
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
32128
+ }
32129
+ }
32130
+ if (this.memory) {
32131
+ const threadId = this.ensureActiveMemoryThread();
32132
+ await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
32133
+ });
32134
+ }
30861
32135
  const start = Date.now();
30862
32136
  let conciergeBody;
30863
32137
  let servedBy = "disabled";
@@ -30874,7 +32148,7 @@ var OperatorChatService = class {
30874
32148
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
30875
32149
  outcome = "substrate_disabled";
30876
32150
  } else {
30877
- const context = await this.assembleConciergeContext();
32151
+ const context = await this.assembleConciergeContext(priorTurns);
30878
32152
  const response = await this.substrateSelector.invokeSummarize(
30879
32153
  "concierge",
30880
32154
  {
@@ -30913,6 +32187,15 @@ var OperatorChatService = class {
30913
32187
  CONCIERGE_THREAD_KEY,
30914
32188
  responseMessage
30915
32189
  );
32190
+ let assistantTurnId;
32191
+ if (this.memory) {
32192
+ const threadId = this.ensureActiveMemoryThread();
32193
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
32194
+ if (persisted) assistantTurnId = persisted.turn_id;
32195
+ }
32196
+ if (this.memory && activeThreadIdForRound) {
32197
+ this.lastInteractionAt = nowMs;
32198
+ }
30916
32199
  const payload = {
30917
32200
  version: "1.2",
30918
32201
  event_id: makeEventId("conc"),
@@ -30924,7 +32207,12 @@ var OperatorChatService = class {
30924
32207
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
30925
32208
  substrate: servedBy,
30926
32209
  latency_ms: latencyMs,
30927
- outcome
32210
+ outcome,
32211
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
32212
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
32213
+ ...this.memory ? {
32214
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32215
+ } : {}
30928
32216
  };
30929
32217
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
30930
32218
  return {
@@ -30934,6 +32222,25 @@ var OperatorChatService = class {
30934
32222
  outcome
30935
32223
  };
30936
32224
  }
32225
+ /**
32226
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
32227
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
32228
+ * with `result: "failure"` since the concierge fell back to
32229
+ * single-turn mode for this round-trip.
32230
+ */
32231
+ emitMemoryReadFailed(threadId, reason) {
32232
+ const payload = {
32233
+ version: "1.2",
32234
+ event_id: makeEventId("conc-memfail"),
32235
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32236
+ identity_id: this.identityId,
32237
+ kind: "operator_concierge_memory_read_failed",
32238
+ surface: "concierge",
32239
+ thread_id: threadId,
32240
+ failure_reason: reason
32241
+ };
32242
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
32243
+ }
30937
32244
  /**
30938
32245
  * Read the persisted concierge thread, oldest message first. Returns
30939
32246
  * an empty array when no thread exists yet.
@@ -30945,6 +32252,105 @@ var OperatorChatService = class {
30945
32252
  );
30946
32253
  return thread ? thread.messages : [];
30947
32254
  }
32255
+ // ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
32256
+ /**
32257
+ * Whether the foundation memory store is wired. Routes use this to
32258
+ * 503 cleanly when called against an unwired service.
32259
+ */
32260
+ hasConciergeMemory() {
32261
+ return this.memory !== void 0;
32262
+ }
32263
+ /**
32264
+ * List concierge memory threads, newest-first. Emits the
32265
+ * `operator_concierge_history_read` audit event with `thread_id="*"`.
32266
+ */
32267
+ async listConciergeMemoryThreads(opts) {
32268
+ if (!this.memory) {
32269
+ throw new Error("concierge memory store not configured");
32270
+ }
32271
+ const summaries = await this.memory.listThreads(opts);
32272
+ const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
32273
+ const payload = {
32274
+ version: "1.2",
32275
+ event_id: makeEventId("conc-hist"),
32276
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32277
+ identity_id: this.identityId,
32278
+ kind: "operator_concierge_history_read",
32279
+ surface: "concierge",
32280
+ thread_id: "*",
32281
+ turn_count: totalTurns
32282
+ };
32283
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
32284
+ return summaries;
32285
+ }
32286
+ /**
32287
+ * Read a concierge memory thread, oldest turn first. Emits the
32288
+ * `operator_concierge_history_read` audit event with the named
32289
+ * thread_id and the count of turns surfaced.
32290
+ */
32291
+ async readConciergeMemoryThread(threadId, opts) {
32292
+ if (!this.memory) {
32293
+ throw new Error("concierge memory store not configured");
32294
+ }
32295
+ const turns = await this.memory.readThread(threadId, opts);
32296
+ const payload = {
32297
+ version: "1.2",
32298
+ event_id: makeEventId("conc-hist"),
32299
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32300
+ identity_id: this.identityId,
32301
+ kind: "operator_concierge_history_read",
32302
+ surface: "concierge",
32303
+ thread_id: threadId,
32304
+ turn_count: turns.length
32305
+ };
32306
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
32307
+ return turns;
32308
+ }
32309
+ /**
32310
+ * Delete a concierge memory thread. Emits
32311
+ * `operator_concierge_thread_deleted` only when a bundle was actually
32312
+ * removed; absent threads return false without an audit event.
32313
+ */
32314
+ async deleteConciergeMemoryThread(threadId) {
32315
+ if (!this.memory) {
32316
+ throw new Error("concierge memory store not configured");
32317
+ }
32318
+ const turnsBefore = await this.memory.readThread(threadId);
32319
+ if (turnsBefore.length === 0) {
32320
+ return await this.memory.deleteThread(threadId);
32321
+ }
32322
+ const removed = await this.memory.deleteThread(threadId);
32323
+ if (!removed) return false;
32324
+ if (this.activeMemoryThreadId === threadId) {
32325
+ this.activeMemoryThreadId = void 0;
32326
+ }
32327
+ const payload = {
32328
+ version: "1.2",
32329
+ event_id: makeEventId("conc-del"),
32330
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32331
+ identity_id: this.identityId,
32332
+ kind: "operator_concierge_thread_deleted",
32333
+ surface: "concierge",
32334
+ thread_id: threadId,
32335
+ turn_count: turnsBefore.length
32336
+ };
32337
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
32338
+ return true;
32339
+ }
32340
+ /**
32341
+ * Reset the active session memory thread. Subsequent sendConcierge
32342
+ * calls allocate a fresh thread_id. Surfaced for tests + future "new
32343
+ * conversation" affordance; not currently called by the dashboard.
32344
+ */
32345
+ resetConciergeMemoryThread() {
32346
+ this.activeMemoryThreadId = void 0;
32347
+ }
32348
+ ensureActiveMemoryThread() {
32349
+ if (!this.activeMemoryThreadId) {
32350
+ this.activeMemoryThreadId = randomUUID();
32351
+ }
32352
+ return this.activeMemoryThreadId;
32353
+ }
30948
32354
  /**
30949
32355
  * Stitch fortress state into a single context blob the substrate
30950
32356
  * folds into its summarization prompt.
@@ -30957,6 +32363,11 @@ var OperatorChatService = class {
30957
32363
  * ## Sanctuary reference
30958
32364
  * <static domain reference block>
30959
32365
  *
32366
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
32367
+ * OPERATOR: ...
32368
+ * CONCIERGE: ...
32369
+ * ---
32370
+ *
30960
32371
  * ## Recent activity
30961
32372
  * <recentActivity output>
30962
32373
  *
@@ -30966,37 +32377,69 @@ var OperatorChatService = class {
30966
32377
  * ## Open inbox
30967
32378
  * <openInbox output>
30968
32379
  * ```
32380
+ *
32381
+ * The substrate selector ships a `context: string` shape (not a
32382
+ * messages array), so multi-turn coherence is folded as a structured
32383
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
32384
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
32385
+ * if available; the v1.2 selector does not expose one, so structured
32386
+ * serialization is the canonical path for v1.3.
30969
32387
  */
30970
- async assembleConciergeContext() {
32388
+ async assembleConciergeContext(priorTurns = []) {
30971
32389
  const ref = `## Sanctuary reference
30972
32390
  ${SANCTUARY_DOMAIN_REFERENCE}`;
32391
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
30973
32392
  if (!this.contextProviders) {
30974
- return `${ref}
30975
-
30976
- ## Recent activity
30977
- (no providers wired)
30978
-
30979
- ## Wrapped agents
30980
- (no providers wired)
30981
-
30982
- ## Open inbox
30983
- (no providers wired)`;
32393
+ return [
32394
+ ref,
32395
+ ...priorSection ? [priorSection] : [],
32396
+ "## Recent activity\n(no providers wired)",
32397
+ "## Wrapped agents\n(no providers wired)",
32398
+ "## Open inbox\n(no providers wired)"
32399
+ ].join("\n\n");
30984
32400
  }
30985
32401
  const [activity, agents, inbox] = await Promise.all([
30986
32402
  this.contextProviders.recentActivity(),
30987
32403
  this.contextProviders.agentInventory(),
30988
32404
  this.contextProviders.openInbox()
30989
32405
  ]);
30990
- return `${ref}
30991
-
30992
- ## Recent activity
30993
- ${activity}
30994
-
30995
- ## Wrapped agents
30996
- ${agents}
30997
-
30998
- ## Open inbox
30999
- ${inbox}`;
32406
+ return [
32407
+ ref,
32408
+ ...priorSection ? [priorSection] : [],
32409
+ `## Recent activity
32410
+ ${activity}`,
32411
+ `## Wrapped agents
32412
+ ${agents}`,
32413
+ `## Open inbox
32414
+ ${inbox}`
32415
+ ].join("\n\n");
32416
+ }
32417
+ /**
32418
+ * Render the prior-conversation section with token-budget enforcement
32419
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
32420
+ * section exceeds `historyTokenBudget`. Returns an empty string when
32421
+ * the input is empty or when the budget excludes every turn.
32422
+ */
32423
+ formatPriorTurnsSection(turns) {
32424
+ if (turns.length === 0) return "";
32425
+ const HEADER = "## Prior conversation";
32426
+ const lines = turns.map(formatPriorTurnLine);
32427
+ const headerTokens = approxTokenLen(`${HEADER}
32428
+ `);
32429
+ const sepTokens = approxTokenLen("\n");
32430
+ let runningTokens = headerTokens;
32431
+ let runningLines = [];
32432
+ for (let i = lines.length - 1; i >= 0; i--) {
32433
+ const line = lines[i];
32434
+ const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
32435
+ if (runningTokens + tokens > this.historyTokenBudget) break;
32436
+ runningTokens += tokens;
32437
+ runningLines.push(line);
32438
+ }
32439
+ if (runningLines.length === 0) return "";
32440
+ runningLines = runningLines.reverse();
32441
+ return `${HEADER}
32442
+ ${runningLines.join("\n")}`;
31000
32443
  }
31001
32444
  // ── audit helpers ────────────────────────────────────────────────────
31002
32445
  emit(operation, payload, result) {
@@ -31012,6 +32455,10 @@ ${inbox}`;
31012
32455
  function makeEventId(prefix) {
31013
32456
  return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
31014
32457
  }
32458
+ function formatPriorTurnLine(turn) {
32459
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
32460
+ return `${label}: ${turn.content}`;
32461
+ }
31015
32462
  function hashOf(input) {
31016
32463
  return hashToString(sha256(stringToBytes(input)));
31017
32464
  }
@@ -31108,6 +32555,297 @@ var OperatorChatStore = class {
31108
32555
  }
31109
32556
  };
31110
32557
 
32558
+ // src/chat/concierge-memory-store.ts
32559
+ init_encryption();
32560
+ init_encoding();
32561
+ var CONCIERGE_MEMORY_NAMESPACE = "_chat";
32562
+ var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
32563
+ var HKDF_INFO2 = "concierge-memory-store-v1";
32564
+ var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
32565
+ var MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
32566
+ var ConciergeMemoryStore = class {
32567
+ storage;
32568
+ encryptionKey;
32569
+ fortressId;
32570
+ retentionDays;
32571
+ locks;
32572
+ constructor(opts) {
32573
+ this.storage = opts.storage;
32574
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
32575
+ this.fortressId = opts.fortressId;
32576
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
32577
+ this.locks = /* @__PURE__ */ new Map();
32578
+ }
32579
+ /**
32580
+ * Append a turn to the named thread, creating the bundle if no record
32581
+ * exists. Returns the persisted turn (with assigned turn_id +
32582
+ * retention_until). Per-thread serialisation guarantees turn_id
32583
+ * monotonicity even under concurrent callers.
32584
+ */
32585
+ async appendTurn(threadId, role, content) {
32586
+ return this.withLock(threadId, async () => {
32587
+ const bundle = await this.loadBundle(threadId) ?? null;
32588
+ const now = /* @__PURE__ */ new Date();
32589
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
32590
+ const retentionUntil = new Date(now.getTime() + retentionMs);
32591
+ const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
32592
+ const turn = {
32593
+ thread_id: threadId,
32594
+ fortress_id: this.fortressId,
32595
+ turn_id: nextTurnId,
32596
+ role,
32597
+ content,
32598
+ created_at: now.toISOString(),
32599
+ retention_until: retentionUntil.toISOString()
32600
+ };
32601
+ const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
32602
+ version: 1,
32603
+ thread_id: threadId,
32604
+ fortress_id: this.fortressId,
32605
+ created_at: now.toISOString(),
32606
+ turns: [turn]
32607
+ };
32608
+ await this.saveBundle(next);
32609
+ return turn;
32610
+ });
32611
+ }
32612
+ /**
32613
+ * Read turns from a thread, oldest-first. Returns an empty array if
32614
+ * the thread does not exist or its bundle is corrupt. Does not emit
32615
+ * audit events; the caller (HTTP route handler) owns audit semantics.
32616
+ */
32617
+ async readThread(threadId, opts) {
32618
+ const bundle = await this.loadBundle(threadId);
32619
+ if (!bundle) return [];
32620
+ let turns = bundle.turns;
32621
+ if (opts?.sinceTurnId !== void 0) {
32622
+ const cutoff = opts.sinceTurnId;
32623
+ turns = turns.filter((t) => t.turn_id > cutoff);
32624
+ }
32625
+ if (opts?.limit !== void 0) {
32626
+ turns = turns.slice(0, opts.limit);
32627
+ }
32628
+ return turns;
32629
+ }
32630
+ /**
32631
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
32632
+ * `readThread` collapses every failure mode to an empty array, this
32633
+ * variant returns a discriminated result so the multi-turn fold path
32634
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
32635
+ * with a concrete cause.
32636
+ *
32637
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
32638
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
32639
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
32640
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
32641
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
32642
+ * - Storage IO error → `io_failed`.
32643
+ */
32644
+ async readThreadStrict(threadId, opts) {
32645
+ const key = bundleKey(threadId);
32646
+ let raw;
32647
+ try {
32648
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
32649
+ } catch {
32650
+ return { ok: false, reason: "io_failed" };
32651
+ }
32652
+ if (!raw) return { ok: true, turns: [] };
32653
+ if (raw.length > MAX_BUNDLE_BYTES2) {
32654
+ return { ok: false, reason: "oversize_bundle" };
32655
+ }
32656
+ let envelope;
32657
+ try {
32658
+ envelope = JSON.parse(bytesToString(raw));
32659
+ } catch {
32660
+ return { ok: false, reason: "schema_mismatch" };
32661
+ }
32662
+ let plaintext;
32663
+ try {
32664
+ const aad = stringToBytes(threadId);
32665
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
32666
+ } catch {
32667
+ return { ok: false, reason: "decrypt_failed" };
32668
+ }
32669
+ let parsed;
32670
+ try {
32671
+ parsed = JSON.parse(bytesToString(plaintext));
32672
+ } catch {
32673
+ return { ok: false, reason: "schema_mismatch" };
32674
+ }
32675
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
32676
+ if (parsed.thread_id !== threadId) {
32677
+ return { ok: false, reason: "schema_mismatch" };
32678
+ }
32679
+ let turns = parsed.turns;
32680
+ if (opts?.sinceTurnId !== void 0) {
32681
+ const cutoff = opts.sinceTurnId;
32682
+ turns = turns.filter((t) => t.turn_id > cutoff);
32683
+ }
32684
+ if (opts?.limit !== void 0) {
32685
+ turns = turns.slice(0, opts.limit);
32686
+ }
32687
+ return { ok: true, turns };
32688
+ }
32689
+ /**
32690
+ * Enumerate concierge threads in this fortress with summary metadata.
32691
+ * Sorted newest-first by last_turn_at.
32692
+ */
32693
+ async listThreads(opts) {
32694
+ const entries = await this.storage.list(
32695
+ CONCIERGE_MEMORY_NAMESPACE,
32696
+ CONCIERGE_MEMORY_KEY_PREFIX
32697
+ );
32698
+ const summaries = [];
32699
+ for (const meta of entries) {
32700
+ const threadId = stripKeyPrefix(meta.key);
32701
+ if (threadId === null) continue;
32702
+ const bundle = await this.loadBundle(threadId);
32703
+ if (!bundle || bundle.turns.length === 0) continue;
32704
+ const last = bundle.turns[bundle.turns.length - 1];
32705
+ summaries.push({
32706
+ thread_id: bundle.thread_id,
32707
+ created_at: bundle.created_at,
32708
+ last_turn_at: last ? last.created_at : bundle.created_at,
32709
+ turn_count: bundle.turns.length
32710
+ });
32711
+ }
32712
+ summaries.sort(
32713
+ (a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
32714
+ );
32715
+ if (opts?.limit !== void 0) {
32716
+ return summaries.slice(0, opts.limit);
32717
+ }
32718
+ return summaries;
32719
+ }
32720
+ /**
32721
+ * Delete a thread's bundle. Returns true if the bundle existed and
32722
+ * was removed; false if no bundle was present. Audit emission is the
32723
+ * caller's responsibility.
32724
+ */
32725
+ async deleteThread(threadId) {
32726
+ const key = bundleKey(threadId);
32727
+ return this.withLock(threadId, async () => {
32728
+ const existed = await this.storage.exists(
32729
+ CONCIERGE_MEMORY_NAMESPACE,
32730
+ key
32731
+ );
32732
+ if (!existed) return false;
32733
+ try {
32734
+ await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
32735
+ } catch {
32736
+ return false;
32737
+ }
32738
+ return true;
32739
+ });
32740
+ }
32741
+ /**
32742
+ * Drop expired turns across all threads. Threads emptied by pruning
32743
+ * are removed entirely. Returns the count of turns pruned.
32744
+ */
32745
+ async pruneExpired(now) {
32746
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
32747
+ const entries = await this.storage.list(
32748
+ CONCIERGE_MEMORY_NAMESPACE,
32749
+ CONCIERGE_MEMORY_KEY_PREFIX
32750
+ );
32751
+ let pruned = 0;
32752
+ for (const meta of entries) {
32753
+ const threadId = stripKeyPrefix(meta.key);
32754
+ if (threadId === null) continue;
32755
+ pruned += await this.withLock(threadId, async () => {
32756
+ const bundle = await this.loadBundle(threadId);
32757
+ if (!bundle) return 0;
32758
+ const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
32759
+ const dropped = bundle.turns.length - kept.length;
32760
+ if (dropped === 0) return 0;
32761
+ if (kept.length === 0) {
32762
+ await this.storage.delete(
32763
+ CONCIERGE_MEMORY_NAMESPACE,
32764
+ bundleKey(threadId)
32765
+ );
32766
+ } else {
32767
+ await this.saveBundle({ ...bundle, turns: kept });
32768
+ }
32769
+ return dropped;
32770
+ });
32771
+ }
32772
+ return { pruned };
32773
+ }
32774
+ // ── internals ────────────────────────────────────────────────────────
32775
+ async loadBundle(threadId) {
32776
+ const key = bundleKey(threadId);
32777
+ let raw;
32778
+ try {
32779
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
32780
+ } catch {
32781
+ return null;
32782
+ }
32783
+ if (!raw) return null;
32784
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
32785
+ try {
32786
+ const envelope = JSON.parse(bytesToString(raw));
32787
+ const aad = stringToBytes(threadId);
32788
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
32789
+ const parsed = JSON.parse(
32790
+ bytesToString(plaintext)
32791
+ );
32792
+ if (parsed.version !== 1) return null;
32793
+ if (parsed.thread_id !== threadId) return null;
32794
+ return parsed;
32795
+ } catch {
32796
+ return null;
32797
+ }
32798
+ }
32799
+ async saveBundle(bundle) {
32800
+ const key = bundleKey(bundle.thread_id);
32801
+ const aad = stringToBytes(bundle.thread_id);
32802
+ const plaintext = stringToBytes(JSON.stringify(bundle));
32803
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
32804
+ await this.storage.write(
32805
+ CONCIERGE_MEMORY_NAMESPACE,
32806
+ key,
32807
+ stringToBytes(JSON.stringify(envelope))
32808
+ );
32809
+ }
32810
+ /**
32811
+ * Run `task` while holding the per-thread async lock. Lock is released
32812
+ * once the task settles (success or failure). Generic helper so
32813
+ * appendTurn / deleteThread / pruneExpired share serialisation.
32814
+ */
32815
+ async withLock(threadId, task) {
32816
+ const previous = this.locks.get(threadId) ?? Promise.resolve();
32817
+ let release;
32818
+ const next = new Promise((resolve6) => {
32819
+ release = resolve6;
32820
+ });
32821
+ const chained = previous.then(() => next);
32822
+ this.locks.set(threadId, chained);
32823
+ try {
32824
+ await previous;
32825
+ return await task();
32826
+ } finally {
32827
+ release();
32828
+ if (this.locks.get(threadId) === chained) {
32829
+ this.locks.delete(threadId);
32830
+ }
32831
+ }
32832
+ }
32833
+ };
32834
+ function bundleKey(threadId) {
32835
+ return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
32836
+ }
32837
+ function stripKeyPrefix(key) {
32838
+ if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
32839
+ return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
32840
+ }
32841
+ function lastTurnId(bundle) {
32842
+ let max = 0;
32843
+ for (const t of bundle.turns) {
32844
+ if (t.turn_id > max) max = t.turn_id;
32845
+ }
32846
+ return max;
32847
+ }
32848
+
31111
32849
  // src/dashboard/v1_1/wiring.ts
31112
32850
  var CapabilityErrorAgentController = class {
31113
32851
  fail(action) {
@@ -31146,6 +32884,14 @@ function buildV11Bindings(inputs) {
31146
32884
  let operatorChatService;
31147
32885
  if (inputs.storage && inputs.masterKey) {
31148
32886
  const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
32887
+ const conciergeMemory = new ConciergeMemoryStore({
32888
+ storage: inputs.storage,
32889
+ masterKey: inputs.masterKey,
32890
+ fortressId: inputs.fortressId,
32891
+ ...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
32892
+ });
32893
+ void conciergeMemory.pruneExpired().catch(() => {
32894
+ });
31149
32895
  operatorChatService = new OperatorChatService({
31150
32896
  store: chatStore,
31151
32897
  auditLog: inputs.auditLog,
@@ -31156,7 +32902,8 @@ function buildV11Bindings(inputs) {
31156
32902
  identityId: inputs.identityId,
31157
32903
  registry
31158
32904
  }),
31159
- conciergePiiFilter: buildConciergePiiFilter()
32905
+ conciergePiiFilter: buildConciergePiiFilter(),
32906
+ conciergeMemory
31160
32907
  });
31161
32908
  }
31162
32909
  const hubService = new HubService({
@@ -31348,13 +33095,13 @@ init_encryption();
31348
33095
  init_encoding();
31349
33096
  var INTELLIGENCE_NAMESPACE = "_intelligence";
31350
33097
  var SUBSTRATE_CONFIG_KEY = "substrate-config";
31351
- var HKDF_INFO2 = "intelligence-substrate-config";
33098
+ var HKDF_INFO3 = "intelligence-substrate-config";
31352
33099
  var IntelligenceConfigStore = class {
31353
33100
  storage;
31354
33101
  encryptionKey;
31355
33102
  constructor(storage, masterKey) {
31356
33103
  this.storage = storage;
31357
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
33104
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
31358
33105
  }
31359
33106
  /**
31360
33107
  * Load the operator's substrate config from disk. Returns the config
@@ -33493,7 +35240,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
33493
35240
  );
33494
35241
  }
33495
35242
  }
33496
- const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
35243
+ const reputationBundleFailed = reputation?.bundle_signature_valid === false;
35244
+ const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
35245
+ const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
33497
35246
  const identityFailed = identity ? !identity.signature_valid : false;
33498
35247
  const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
33499
35248
  const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
@@ -33502,6 +35251,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
33502
35251
  `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
33503
35252
  );
33504
35253
  }
35254
+ let detailedFailureClass;
35255
+ if (identityFailed) {
35256
+ detailedFailureClass = "identity_signature_invalid";
35257
+ } else if (reputationBundleFailed) {
35258
+ detailedFailureClass = "reputation_bundle_signature_invalid";
35259
+ } else if (reputationAttestationFailed) {
35260
+ detailedFailureClass = "reputation_attestation_signature_invalid";
35261
+ } else if (unverifiableFailed) {
35262
+ detailedFailureClass = "reputation_unverifiable_attestations";
35263
+ }
33505
35264
  return {
33506
35265
  version: "1.1",
33507
35266
  passed: !reputationFailed && !identityFailed && !unverifiableFailed,
@@ -33521,7 +35280,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
33521
35280
  identity,
33522
35281
  audit,
33523
35282
  reputation,
33524
- failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
35283
+ failure_class: detailedFailureClass
33525
35284
  };
33526
35285
  }
33527
35286
 
@@ -33911,7 +35670,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
33911
35670
  }
33912
35671
  return null;
33913
35672
  }
33914
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
35673
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
33915
35674
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
33916
35675
  if (!destinationSigner) {
33917
35676
  return {
@@ -33973,8 +35732,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
33973
35732
  }
33974
35733
  }
33975
35734
  }
35735
+ let plaintext;
33976
35736
  try {
33977
- const plaintext = decrypt(
35737
+ plaintext = decrypt(
33978
35738
  item.entry.payload,
33979
35739
  deriveNamespaceKey(sourceMasterKey, item.namespace)
33980
35740
  );
@@ -33983,28 +35743,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
33983
35743
  skipped++;
33984
35744
  continue;
33985
35745
  }
33986
- await stateStore.write(
33987
- item.namespace,
33988
- item.key,
33989
- bytesToString(plaintext),
33990
- destinationSigner.identity_id,
33991
- destinationSigner.encrypted_private_key,
33992
- identityEncryptionKey,
33993
- {
33994
- content_type: item.entry.metadata.content_type,
33995
- ttl_seconds: item.entry.metadata.ttl_seconds,
33996
- tags: [
33997
- ...item.entry.metadata.tags ?? [],
33998
- "exit-import",
33999
- `source:${item.entry.kid}`
34000
- ]
34001
- }
34002
- );
34003
- imported++;
34004
35746
  } catch {
34005
35747
  skippedInvalidSig++;
34006
35748
  skipped++;
35749
+ continue;
34007
35750
  }
35751
+ await stateStore.write(
35752
+ item.namespace,
35753
+ item.key,
35754
+ bytesToString(plaintext),
35755
+ destinationSigner.identity_id,
35756
+ destinationSigner.encrypted_private_key,
35757
+ identityEncryptionKey,
35758
+ {
35759
+ content_type: item.entry.metadata.content_type,
35760
+ ttl_seconds: item.entry.metadata.ttl_seconds,
35761
+ tags: [
35762
+ ...item.entry.metadata.tags ?? [],
35763
+ "exit-import",
35764
+ `source:${item.entry.kid}`
35765
+ ]
35766
+ }
35767
+ );
35768
+ imported++;
35769
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
34008
35770
  }
34009
35771
  return {
34010
35772
  status: "rekeyed",
@@ -34015,6 +35777,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
34015
35777
  conflicts
34016
35778
  };
34017
35779
  }
35780
+ async function cleanupStagedPaths(storage, staged) {
35781
+ let removed = 0;
35782
+ const failed = [];
35783
+ for (const loc of staged) {
35784
+ try {
35785
+ const ok = await storage.delete(loc.namespace, loc.key);
35786
+ if (ok) {
35787
+ removed++;
35788
+ } else {
35789
+ failed.push(loc);
35790
+ }
35791
+ } catch {
35792
+ failed.push(loc);
35793
+ }
35794
+ }
35795
+ return { removed, failed };
35796
+ }
34018
35797
  async function stageArtifact(storage, namespace, key, value) {
34019
35798
  await storage.write(namespace, key, jsonBytes(value));
34020
35799
  }
@@ -34139,6 +35918,8 @@ async function importExitBundle(opts) {
34139
35918
  }
34140
35919
  const importId = importIdForManifest(manifest);
34141
35920
  const stagedArtifacts = [];
35921
+ const stagedLocations = [];
35922
+ const importedRekeyEntries = [];
34142
35923
  if (identityArtifact) {
34143
35924
  await stageArtifact(
34144
35925
  opts.storage,
@@ -34147,10 +35928,15 @@ async function importExitBundle(opts) {
34147
35928
  identityArtifact.json
34148
35929
  );
34149
35930
  stagedArtifacts.push("public_identity");
35931
+ stagedLocations.push({
35932
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
35933
+ key: identityArtifact.json.bundle.identity_id
35934
+ });
34150
35935
  }
34151
35936
  if (policySet) {
34152
35937
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
34153
35938
  stagedArtifacts.push("policy_set");
35939
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
34154
35940
  }
34155
35941
  if (auditReceipts) {
34156
35942
  await stageArtifact(
@@ -34160,10 +35946,12 @@ async function importExitBundle(opts) {
34160
35946
  auditReceipts.json
34161
35947
  );
34162
35948
  stagedArtifacts.push("audit_receipts");
35949
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
34163
35950
  }
34164
35951
  if (commitments) {
34165
35952
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
34166
35953
  stagedArtifacts.push("commitments");
35954
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
34167
35955
  }
34168
35956
  if (placeholderMetadata) {
34169
35957
  await stageArtifact(
@@ -34173,12 +35961,17 @@ async function importExitBundle(opts) {
34173
35961
  placeholderMetadata.json
34174
35962
  );
34175
35963
  stagedArtifacts.push("placeholder_vault_metadata");
35964
+ stagedLocations.push({
35965
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
35966
+ key: importId
35967
+ });
34176
35968
  }
34177
35969
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
34178
35970
  manifest: manifest.body,
34179
35971
  verified_at: verification.verified_at,
34180
35972
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
34181
35973
  });
35974
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
34182
35975
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
34183
35976
  let reputationResult = {
34184
35977
  imported_attestations: 0,
@@ -34203,26 +35996,57 @@ async function importExitBundle(opts) {
34203
35996
  encryptedState?.json ?? null,
34204
35997
  opts
34205
35998
  );
34206
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
34207
- encryptedState.json,
34208
- opts,
34209
- sourceMasterKey,
34210
- publicKeys.byIdentityId
34211
- ) : {
34212
- status: "staged_requires_source_key",
34213
- imported_keys: 0,
34214
- skipped_keys: encryptedState.json.entries.length,
34215
- skipped_invalid_sig: 0,
34216
- skipped_unknown_kid: 0,
34217
- conflicts: conflicts.state_conflicts.length
34218
- } : {
34219
- status: "not_requested",
34220
- imported_keys: 0,
34221
- skipped_keys: 0,
34222
- skipped_invalid_sig: 0,
34223
- skipped_unknown_kid: 0,
34224
- conflicts: 0
34225
- };
35999
+ let stateResult;
36000
+ try {
36001
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36002
+ encryptedState.json,
36003
+ opts,
36004
+ sourceMasterKey,
36005
+ publicKeys.byIdentityId,
36006
+ importedRekeyEntries
36007
+ ) : {
36008
+ status: "staged_requires_source_key",
36009
+ imported_keys: 0,
36010
+ skipped_keys: encryptedState.json.entries.length,
36011
+ skipped_invalid_sig: 0,
36012
+ skipped_unknown_kid: 0,
36013
+ conflicts: conflicts.state_conflicts.length
36014
+ } : {
36015
+ status: "not_requested",
36016
+ imported_keys: 0,
36017
+ skipped_keys: 0,
36018
+ skipped_invalid_sig: 0,
36019
+ skipped_unknown_kid: 0,
36020
+ conflicts: 0
36021
+ };
36022
+ } catch (err) {
36023
+ const toCleanup = [
36024
+ ...importedRekeyEntries,
36025
+ ...stagedLocations
36026
+ ];
36027
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
36028
+ opts.auditLog.append(
36029
+ "l1",
36030
+ "exit_bundle_rekey_failed_cleanup",
36031
+ manifest.body.identity_binding.identity_id,
36032
+ {
36033
+ import_id: importId,
36034
+ manifest_version: manifest.body.manifest_version,
36035
+ rekey_entries_removed: importedRekeyEntries.length,
36036
+ staged_artifacts_removed: stagedLocations.length,
36037
+ removed_total: cleanup.removed,
36038
+ cleanup_failed_count: cleanup.failed.length,
36039
+ original_error: err instanceof Error ? err.message : String(err)
36040
+ },
36041
+ "failure"
36042
+ );
36043
+ await opts.auditLog.flush();
36044
+ const originalMessage = err instanceof Error ? err.message : String(err);
36045
+ throw new ExitBundleImportError(
36046
+ "REKEY_FAILED_AND_CLEANED",
36047
+ `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).`
36048
+ );
36049
+ }
34226
36050
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
34227
36051
  import_id: importId,
34228
36052
  manifest_version: manifest.body.manifest_version,
@@ -34448,7 +36272,19 @@ async function runExitCommand(args) {
34448
36272
  }
34449
36273
  const config = await loadConfig();
34450
36274
  const ctx = await openExitContext(argv, env);
34451
- const policy = await loadPrincipalPolicy(ctx.storagePath);
36275
+ let policy;
36276
+ try {
36277
+ policy = await loadPrincipalPolicy(ctx.storagePath);
36278
+ } catch (policyErr) {
36279
+ if (policyErr instanceof MalformedPrincipalPolicyError) {
36280
+ write(err, `
36281
+ Sanctuary cannot proceed.
36282
+ ${policyErr.message}
36283
+ `);
36284
+ return 1;
36285
+ }
36286
+ throw policyErr;
36287
+ }
34452
36288
  const result = await exportExitBundle({
34453
36289
  bundleDir: outDir,
34454
36290
  storage: ctx.storage,
@@ -35100,7 +36936,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
35100
36936
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
35101
36937
  await profileStore.load();
35102
36938
  const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
35103
- const policy = await loadPrincipalPolicy(config.storage_path);
36939
+ let policy;
36940
+ try {
36941
+ policy = await loadPrincipalPolicy(config.storage_path);
36942
+ } catch (err) {
36943
+ if (err instanceof MalformedPrincipalPolicyError) {
36944
+ console.error(`
36945
+ Sanctuary cannot start.
36946
+ ${err.message}
36947
+ `);
36948
+ process.exit(1);
36949
+ }
36950
+ throw err;
36951
+ }
35104
36952
  const baseline = new BaselineTracker(storage, masterKey);
35105
36953
  await baseline.load();
35106
36954
  let approvalChannel;
@@ -35197,7 +37045,35 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
35197
37045
  timestamp: alert.timestamp
35198
37046
  });
35199
37047
  } : void 0;
35200
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37048
+ const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37049
+ const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
37050
+ const approvalAggregator = new ApprovalAggregator({
37051
+ storage,
37052
+ masterKey,
37053
+ auditLog,
37054
+ identityId: aggregatorIdentityId,
37055
+ fortressId: fortressIdForAggregator
37056
+ });
37057
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
37058
+ underlying: approvalChannel,
37059
+ aggregator: approvalAggregator,
37060
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
37061
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
37062
+ });
37063
+ const gate = new ApprovalGate(
37064
+ policy,
37065
+ baseline,
37066
+ wrappedApprovalChannel,
37067
+ auditLog,
37068
+ injectionDetector,
37069
+ onInjectionAlert
37070
+ );
37071
+ gate.setApprovalEventCallback((event) => {
37072
+ void approvalAggregator.ingest(event);
37073
+ });
37074
+ if (dashboard) {
37075
+ dashboard.setApprovalAggregator(approvalAggregator);
37076
+ }
35201
37077
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
35202
37078
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
35203
37079
  config,
@@ -35373,6 +37249,6 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
35373
37249
  };
35374
37250
  }
35375
37251
 
35376
- export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
37252
+ export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, ExitBundleImportError, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MalformedPrincipalPolicyError, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize2 as canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate2 as getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
35377
37253
  //# sourceMappingURL=index.js.map
35378
37254
  //# sourceMappingURL=index.js.map