@sanctuary-framework/mcp-server 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -4428,13 +4428,23 @@ function parseScalar(value) {
4428
4428
  return value.replace(/^["']|["']$/g, "");
4429
4429
  }
4430
4430
  function validatePolicy(raw) {
4431
+ if (!("tier1_always_approve" in raw)) {
4432
+ throw new Error(
4433
+ "Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
4434
+ );
4435
+ }
4436
+ if (!("approval_channel" in raw)) {
4437
+ throw new Error(
4438
+ "Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
4439
+ );
4440
+ }
4431
4441
  const userTier3 = raw.tier3_always_allow ?? [];
4432
4442
  const mergedTier3 = [
4433
4443
  .../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
4434
4444
  ];
4435
4445
  return {
4436
4446
  version: raw.version ?? 1,
4437
- tier1_always_approve: raw.tier1_always_approve ?? DEFAULT_POLICY.tier1_always_approve,
4447
+ tier1_always_approve: raw.tier1_always_approve,
4438
4448
  tier2_anomaly: {
4439
4449
  ...DEFAULT_TIER2,
4440
4450
  ...raw.tier2_anomaly ?? {}
@@ -4455,6 +4465,11 @@ function generateDefaultPolicyYaml() {
4455
4465
  # This file controls what your agent can do without asking.
4456
4466
  # Edit this file directly. Your agent cannot modify it.
4457
4467
  # Changes take effect on server restart.
4468
+ #
4469
+ # Required keys (must be present; use [] or {} for empty):
4470
+ # tier1_always_approve, approval_channel
4471
+ # Optional keys (omit to use defaults; new defaults merge automatically):
4472
+ # tier2_anomaly, tier3_always_allow
4458
4473
 
4459
4474
  version: 1
4460
4475
 
@@ -4561,21 +4576,39 @@ approval_channel:
4561
4576
  }
4562
4577
  async function loadPrincipalPolicy(storagePath) {
4563
4578
  const policyPath = path.join(storagePath, "principal-policy.yaml");
4579
+ let content;
4580
+ try {
4581
+ content = await promises.readFile(policyPath, "utf-8");
4582
+ } catch (err) {
4583
+ const code = err?.code;
4584
+ if (code === "ENOENT") {
4585
+ const defaultYaml = generateDefaultPolicyYaml();
4586
+ try {
4587
+ await promises.writeFile(policyPath, defaultYaml, "utf-8");
4588
+ await promises.chmod(policyPath, 384);
4589
+ } catch (writeErr) {
4590
+ console.warn(
4591
+ `Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
4592
+ );
4593
+ }
4594
+ return Object.freeze({ ...DEFAULT_POLICY });
4595
+ }
4596
+ throw new MalformedPrincipalPolicyError(
4597
+ policyPath,
4598
+ `read failed: ${err.message}`
4599
+ );
4600
+ }
4564
4601
  try {
4565
- const content = await promises.readFile(policyPath, "utf-8");
4566
4602
  const policy = parsePolicy(content);
4567
4603
  return Object.freeze(policy);
4568
- } catch {
4569
- const defaultYaml = generateDefaultPolicyYaml();
4570
- try {
4571
- await promises.writeFile(policyPath, defaultYaml, "utf-8");
4572
- await promises.chmod(policyPath, 384);
4573
- } catch {
4574
- }
4575
- return Object.freeze({ ...DEFAULT_POLICY });
4604
+ } catch (parseErr) {
4605
+ throw new MalformedPrincipalPolicyError(
4606
+ policyPath,
4607
+ parseErr.message
4608
+ );
4576
4609
  }
4577
4610
  }
4578
- var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
4611
+ var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4579
4612
  var init_loader = __esm({
4580
4613
  "src/principal-policy/loader.ts"() {
4581
4614
  DEFAULT_TIER2 = {
@@ -4708,6 +4741,20 @@ var init_loader = __esm({
4708
4741
  ],
4709
4742
  approval_channel: DEFAULT_CHANNEL
4710
4743
  };
4744
+ MalformedPrincipalPolicyError = class extends Error {
4745
+ constructor(policyPath, reason) {
4746
+ super(
4747
+ `Principal policy at ${policyPath} is malformed and cannot be loaded.
4748
+ Reason: ${reason}
4749
+ 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.`
4750
+ );
4751
+ this.policyPath = policyPath;
4752
+ this.reason = reason;
4753
+ this.name = "MalformedPrincipalPolicyError";
4754
+ }
4755
+ policyPath;
4756
+ reason;
4757
+ };
4711
4758
  }
4712
4759
  });
4713
4760
 
@@ -4927,7 +4974,7 @@ function deepSortKeys(obj) {
4927
4974
  return sorted;
4928
4975
  }
4929
4976
  function canonicalizeForSigning(body) {
4930
- return JSON.stringify(deepSortKeys(body));
4977
+ return JSON.stringify(deepSortKeys(body)).normalize("NFC");
4931
4978
  }
4932
4979
  var init_types = __esm({
4933
4980
  "src/shr/types.ts"() {
@@ -11574,6 +11621,7 @@ __export(passphrase_exports, {
11574
11621
  getOrCreatePassphrase: () => getOrCreatePassphrase,
11575
11622
  isOsKeyringLocation: () => isOsKeyringLocation,
11576
11623
  keychainServiceFor: () => keychainServiceFor,
11624
+ legacyKeychainServiceFor: () => legacyKeychainServiceFor,
11577
11625
  persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
11578
11626
  readStoredPassphrase: () => readStoredPassphrase
11579
11627
  });
@@ -11587,16 +11635,29 @@ async function getOrCreatePassphrase(opts = {}) {
11587
11635
  const plat = opts.platformOverride ?? os.platform();
11588
11636
  const exec2 = opts.exec ?? defaultExec;
11589
11637
  const derive = opts.deriveMachineKey ?? deriveMachineKey;
11638
+ const legacyService = legacyKeychainServiceFor(storagePath, home);
11590
11639
  if (plat === "darwin") {
11591
11640
  const fromKc = await readFromKeychain(exec2, service);
11592
11641
  if (fromKc) {
11593
11642
  return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11594
11643
  }
11644
+ if (legacyService !== service) {
11645
+ const fromLegacy = await readFromKeychain(exec2, legacyService);
11646
+ if (fromLegacy) {
11647
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11648
+ }
11649
+ }
11595
11650
  } else if (plat === "linux") {
11596
11651
  const fromSs = await readFromSecretService(exec2, service);
11597
11652
  if (fromSs) {
11598
11653
  return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11599
11654
  }
11655
+ if (legacyService !== service) {
11656
+ const fromLegacy = await readFromSecretService(exec2, legacyService);
11657
+ if (fromLegacy) {
11658
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11659
+ }
11660
+ }
11600
11661
  }
11601
11662
  const fallback = fallbackFilePath(home, storagePath);
11602
11663
  const fromFile = await readFromFallbackFile(fallback, home, derive);
@@ -11629,6 +11690,7 @@ async function readStoredPassphrase(opts = {}) {
11629
11690
  const home = opts.home ?? os.homedir();
11630
11691
  const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
11631
11692
  const service = keychainServiceFor(storagePath, home);
11693
+ const legacyService = legacyKeychainServiceFor(storagePath, home);
11632
11694
  const plat = opts.platformOverride ?? os.platform();
11633
11695
  const exec2 = opts.exec ?? defaultExec;
11634
11696
  const derive = opts.deriveMachineKey ?? deriveMachineKey;
@@ -11637,11 +11699,23 @@ async function readStoredPassphrase(opts = {}) {
11637
11699
  if (fromKc) {
11638
11700
  return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11639
11701
  }
11702
+ if (legacyService !== service) {
11703
+ const fromLegacy = await readFromKeychain(exec2, legacyService);
11704
+ if (fromLegacy) {
11705
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11706
+ }
11707
+ }
11640
11708
  } else if (plat === "linux") {
11641
11709
  const fromSs = await readFromSecretService(exec2, service);
11642
11710
  if (fromSs) {
11643
11711
  return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11644
11712
  }
11713
+ if (legacyService !== service) {
11714
+ const fromLegacy = await readFromSecretService(exec2, legacyService);
11715
+ if (fromLegacy) {
11716
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11717
+ }
11718
+ }
11645
11719
  }
11646
11720
  const fallback = fallbackFilePath(home, storagePath);
11647
11721
  const fromFile = await readFromFallbackFile(fallback, home, derive);
@@ -11722,9 +11796,18 @@ async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT)
11722
11796
  }
11723
11797
  }
11724
11798
  function keychainServiceFor(storagePath, home = os.homedir()) {
11725
- const defaultPath = path.join(home, DEFAULT_STORAGE_DIR);
11726
- if (storagePath === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11727
- const digest = sha256.sha256(Buffer.from(storagePath, "utf-8"));
11799
+ const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
11800
+ const canonicalStorage = path.resolve(storagePath);
11801
+ if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11802
+ const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
11803
+ const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
11804
+ return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
11805
+ }
11806
+ function legacyKeychainServiceFor(storagePath, home = os.homedir()) {
11807
+ const defaultPath = path.resolve(path.join(home, DEFAULT_STORAGE_DIR));
11808
+ const canonicalStorage = path.resolve(storagePath);
11809
+ if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11810
+ const digest = sha256.sha256(Buffer.from(canonicalStorage, "utf-8"));
11728
11811
  const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
11729
11812
  return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
11730
11813
  }
@@ -11811,7 +11894,7 @@ function deriveMachineKey(home) {
11811
11894
  return hkdf.hkdf(sha256.sha256, material, void 0, "sanctuary-passphrase-v1", 32);
11812
11895
  }
11813
11896
  async function defaultExec(cmd, args, input) {
11814
- return new Promise((resolve6, reject) => {
11897
+ return new Promise((resolve8, reject) => {
11815
11898
  const child = child_process.spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
11816
11899
  let stdout = "";
11817
11900
  let stderr = "";
@@ -11822,7 +11905,7 @@ async function defaultExec(cmd, args, input) {
11822
11905
  stderr += d.toString();
11823
11906
  });
11824
11907
  child.on("error", reject);
11825
- child.on("close", (code) => resolve6({ stdout, stderr, code }));
11908
+ child.on("close", (code) => resolve8({ stdout, stderr, code }));
11826
11909
  if (input !== void 0) {
11827
11910
  child.stdin.write(input);
11828
11911
  }
@@ -12016,7 +12099,7 @@ async function discoverTenants(options = {}) {
12016
12099
  for (const child of children) {
12017
12100
  const childPath = path.join(root, child);
12018
12101
  if (child.startsWith(".")) continue;
12019
- if (child === "state" || child === "backup" || child === "config") continue;
12102
+ if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
12020
12103
  const s = await promises.stat(childPath).catch(() => null);
12021
12104
  if (!s || !s.isDirectory()) continue;
12022
12105
  const desc = await describeTenant(child, childPath, home);
@@ -12028,6 +12111,17 @@ async function discoverTenants(options = {}) {
12028
12111
  const desc = await describeTenant(path.basename(extra), extra, home);
12029
12112
  if (desc) tenants.push(desc);
12030
12113
  }
12114
+ const seen = /* @__PURE__ */ new Map();
12115
+ for (const t of tenants) {
12116
+ seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
12117
+ }
12118
+ for (const [name, count] of seen) {
12119
+ if (count > 1) {
12120
+ console.error(
12121
+ `[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
12122
+ );
12123
+ }
12124
+ }
12031
12125
  tenants.sort((a, b) => {
12032
12126
  if (a.name === "default") return -1;
12033
12127
  if (b.name === "default") return 1;
@@ -12379,7 +12473,7 @@ var init_auth_middleware = __esm({
12379
12473
  });
12380
12474
 
12381
12475
  // src/hub/constants.ts
12382
- var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
12476
+ var HUB_API_PREFIX, HUB_ROUTES, HUB_FORTRESS_AGENT_ID_SENTINEL, HUB_INBOX_ACTIONS, HUB_AGENT_CONTROL_ACTIONS, HUB_TIER_1_AGENT_CONTROL_ACTIONS, HUB_ACTIVITY_DEFAULT_LIMIT, HUB_ACTIVITY_MAX_LIMIT, HUB_CHAT_THREADS_DEFAULT_LIMIT, HUB_CHAT_THREADS_MAX_LIMIT, HUB_CHAT_TURNS_DEFAULT_LIMIT, HUB_CHAT_TURNS_MAX_LIMIT, HUB_INBOX_DEFAULT_LIMIT, HUB_INBOX_MAX_LIMIT, HUB_AGENTS_DEFAULT_LIMIT, HUB_AGENTS_MAX_LIMIT, HUB_MAX_REQUEST_BODY_BYTES, HUB_CHAT_MESSAGE_MAX_CHARS, HUB_INBOX_TEMPLATE_NAMESPACES, HUB_ACTIVITY_TEMPLATE_NAMESPACES;
12383
12477
  var init_constants3 = __esm({
12384
12478
  "src/hub/constants.ts"() {
12385
12479
  HUB_API_PREFIX = "/api/hub";
@@ -12407,6 +12501,16 @@ var init_constants3 = __esm({
12407
12501
  */
12408
12502
  CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
12409
12503
  CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
12504
+ /**
12505
+ * Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
12506
+ * scrollback, and operator-initiated thread delete. Distinct from the
12507
+ * v1.2 `/history` route, which surfaces the active in-session thread
12508
+ * shape; the new routes target persisted multi-thread memory used by
12509
+ * v1.3 conversational sovereignty depth.
12510
+ */
12511
+ CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
12512
+ CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
12513
+ CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
12410
12514
  /**
12411
12515
  * Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
12412
12516
  * recent activity feed, pending Tier 1 approvals routed through this
@@ -12430,6 +12534,10 @@ var init_constants3 = __esm({
12430
12534
  ];
12431
12535
  HUB_ACTIVITY_DEFAULT_LIMIT = 50;
12432
12536
  HUB_ACTIVITY_MAX_LIMIT = 500;
12537
+ HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
12538
+ HUB_CHAT_THREADS_MAX_LIMIT = 500;
12539
+ HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
12540
+ HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
12433
12541
  HUB_INBOX_DEFAULT_LIMIT = 100;
12434
12542
  HUB_INBOX_MAX_LIMIT = 500;
12435
12543
  HUB_AGENTS_DEFAULT_LIMIT = 100;
@@ -12612,6 +12720,23 @@ function checkChatMessage(value) {
12612
12720
  }
12613
12721
  return trimmed;
12614
12722
  }
12723
+ function matchConciergeThreadRoute(path) {
12724
+ const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
12725
+ if (!path.startsWith(prefix)) return null;
12726
+ const rest = path.slice(prefix.length);
12727
+ if (rest.length === 0 || rest.includes("/")) return null;
12728
+ const decoded = decodeURIComponent(rest);
12729
+ if (decoded.length === 0) return null;
12730
+ return { threadId: decoded };
12731
+ }
12732
+ function parseSince(raw) {
12733
+ if (raw === null || raw === "") return void 0;
12734
+ const parsed = Number.parseInt(raw, 10);
12735
+ if (Number.isNaN(parsed) || parsed < 0) {
12736
+ throw new HubValidationError("since must be a non-negative integer");
12737
+ }
12738
+ return parsed;
12739
+ }
12615
12740
  function matchInboxRoute(path) {
12616
12741
  const prefix = `${HUB_API_PREFIX}/inbox/`;
12617
12742
  if (!path.startsWith(prefix)) return null;
@@ -12795,6 +12920,47 @@ async function handleHubRoute(deps, req, res) {
12795
12920
  writeJSON2(res, 200, { ok: true, data: { messages } });
12796
12921
  return true;
12797
12922
  }
12923
+ if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
12924
+ const limit = parseLimit(
12925
+ url.searchParams.get("limit"),
12926
+ HUB_CHAT_THREADS_DEFAULT_LIMIT,
12927
+ HUB_CHAT_THREADS_MAX_LIMIT
12928
+ );
12929
+ const threads = await deps.service.listConciergeMemoryThreads({ limit });
12930
+ writeJSON2(res, 200, { ok: true, data: { threads } });
12931
+ return true;
12932
+ }
12933
+ {
12934
+ const threadMatch = matchConciergeThreadRoute(path);
12935
+ if (threadMatch) {
12936
+ if (method === "GET") {
12937
+ const since = parseSince(url.searchParams.get("since"));
12938
+ const limit = parseLimit(
12939
+ url.searchParams.get("limit"),
12940
+ HUB_CHAT_TURNS_DEFAULT_LIMIT,
12941
+ HUB_CHAT_TURNS_MAX_LIMIT
12942
+ );
12943
+ const readOpts = { limit };
12944
+ if (since !== void 0) readOpts.sinceTurnId = since;
12945
+ const turns = await deps.service.readConciergeMemoryThread(
12946
+ threadMatch.threadId,
12947
+ readOpts
12948
+ );
12949
+ writeJSON2(res, 200, { ok: true, data: { turns } });
12950
+ return true;
12951
+ }
12952
+ if (method === "DELETE") {
12953
+ const removed = await deps.service.deleteConciergeMemoryThread(
12954
+ threadMatch.threadId
12955
+ );
12956
+ writeJSON2(res, removed ? 200 : 404, {
12957
+ ok: removed,
12958
+ data: { thread_id: threadMatch.threadId, removed }
12959
+ });
12960
+ return true;
12961
+ }
12962
+ }
12963
+ }
12798
12964
  writeJSON2(res, 404, { ok: false, error: "not_found", path });
12799
12965
  return true;
12800
12966
  } catch (err) {
@@ -16826,6 +16992,8 @@ var init_intelligence_api_router = __esm({
16826
16992
  this.code = code;
16827
16993
  this.name = "IntelligenceRouterError";
16828
16994
  }
16995
+ statusCode;
16996
+ code;
16829
16997
  };
16830
16998
  }
16831
16999
  });
@@ -16915,6 +17083,168 @@ var init_dispatch = __esm({
16915
17083
  init_intelligence_api_router();
16916
17084
  }
16917
17085
  });
17086
+
17087
+ // src/principal-policy/approval-aggregator-routes.ts
17088
+ function writeJSON4(res, status, payload) {
17089
+ res.writeHead(status, {
17090
+ "Content-Type": "application/json",
17091
+ "Cache-Control": "no-store"
17092
+ });
17093
+ res.end(JSON.stringify(payload));
17094
+ }
17095
+ function parseLimit2(raw, defaultValue, max) {
17096
+ if (raw === null || raw === "") return defaultValue;
17097
+ const parsed = Number.parseInt(raw, 10);
17098
+ if (Number.isNaN(parsed) || parsed < 0) {
17099
+ return defaultValue;
17100
+ }
17101
+ return Math.min(parsed, max);
17102
+ }
17103
+ function isStatusFilter(value) {
17104
+ return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
17105
+ }
17106
+ function matchEntryRoute(path) {
17107
+ const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
17108
+ if (!path.startsWith(prefix)) return null;
17109
+ const rest = path.slice(prefix.length);
17110
+ if (rest.length === 0) return null;
17111
+ const slash = rest.indexOf("/");
17112
+ if (slash === -1) {
17113
+ return { aggregatorId: decodeURIComponent(rest), action: null };
17114
+ }
17115
+ return {
17116
+ aggregatorId: decodeURIComponent(rest.slice(0, slash)),
17117
+ action: rest.slice(slash + 1)
17118
+ };
17119
+ }
17120
+ async function handleStream2(deps, res) {
17121
+ res.writeHead(200, {
17122
+ "Content-Type": "text/event-stream",
17123
+ "Cache-Control": "no-cache, no-transform",
17124
+ Connection: "keep-alive",
17125
+ "X-Accel-Buffering": "no"
17126
+ });
17127
+ const initial = await deps.aggregator.list({ status: "pending" });
17128
+ res.write(
17129
+ `event: approval_inbox_snapshot
17130
+ data: ${JSON.stringify({ entries: initial })}
17131
+
17132
+ `
17133
+ );
17134
+ const unsubscribe = deps.aggregator.onEvent((event) => {
17135
+ try {
17136
+ res.write(
17137
+ `event: approval_inbox_${event.type}
17138
+ data: ${JSON.stringify(event.entry)}
17139
+
17140
+ `
17141
+ );
17142
+ } catch {
17143
+ }
17144
+ });
17145
+ const keepAlive = setInterval(() => {
17146
+ try {
17147
+ res.write(": keepalive\n\n");
17148
+ } catch {
17149
+ }
17150
+ }, 25e3);
17151
+ const cleanup = () => {
17152
+ clearInterval(keepAlive);
17153
+ unsubscribe();
17154
+ };
17155
+ res.on("close", cleanup);
17156
+ res.on("error", cleanup);
17157
+ }
17158
+ async function handleApprovalInboxRoute(deps, req, res) {
17159
+ const host = req.headers.host || "localhost";
17160
+ const url = new URL(req.url ?? "/", `http://${host}`);
17161
+ const method = (req.method ?? "GET").toUpperCase();
17162
+ const path = url.pathname;
17163
+ if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
17164
+ return false;
17165
+ }
17166
+ const checkAuth = authMiddleware(deps.authConfig);
17167
+ if (!checkAuth(req, res, url)) return true;
17168
+ try {
17169
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
17170
+ await handleStream2(deps, res);
17171
+ return true;
17172
+ }
17173
+ if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
17174
+ const limit = parseLimit2(
17175
+ url.searchParams.get("limit"),
17176
+ APPROVAL_INBOX_DEFAULT_LIMIT,
17177
+ APPROVAL_INBOX_MAX_LIMIT
17178
+ );
17179
+ const statusRaw = url.searchParams.get("status");
17180
+ const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
17181
+ const sinceTs = url.searchParams.get("since") ?? void 0;
17182
+ const entries = await deps.aggregator.list({
17183
+ status,
17184
+ limit,
17185
+ ...sinceTs !== void 0 ? { sinceTs } : {}
17186
+ });
17187
+ writeJSON4(res, 200, { ok: true, data: { entries } });
17188
+ return true;
17189
+ }
17190
+ const entryMatch = matchEntryRoute(path);
17191
+ if (entryMatch === null) {
17192
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
17193
+ return true;
17194
+ }
17195
+ if (method === "GET" && entryMatch.action === null) {
17196
+ const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
17197
+ const entry = entries.find(
17198
+ (e) => e.aggregator_id === entryMatch.aggregatorId
17199
+ );
17200
+ if (!entry) {
17201
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17202
+ return true;
17203
+ }
17204
+ const payload = await deps.aggregator.getFullPayload(
17205
+ entryMatch.aggregatorId
17206
+ );
17207
+ writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
17208
+ return true;
17209
+ }
17210
+ if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
17211
+ const decision = entryMatch.action === "approve" ? "approved" : "denied";
17212
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
17213
+ try {
17214
+ const entry = await deps.aggregator.resolve(
17215
+ entryMatch.aggregatorId,
17216
+ decision,
17217
+ operatorId
17218
+ );
17219
+ writeJSON4(res, 200, { ok: true, data: { entry } });
17220
+ } catch (err) {
17221
+ const msg = err instanceof Error ? err.message : String(err);
17222
+ if (msg === "approval-aggregator: not_found") {
17223
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17224
+ } else {
17225
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
17226
+ }
17227
+ }
17228
+ return true;
17229
+ }
17230
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
17231
+ return true;
17232
+ } catch (err) {
17233
+ const msg = err instanceof Error ? err.message : String(err);
17234
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
17235
+ return true;
17236
+ }
17237
+ }
17238
+ var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
17239
+ var init_approval_aggregator_routes = __esm({
17240
+ "src/principal-policy/approval-aggregator-routes.ts"() {
17241
+ init_auth_middleware();
17242
+ APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
17243
+ APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
17244
+ APPROVAL_INBOX_DEFAULT_LIMIT = 50;
17245
+ APPROVAL_INBOX_MAX_LIMIT = 200;
17246
+ }
17247
+ });
16918
17248
  function isDashboardViewRoute(method, path) {
16919
17249
  if (method !== "GET") return false;
16920
17250
  return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
@@ -16928,6 +17258,7 @@ var init_dashboard = __esm({
16928
17258
  init_fortress_view();
16929
17259
  init_system_prompt_generator();
16930
17260
  init_dispatch();
17261
+ init_approval_aggregator_routes();
16931
17262
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
16932
17263
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
16933
17264
  MAX_SESSIONS = 1e3;
@@ -16987,6 +17318,14 @@ var init_dashboard = __esm({
16987
17318
  * regardless. Default route flip is deferred to v1.2.
16988
17319
  */
16989
17320
  v11Bindings = null;
17321
+ /**
17322
+ * v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
17323
+ * additively at `/api/approval-inbox/*` when set. Legacy approval
17324
+ * routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
17325
+ * aggregator is a passive subscriber to the gate; the routes here are
17326
+ * the operator-facing query / decision surface.
17327
+ */
17328
+ approvalAggregator = null;
16990
17329
  constructor(config) {
16991
17330
  this.config = config;
16992
17331
  this.authToken = config.auth_token;
@@ -17037,6 +17376,34 @@ var init_dashboard = __esm({
17037
17376
  setV11Bindings(bindings) {
17038
17377
  this.v11Bindings = bindings;
17039
17378
  }
17379
+ /**
17380
+ * v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
17381
+ * aggregator. Once set, requests to `/api/approval-inbox/*` route
17382
+ * through `handleApprovalInboxRoute`. Pass `null` to detach (used by
17383
+ * tests + during shutdown).
17384
+ */
17385
+ setApprovalAggregator(aggregator) {
17386
+ this.approvalAggregator = aggregator;
17387
+ }
17388
+ /**
17389
+ * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
17390
+ * before the legacy approval route table. Returns true when served.
17391
+ */
17392
+ async dispatchApprovalInbox(req, res) {
17393
+ if (!this.approvalAggregator) return false;
17394
+ return handleApprovalInboxRoute(
17395
+ {
17396
+ authConfig: {
17397
+ loopbackAutoAuth: this._autoAuthLocalhost,
17398
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
17399
+ },
17400
+ aggregator: this.approvalAggregator,
17401
+ operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
17402
+ },
17403
+ req,
17404
+ res
17405
+ );
17406
+ }
17040
17407
  /**
17041
17408
  * v1.1 dispatch entry point. Called from `handleRequest` before the
17042
17409
  * legacy route table. Returns true when the request was served by v1.1
@@ -17102,7 +17469,7 @@ var init_dashboard = __esm({
17102
17469
  server = http.createServer(handler);
17103
17470
  }
17104
17471
  this.httpServer = server;
17105
- return new Promise((resolve6, reject) => {
17472
+ return new Promise((resolve8, reject) => {
17106
17473
  const protocol = this.useTLS ? "https" : "http";
17107
17474
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
17108
17475
  server.listen(this.config.port, this.config.host, () => {
@@ -17127,7 +17494,7 @@ var init_dashboard = __esm({
17127
17494
  if (shouldAutoOpen) {
17128
17495
  this.openInBrowser(sessionUrl);
17129
17496
  }
17130
- resolve6();
17497
+ resolve8();
17131
17498
  });
17132
17499
  server.on("error", (err) => {
17133
17500
  if (err.code === "EADDRINUSE") {
@@ -17173,8 +17540,8 @@ var init_dashboard = __esm({
17173
17540
  }
17174
17541
  this.rateLimits.clear();
17175
17542
  if (this.httpServer) {
17176
- return new Promise((resolve6) => {
17177
- this.httpServer.close(() => resolve6());
17543
+ return new Promise((resolve8) => {
17544
+ this.httpServer.close(() => resolve8());
17178
17545
  });
17179
17546
  }
17180
17547
  }
@@ -17188,7 +17555,7 @@ var init_dashboard = __esm({
17188
17555
  `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
17189
17556
  `
17190
17557
  );
17191
- return new Promise((resolve6) => {
17558
+ return new Promise((resolve8) => {
17192
17559
  const timer = setTimeout(() => {
17193
17560
  this.pending.delete(id);
17194
17561
  const response = {
@@ -17202,12 +17569,12 @@ var init_dashboard = __esm({
17202
17569
  decision: response.decision,
17203
17570
  decided_by: "timeout"
17204
17571
  });
17205
- resolve6(response);
17572
+ resolve8(response);
17206
17573
  }, this.config.timeout_seconds * 1e3);
17207
17574
  const pending = {
17208
17575
  id,
17209
17576
  request,
17210
- resolve: resolve6,
17577
+ resolve: resolve8,
17211
17578
  timer,
17212
17579
  created_at: (/* @__PURE__ */ new Date()).toISOString()
17213
17580
  };
@@ -17412,6 +17779,18 @@ var init_dashboard = __esm({
17412
17779
  res.end();
17413
17780
  return;
17414
17781
  }
17782
+ if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
17783
+ this.dispatchApprovalInbox(req, res).then((handled) => {
17784
+ if (handled) return;
17785
+ this.handleLegacyRequest(req, res, url, method);
17786
+ }).catch(() => {
17787
+ if (!res.headersSent) {
17788
+ res.writeHead(500, { "Content-Type": "application/json" });
17789
+ res.end(JSON.stringify({ error: "Internal server error" }));
17790
+ }
17791
+ });
17792
+ return;
17793
+ }
17415
17794
  if (this.v11Bindings) {
17416
17795
  this.dispatchV11(req, res, url, method).then((handled) => {
17417
17796
  if (handled) return;
@@ -18073,7 +18452,7 @@ var init_webhook = __esm({
18073
18452
  * Start the callback listener server.
18074
18453
  */
18075
18454
  async start() {
18076
- return new Promise((resolve6, reject) => {
18455
+ return new Promise((resolve8, reject) => {
18077
18456
  this.callbackServer = http.createServer(
18078
18457
  (req, res) => this.handleCallback(req, res)
18079
18458
  );
@@ -18088,7 +18467,7 @@ var init_webhook = __esm({
18088
18467
 
18089
18468
  `
18090
18469
  );
18091
- resolve6();
18470
+ resolve8();
18092
18471
  }
18093
18472
  );
18094
18473
  this.callbackServer.on("error", reject);
@@ -18108,8 +18487,8 @@ var init_webhook = __esm({
18108
18487
  }
18109
18488
  this.pending.clear();
18110
18489
  if (this.callbackServer) {
18111
- return new Promise((resolve6) => {
18112
- this.callbackServer.close(() => resolve6());
18490
+ return new Promise((resolve8) => {
18491
+ this.callbackServer.close(() => resolve8());
18113
18492
  });
18114
18493
  }
18115
18494
  }
@@ -18122,7 +18501,7 @@ var init_webhook = __esm({
18122
18501
  `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
18123
18502
  `
18124
18503
  );
18125
- return new Promise((resolve6) => {
18504
+ return new Promise((resolve8) => {
18126
18505
  const timer = setTimeout(() => {
18127
18506
  this.pending.delete(id);
18128
18507
  const response = {
@@ -18131,12 +18510,12 @@ var init_webhook = __esm({
18131
18510
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
18132
18511
  decided_by: "timeout"
18133
18512
  };
18134
- resolve6(response);
18513
+ resolve8(response);
18135
18514
  }, this.config.timeout_seconds * 1e3);
18136
18515
  const pending = {
18137
18516
  id,
18138
18517
  request,
18139
- resolve: resolve6,
18518
+ resolve: resolve8,
18140
18519
  timer,
18141
18520
  created_at: (/* @__PURE__ */ new Date()).toISOString()
18142
18521
  };
@@ -19429,12 +19808,25 @@ var init_injection_detector = __esm({
19429
19808
  }
19430
19809
  });
19431
19810
 
19811
+ // src/principal-policy/deny-vocabulary.ts
19812
+ var AGENT_VISIBLE_DENY_REASONS;
19813
+ var init_deny_vocabulary = __esm({
19814
+ "src/principal-policy/deny-vocabulary.ts"() {
19815
+ AGENT_VISIBLE_DENY_REASONS = {
19816
+ REQUIRES_APPROVAL: "operation requires operator approval",
19817
+ NOT_PERMITTED: "operation not permitted",
19818
+ REQUIRES_OPERATOR: "operation requires operator action"
19819
+ };
19820
+ }
19821
+ });
19822
+
19432
19823
  // src/principal-policy/gate.ts
19433
19824
  var ApprovalGate;
19434
19825
  var init_gate = __esm({
19435
19826
  "src/principal-policy/gate.ts"() {
19436
19827
  init_loader();
19437
19828
  init_injection_detector();
19829
+ init_deny_vocabulary();
19438
19830
  ApprovalGate = class {
19439
19831
  policy;
19440
19832
  baseline;
@@ -19442,14 +19834,25 @@ var init_gate = __esm({
19442
19834
  auditLog;
19443
19835
  injectionDetector;
19444
19836
  onInjectionAlert;
19837
+ onApprovalEvent;
19445
19838
  proxyTierResolver;
19446
- constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
19839
+ constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
19447
19840
  this.policy = policy;
19448
19841
  this.baseline = baseline;
19449
19842
  this.channel = channel;
19450
19843
  this.auditLog = auditLog;
19451
19844
  this.injectionDetector = injectionDetector ?? new InjectionDetector();
19452
19845
  this.onInjectionAlert = onInjectionAlert;
19846
+ this.onApprovalEvent = onApprovalEvent;
19847
+ }
19848
+ /**
19849
+ * Set the approval-event callback after construction. Used by the
19850
+ * Upsilon-1 wire-up when the aggregator is constructed alongside the
19851
+ * gate. The aggregator subscribes through this setter rather than the
19852
+ * constructor so existing call sites continue to work unchanged.
19853
+ */
19854
+ setApprovalEventCallback(cb) {
19855
+ this.onApprovalEvent = cb;
19453
19856
  }
19454
19857
  /**
19455
19858
  * Set the proxy tier resolver. Called after the proxy router is initialized.
@@ -19486,10 +19889,16 @@ var init_gate = __esm({
19486
19889
  });
19487
19890
  }
19488
19891
  if (injectionResult.recommendation === "block") {
19892
+ this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
19893
+ tier: 1,
19894
+ operation,
19895
+ injection_confidence: injectionResult.confidence,
19896
+ signal_count: injectionResult.signals.length
19897
+ });
19489
19898
  return {
19490
19899
  allowed: false,
19491
19900
  tier: 1,
19492
- reason: `Blocked: prompt injection detected in "${operation}" (confidence: ${(injectionResult.confidence * 100).toFixed(0)}%)`,
19901
+ reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
19493
19902
  approval_required: false
19494
19903
  };
19495
19904
  }
@@ -19566,7 +19975,7 @@ var init_gate = __esm({
19566
19975
  this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
19567
19976
  tier: 1,
19568
19977
  operation,
19569
- warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
19978
+ warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
19570
19979
  });
19571
19980
  return this.requestApproval(
19572
19981
  operation,
@@ -19677,25 +20086,109 @@ var init_gate = __esm({
19677
20086
  }
19678
20087
  /**
19679
20088
  * Request approval from the human principal.
20089
+ *
20090
+ * Fail-closed contract (full-sweep #49): if the channel throws (network
20091
+ * down, callback unreachable, dashboard SSE peer dropped, webhook DNS
20092
+ * failure, etc.), the gate denies the operation and audit-logs the cause.
20093
+ * Channel-internal timeouts already resolve with decision: "deny" per
20094
+ * SEC-002; this catch covers the remaining "channel raised" path so an
20095
+ * unhandled rejection cannot turn into an indeterminate state at the gate.
19680
20096
  */
19681
20097
  async requestApproval(operation, tier, reason, context) {
20098
+ const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
19682
20099
  const request = {
19683
20100
  operation,
19684
20101
  tier,
19685
20102
  reason,
19686
20103
  context,
19687
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
20104
+ timestamp: requestTimestamp
19688
20105
  };
19689
- const response = await this.channel.requestApproval(request);
20106
+ const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
20107
+ if (this.onApprovalEvent) {
20108
+ try {
20109
+ this.onApprovalEvent({
20110
+ phase: "requested",
20111
+ operation,
20112
+ tier,
20113
+ reason,
20114
+ context,
20115
+ request_timestamp: requestTimestamp,
20116
+ correlation_id: correlationId
20117
+ });
20118
+ } catch {
20119
+ }
20120
+ }
20121
+ let response;
20122
+ try {
20123
+ response = await this.channel.requestApproval(request);
20124
+ } catch (err) {
20125
+ const errMessage = err instanceof Error ? err.message : String(err);
20126
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
20127
+ this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
20128
+ tier,
20129
+ reason,
20130
+ decided_by: "channel_failure",
20131
+ channel_error: errMessage
20132
+ });
20133
+ if (this.onApprovalEvent) {
20134
+ try {
20135
+ this.onApprovalEvent({
20136
+ phase: "resolved",
20137
+ operation,
20138
+ tier,
20139
+ reason,
20140
+ context,
20141
+ request_timestamp: requestTimestamp,
20142
+ resolution: {
20143
+ decision: "deny",
20144
+ decided_at: decidedAt,
20145
+ decided_by: "channel_failure"
20146
+ },
20147
+ correlation_id: correlationId
20148
+ });
20149
+ } catch {
20150
+ }
20151
+ }
20152
+ return {
20153
+ allowed: false,
20154
+ tier,
20155
+ reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
20156
+ approval_required: true,
20157
+ approval_response: {
20158
+ decision: "deny",
20159
+ decided_at: decidedAt,
20160
+ decided_by: "channel_failure"
20161
+ }
20162
+ };
20163
+ }
19690
20164
  this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
19691
20165
  tier,
19692
20166
  reason,
19693
20167
  decided_by: response.decided_by
19694
20168
  });
20169
+ if (this.onApprovalEvent) {
20170
+ try {
20171
+ this.onApprovalEvent({
20172
+ phase: "resolved",
20173
+ operation,
20174
+ tier,
20175
+ reason,
20176
+ context,
20177
+ request_timestamp: requestTimestamp,
20178
+ resolution: {
20179
+ decision: response.decision,
20180
+ decided_at: response.decided_at,
20181
+ decided_by: response.decided_by
20182
+ },
20183
+ correlation_id: correlationId
20184
+ });
20185
+ } catch {
20186
+ }
20187
+ }
19695
20188
  return {
19696
20189
  allowed: response.decision === "approve",
19697
20190
  tier,
19698
- reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : `Tier ${tier} operation requires approval`,
20191
+ reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
19699
20192
  approval_required: true,
19700
20193
  approval_response: response
19701
20194
  };
@@ -19726,6 +20219,356 @@ var init_gate = __esm({
19726
20219
  };
19727
20220
  }
19728
20221
  });
20222
+ var APPROVAL_AGGREGATOR_NAMESPACE, APPROVAL_AGGREGATOR_HKDF_INFO, APPROVAL_AGGREGATOR_AUDIT_OPS, DEFAULT_PENDING_TTL_MS, DEFAULT_MAX_LIST_LIMIT, DEFAULT_LIST_PAGE_SIZE, ApprovalAggregator;
20223
+ var init_approval_aggregator = __esm({
20224
+ "src/principal-policy/approval-aggregator.ts"() {
20225
+ init_encryption();
20226
+ init_key_derivation();
20227
+ init_encoding();
20228
+ APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
20229
+ APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
20230
+ APPROVAL_AGGREGATOR_AUDIT_OPS = {
20231
+ AGGREGATED: "cross_harness_approval_aggregated",
20232
+ RESOLVED: "cross_harness_approval_resolved",
20233
+ DEDUPED: "cross_harness_approval_deduped"
20234
+ };
20235
+ DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
20236
+ DEFAULT_MAX_LIST_LIMIT = 200;
20237
+ DEFAULT_LIST_PAGE_SIZE = 50;
20238
+ ApprovalAggregator = class {
20239
+ storage;
20240
+ encryptionKey;
20241
+ auditLog;
20242
+ identityId;
20243
+ fortressId;
20244
+ pendingTtlMs;
20245
+ maxListLimit;
20246
+ now;
20247
+ resolveSourceContext;
20248
+ resolveHubInboxItemId;
20249
+ /** Cached entries by `aggregator_id`. */
20250
+ entries = /* @__PURE__ */ new Map();
20251
+ /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
20252
+ dedupIndex = /* @__PURE__ */ new Map();
20253
+ /** Correlation index: gate `correlation_id` -> aggregator_id. */
20254
+ correlationIndex = /* @__PURE__ */ new Map();
20255
+ /** Original request payloads kept in-memory for `getFullPayload()`. */
20256
+ fullPayloads = /* @__PURE__ */ new Map();
20257
+ /** Has the aggregator hydrated persisted entries on this process? */
20258
+ hydrated = false;
20259
+ /** Active SSE listeners. */
20260
+ listeners = /* @__PURE__ */ new Set();
20261
+ constructor(deps) {
20262
+ this.storage = deps.storage;
20263
+ this.encryptionKey = derivePurposeKey(
20264
+ deps.masterKey,
20265
+ APPROVAL_AGGREGATOR_HKDF_INFO
20266
+ );
20267
+ this.auditLog = deps.auditLog;
20268
+ this.identityId = deps.identityId;
20269
+ this.fortressId = deps.fortressId;
20270
+ this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
20271
+ this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
20272
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
20273
+ this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
20274
+ source_harness: this.fortressId,
20275
+ source_agent_id: this.fortressId
20276
+ }));
20277
+ this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
20278
+ }
20279
+ /**
20280
+ * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
20281
+ * use this to forward aggregator emissions to the dashboard.
20282
+ */
20283
+ onEvent(listener) {
20284
+ this.listeners.add(listener);
20285
+ return () => this.listeners.delete(listener);
20286
+ }
20287
+ /**
20288
+ * Ingest a gate event. Returns the aggregator entry on first sight,
20289
+ * `null` when deduped. Resolution events update the existing record;
20290
+ * unmatched resolutions are dropped silently (caller's gate emitted a
20291
+ * resolved-without-requested pair, which the aggregator does not invent
20292
+ * a record for).
20293
+ */
20294
+ async ingest(event) {
20295
+ await this.hydrate();
20296
+ if (event.phase === "requested") {
20297
+ return this.ingestRequested(event);
20298
+ }
20299
+ if (event.phase === "resolved") {
20300
+ return this.ingestResolved(event);
20301
+ }
20302
+ return null;
20303
+ }
20304
+ /**
20305
+ * List pending or recently resolved entries. Pending entries past TTL
20306
+ * are lazily transitioned to `expired` and persisted before the list
20307
+ * snapshot is returned.
20308
+ */
20309
+ async list(opts) {
20310
+ await this.hydrate();
20311
+ await this.expireStale();
20312
+ const limit = Math.min(
20313
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
20314
+ this.maxListLimit
20315
+ );
20316
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
20317
+ const matching = [];
20318
+ for (const entry of this.entries.values()) {
20319
+ if (opts?.status && entry.status !== opts.status) continue;
20320
+ if (Date.parse(entry.created_at) < sinceMs) continue;
20321
+ matching.push(entry);
20322
+ }
20323
+ matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
20324
+ return matching.slice(0, limit);
20325
+ }
20326
+ /**
20327
+ * Return the original (unhashed) request payload for the entry. Returns
20328
+ * `null` when the entry is unknown or the payload was evicted (e.g. the
20329
+ * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
20330
+ */
20331
+ async getFullPayload(aggregatorId) {
20332
+ await this.hydrate();
20333
+ if (!this.entries.has(aggregatorId)) return null;
20334
+ return this.fullPayloads.get(aggregatorId) ?? null;
20335
+ }
20336
+ /**
20337
+ * Resolve an entry. Used by both:
20338
+ * 1. The gate wire-up on channel-decision return.
20339
+ * 2. The HTTP `approve`/`deny` routes when an operator clicks.
20340
+ *
20341
+ * Idempotent: resolving an already-resolved entry is a no-op (the record
20342
+ * keeps its first decision and the audit log is not double-fired).
20343
+ * Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
20344
+ * routes return 404.
20345
+ */
20346
+ async resolve(aggregatorId, decision, operatorId) {
20347
+ await this.hydrate();
20348
+ const entry = this.entries.get(aggregatorId);
20349
+ if (!entry) {
20350
+ throw new Error("approval-aggregator: not_found");
20351
+ }
20352
+ if (entry.status !== "pending") {
20353
+ return entry;
20354
+ }
20355
+ entry.status = decision;
20356
+ entry.resolved_at = this.now().toISOString();
20357
+ entry.resolved_by = operatorId;
20358
+ await this.persist(entry);
20359
+ this.auditLog.append(
20360
+ "l2",
20361
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20362
+ this.identityId,
20363
+ {
20364
+ aggregator_id: entry.aggregator_id,
20365
+ source_harness: entry.source_harness,
20366
+ source_agent_id: entry.source_agent_id,
20367
+ audit_log_entry_id: entry.audit_log_entry_id,
20368
+ policy_rule_id: entry.policy_rule_id,
20369
+ decision,
20370
+ decided_by: operatorId,
20371
+ decided_at: entry.resolved_at
20372
+ }
20373
+ );
20374
+ this.emit({ type: "resolved", entry: { ...entry } });
20375
+ return entry;
20376
+ }
20377
+ // ── Internal: ingest paths ─────────────────────────────────────────────
20378
+ async ingestRequested(event) {
20379
+ const ctx = this.resolveSourceContext(event);
20380
+ const auditId = this.auditEntryIdForEvent(event);
20381
+ const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
20382
+ const existing = this.dedupIndex.get(dedupKey);
20383
+ if (existing) {
20384
+ const existingEntry = this.entries.get(existing);
20385
+ if (existingEntry) {
20386
+ this.correlationIndex.set(event.correlation_id, existing);
20387
+ this.auditLog.append(
20388
+ "l2",
20389
+ APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
20390
+ this.identityId,
20391
+ {
20392
+ aggregator_id: existing,
20393
+ source_harness: ctx.source_harness,
20394
+ source_agent_id: ctx.source_agent_id,
20395
+ audit_log_entry_id: auditId,
20396
+ policy_rule_id: this.derivePolicyRuleId(event),
20397
+ correlation_id: event.correlation_id
20398
+ }
20399
+ );
20400
+ this.emit({ type: "deduped", entry: { ...existingEntry } });
20401
+ return null;
20402
+ }
20403
+ }
20404
+ const id = crypto.randomUUID();
20405
+ const now = this.now();
20406
+ const expires = new Date(now.getTime() + this.pendingTtlMs);
20407
+ const hubInboxId = this.resolveHubInboxItemId(event);
20408
+ const entry = {
20409
+ aggregator_id: id,
20410
+ source_harness: ctx.source_harness,
20411
+ source_agent_id: ctx.source_agent_id,
20412
+ audit_log_entry_id: auditId,
20413
+ policy_rule_id: this.derivePolicyRuleId(event),
20414
+ action_summary: this.deriveActionSummary(event),
20415
+ request_payload_hash: this.hashPayload(event.context),
20416
+ status: "pending",
20417
+ created_at: now.toISOString(),
20418
+ expires_at: expires.toISOString(),
20419
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
20420
+ };
20421
+ this.entries.set(id, entry);
20422
+ this.dedupIndex.set(dedupKey, id);
20423
+ this.correlationIndex.set(event.correlation_id, id);
20424
+ this.fullPayloads.set(id, event.context);
20425
+ await this.persist(entry);
20426
+ this.auditLog.append(
20427
+ "l2",
20428
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
20429
+ this.identityId,
20430
+ {
20431
+ aggregator_id: id,
20432
+ source_harness: ctx.source_harness,
20433
+ source_agent_id: ctx.source_agent_id,
20434
+ audit_log_entry_id: auditId,
20435
+ policy_rule_id: entry.policy_rule_id,
20436
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
20437
+ }
20438
+ );
20439
+ this.emit({ type: "aggregated", entry: { ...entry } });
20440
+ return entry;
20441
+ }
20442
+ async ingestResolved(event) {
20443
+ const id = this.correlationIndex.get(event.correlation_id);
20444
+ if (!id) return null;
20445
+ const entry = this.entries.get(id);
20446
+ if (!entry) return null;
20447
+ if (entry.status !== "pending") return entry;
20448
+ if (!event.resolution) return entry;
20449
+ const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
20450
+ const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
20451
+ entry.status = status;
20452
+ entry.resolved_at = event.resolution.decided_at;
20453
+ entry.resolved_by = event.resolution.decided_by;
20454
+ await this.persist(entry);
20455
+ this.auditLog.append(
20456
+ "l2",
20457
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20458
+ this.identityId,
20459
+ {
20460
+ aggregator_id: id,
20461
+ source_harness: entry.source_harness,
20462
+ source_agent_id: entry.source_agent_id,
20463
+ audit_log_entry_id: entry.audit_log_entry_id,
20464
+ policy_rule_id: entry.policy_rule_id,
20465
+ decision: status,
20466
+ decided_by: entry.resolved_by,
20467
+ decided_at: entry.resolved_at,
20468
+ fail_closed: failClosed
20469
+ }
20470
+ );
20471
+ this.emit({ type: "resolved", entry: { ...entry } });
20472
+ return entry;
20473
+ }
20474
+ // ── Internal: helpers ──────────────────────────────────────────────────
20475
+ /**
20476
+ * Audit-log entry id for the dedup tuple. The audit log itself does not
20477
+ * surface a stable per-entry id (counter-prefixed keys are internal); the
20478
+ * aggregator uses the request timestamp + operation, which together pin
20479
+ * the audit entry the gate appended on the same call.
20480
+ */
20481
+ auditEntryIdForEvent(event) {
20482
+ return `${event.request_timestamp}:${event.operation}`;
20483
+ }
20484
+ derivePolicyRuleId(event) {
20485
+ return `tier${event.tier}:${event.operation}`;
20486
+ }
20487
+ deriveActionSummary(event) {
20488
+ return `${event.operation} (tier ${event.tier})`;
20489
+ }
20490
+ /**
20491
+ * Canonical SHA-256 of the request context. Sorted-keys serialization so
20492
+ * identical payloads always hash the same, even when key insertion order
20493
+ * varies. Defends against payload-replay smuggling (the aggregator can
20494
+ * tell the same payload was seen twice without storing it cleartext).
20495
+ */
20496
+ hashPayload(payload) {
20497
+ const canonical = JSON.stringify(payload, Object.keys(payload).sort());
20498
+ return crypto.createHash("sha256").update(canonical).digest("hex");
20499
+ }
20500
+ emit(event) {
20501
+ for (const listener of this.listeners) {
20502
+ try {
20503
+ listener(event);
20504
+ } catch {
20505
+ }
20506
+ }
20507
+ }
20508
+ async expireStale() {
20509
+ const nowMs = this.now().getTime();
20510
+ for (const entry of this.entries.values()) {
20511
+ if (entry.status !== "pending") continue;
20512
+ if (Date.parse(entry.expires_at) > nowMs) continue;
20513
+ entry.status = "expired";
20514
+ entry.resolved_at = this.now().toISOString();
20515
+ entry.resolved_by = "system_ttl";
20516
+ await this.persist(entry);
20517
+ this.auditLog.append(
20518
+ "l2",
20519
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20520
+ this.identityId,
20521
+ {
20522
+ aggregator_id: entry.aggregator_id,
20523
+ source_harness: entry.source_harness,
20524
+ source_agent_id: entry.source_agent_id,
20525
+ audit_log_entry_id: entry.audit_log_entry_id,
20526
+ policy_rule_id: entry.policy_rule_id,
20527
+ decision: "expired",
20528
+ decided_by: "system_ttl",
20529
+ decided_at: entry.resolved_at
20530
+ }
20531
+ );
20532
+ this.emit({ type: "resolved", entry: { ...entry } });
20533
+ }
20534
+ }
20535
+ async persist(entry) {
20536
+ const serialized = stringToBytes(JSON.stringify(entry));
20537
+ const encrypted = encrypt(serialized, this.encryptionKey);
20538
+ await this.storage.write(
20539
+ APPROVAL_AGGREGATOR_NAMESPACE,
20540
+ entry.aggregator_id,
20541
+ stringToBytes(JSON.stringify(encrypted))
20542
+ );
20543
+ }
20544
+ async hydrate() {
20545
+ if (this.hydrated) return;
20546
+ this.hydrated = true;
20547
+ try {
20548
+ const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
20549
+ for (const meta of metas) {
20550
+ const raw = await this.storage.read(
20551
+ APPROVAL_AGGREGATOR_NAMESPACE,
20552
+ meta.key
20553
+ );
20554
+ if (!raw) continue;
20555
+ try {
20556
+ const encrypted = JSON.parse(bytesToString(raw));
20557
+ const decrypted = decrypt(encrypted, this.encryptionKey);
20558
+ const entry = JSON.parse(bytesToString(decrypted));
20559
+ this.entries.set(entry.aggregator_id, entry);
20560
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
20561
+ this.dedupIndex.set(dedupKey, entry.aggregator_id);
20562
+ } catch {
20563
+ }
20564
+ }
20565
+ } catch {
20566
+ this.hydrated = false;
20567
+ }
20568
+ }
20569
+ };
20570
+ }
20571
+ });
19729
20572
 
19730
20573
  // src/principal-policy/tools.ts
19731
20574
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
@@ -20284,7 +21127,11 @@ var init_tools5 = __esm({
20284
21127
 
20285
21128
  // src/handshake/protocol.ts
20286
21129
  function generateNonce() {
20287
- return toBase64url(randomBytes(32));
21130
+ const nonce = randomBytes(32);
21131
+ if (!nonce || nonce.length !== 32) {
21132
+ throw new Error("Nonce generation failed: randomBytes returned unexpected length");
21133
+ }
21134
+ return toBase64url(nonce);
20288
21135
  }
20289
21136
  function initiateHandshake(ourSHR) {
20290
21137
  const nonce = generateNonce();
@@ -20395,6 +21242,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
20395
21242
  return { completion, result };
20396
21243
  }
20397
21244
  function verifyCompletion(completion, session) {
21245
+ if (completion.protocol_version !== "1.0") {
21246
+ return {
21247
+ counterparty_id: "unknown",
21248
+ counterparty_shr: session.our_shr,
21249
+ verified: false,
21250
+ sovereignty_level: "unverified",
21251
+ trust_tier: "unverified",
21252
+ completed_at: completion.completed_at,
21253
+ expires_at: (/* @__PURE__ */ new Date()).toISOString(),
21254
+ errors: [`Unsupported protocol version: ${completion.protocol_version}`]
21255
+ };
21256
+ }
20398
21257
  const errors = [];
20399
21258
  if (!session.their_shr) {
20400
21259
  return {
@@ -22627,6 +23486,12 @@ function typed(markerPath, lineNumber, field, expected) {
22627
23486
  }
22628
23487
  async function consumeResetHistoryMarker(options) {
22629
23488
  const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
23489
+ const consumedPath = markerPath + ".consumed";
23490
+ if (await fileExists3(consumedPath)) {
23491
+ await promises.rm(markerPath, { force: true });
23492
+ await promises.rm(consumedPath, { force: true });
23493
+ return { emitted: 0, markerPath };
23494
+ }
22630
23495
  if (!await fileExists3(markerPath)) {
22631
23496
  return { emitted: 0, markerPath };
22632
23497
  }
@@ -22651,7 +23516,9 @@ async function consumeResetHistoryMarker(options) {
22651
23516
  });
22652
23517
  }
22653
23518
  await options.auditLog.flush();
23519
+ await promises.writeFile(consumedPath, "", "utf-8");
22654
23520
  await promises.rm(markerPath, { force: true });
23521
+ await promises.rm(consumedPath, { force: true });
22655
23522
  return { emitted: markers.length, markerHash, markerPath };
22656
23523
  }
22657
23524
  async function fileExists3(path) {
@@ -22681,6 +23548,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
22681
23548
  this.cause = cause;
22682
23549
  this.name = "ResetHistoryMalformedError";
22683
23550
  }
23551
+ markerPath;
23552
+ lineNumber;
23553
+ cause;
22684
23554
  };
22685
23555
  }
22686
23556
  });
@@ -24839,7 +25709,7 @@ async function runOpenAIPrivacyFilter(text, config) {
24839
25709
  return parsed;
24840
25710
  }
24841
25711
  function runCommand(command, input, timeoutMs) {
24842
- return new Promise((resolve6, reject) => {
25712
+ return new Promise((resolve8, reject) => {
24843
25713
  const child = child_process.spawn(command, [], {
24844
25714
  stdio: ["pipe", "pipe", "pipe"],
24845
25715
  shell: false
@@ -24870,7 +25740,7 @@ function runCommand(command, input, timeoutMs) {
24870
25740
  ));
24871
25741
  return;
24872
25742
  }
24873
- resolve6(stdout);
25743
+ resolve8(stdout);
24874
25744
  });
24875
25745
  child.stdin.end(input);
24876
25746
  });
@@ -26746,13 +27616,13 @@ var init_proxy_router = __esm({
26746
27616
  * Call an upstream tool with a timeout.
26747
27617
  */
26748
27618
  async callWithTimeout(serverName, toolName, args, timeoutMs) {
26749
- return new Promise((resolve6, reject) => {
27619
+ return new Promise((resolve8, reject) => {
26750
27620
  const timer = setTimeout(() => {
26751
27621
  reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
26752
27622
  }, timeoutMs);
26753
27623
  this.clientManager.callTool(serverName, toolName, args).then((result) => {
26754
27624
  clearTimeout(timer);
26755
- resolve6(result);
27625
+ resolve8(result);
26756
27626
  }).catch((err) => {
26757
27627
  clearTimeout(timer);
26758
27628
  reject(err);
@@ -31931,6 +32801,36 @@ var init_hub_service = __esm({
31931
32801
  const chat = this.requireOperatorChat();
31932
32802
  return chat.getConciergeHistory();
31933
32803
  }
32804
+ // ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
32805
+ /**
32806
+ * Whether the operator-chat service has the WP-V1.3-9 memory store
32807
+ * wired. Routes use this to 503 cleanly when the foundation memory
32808
+ * surface is unavailable on a given fortress.
32809
+ */
32810
+ hasConciergeMemory() {
32811
+ return Boolean(this.deps.operatorChat?.hasConciergeMemory());
32812
+ }
32813
+ async listConciergeMemoryThreads(opts) {
32814
+ const chat = this.requireOperatorChat();
32815
+ if (!chat.hasConciergeMemory()) {
32816
+ throw new HubCapabilityError("concierge_memory_not_wired");
32817
+ }
32818
+ return chat.listConciergeMemoryThreads(opts);
32819
+ }
32820
+ async readConciergeMemoryThread(threadId, opts) {
32821
+ const chat = this.requireOperatorChat();
32822
+ if (!chat.hasConciergeMemory()) {
32823
+ throw new HubCapabilityError("concierge_memory_not_wired");
32824
+ }
32825
+ return chat.readConciergeMemoryThread(threadId, opts);
32826
+ }
32827
+ async deleteConciergeMemoryThread(threadId) {
32828
+ const chat = this.requireOperatorChat();
32829
+ if (!chat.hasConciergeMemory()) {
32830
+ throw new HubCapabilityError("concierge_memory_not_wired");
32831
+ }
32832
+ return chat.deleteConciergeMemoryThread(threadId);
32833
+ }
31934
32834
  /**
31935
32835
  * Open the click-to-inspect/approve panel for a wrapped agent. The
31936
32836
  * panel surfaces recent activity routed through this agent, pending
@@ -32069,7 +32969,20 @@ var init_operator_chat_audit_events = __esm({
32069
32969
  * affordance now opens an inspect/approve panel (recent activity +
32070
32970
  * pending approvals + policy summary) instead of a chat session.
32071
32971
  */
32072
- AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
32972
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
32973
+ /**
32974
+ * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
32975
+ * when the operator hits the list-threads or read-thread route. Body
32976
+ * carries the thread_id (or `*` for the list endpoint) and a count;
32977
+ * raw turn content never crosses the audit surface.
32978
+ */
32979
+ CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
32980
+ /**
32981
+ * Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
32982
+ * successful thread removal. Body carries thread_id + turn_count of
32983
+ * the deleted bundle.
32984
+ */
32985
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
32073
32986
  };
32074
32987
  }
32075
32988
  });
@@ -32088,7 +33001,7 @@ function makeEventId(prefix) {
32088
33001
  function hashOf(input) {
32089
33002
  return hashToString(sha256.sha256(stringToBytes(input)));
32090
33003
  }
32091
- var DEFAULT_CONCIERGE_MAX_TOKENS, OperatorChatService;
33004
+ var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
32092
33005
  var init_operator_chat_service = __esm({
32093
33006
  "src/chat/operator-chat-service.ts"() {
32094
33007
  init_hashing();
@@ -32096,6 +33009,33 @@ var init_operator_chat_service = __esm({
32096
33009
  init_operator_chat_audit_events();
32097
33010
  init_operator_chat_types();
32098
33011
  DEFAULT_CONCIERGE_MAX_TOKENS = 512;
33012
+ SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
33013
+ 1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
33014
+ 2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
33015
+ 3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
33016
+ 4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
33017
+
33018
+ Five channel templates (canonical names):
33019
+ - request-approve-act: agent proposes an action, operator approves or denies before execution.
33020
+ - read-then-report: agent reads outputs from a data source and reports summaries to the operator.
33021
+ - scheduled-digest: agent runs on a schedule and delivers a periodic digest.
33022
+ - plan-draft-only: agent drafts plans; operator reviews before any execution step.
33023
+ - fortress-relay: agent relays messages between fortresses under operator-scoped policy.
33024
+
33025
+ Four canonical policy slots:
33026
+ - memory: governs what the agent may persist and retrieve from encrypted state.
33027
+ - credentials: governs access to secrets, API keys, and tokens held in the broker.
33028
+ - plans: governs the agent's ability to create, modify, or execute plans.
33029
+ - outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
33030
+
33031
+ Key concepts:
33032
+ - Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
33033
+ - Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
33034
+ - Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
33035
+ - Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
33036
+ - Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
33037
+
33038
+ Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
32099
33039
  OperatorChatService = class {
32100
33040
  store;
32101
33041
  auditLog;
@@ -32104,6 +33044,14 @@ var init_operator_chat_service = __esm({
32104
33044
  contextProviders;
32105
33045
  piiFilter;
32106
33046
  conciergeMaxTokens;
33047
+ memory;
33048
+ /**
33049
+ * In-memory thread_id assigned to the active concierge session.
33050
+ * The first sendConcierge call after construction allocates a fresh
33051
+ * UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
33052
+ * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
33053
+ */
33054
+ activeMemoryThreadId;
32107
33055
  constructor(deps) {
32108
33056
  this.store = deps.store;
32109
33057
  this.auditLog = deps.auditLog;
@@ -32114,6 +33062,7 @@ var init_operator_chat_service = __esm({
32114
33062
  }
32115
33063
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
32116
33064
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
33065
+ if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
32117
33066
  }
32118
33067
  // ── Concierge ─────────────────────────────────────────────────────────
32119
33068
  /**
@@ -32144,6 +33093,11 @@ var init_operator_chat_service = __esm({
32144
33093
  CONCIERGE_THREAD_KEY,
32145
33094
  operatorMessage
32146
33095
  );
33096
+ if (this.memory) {
33097
+ const threadId = this.ensureActiveMemoryThread();
33098
+ await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
33099
+ });
33100
+ }
32147
33101
  const start = Date.now();
32148
33102
  let conciergeBody;
32149
33103
  let servedBy = "disabled";
@@ -32199,6 +33153,11 @@ var init_operator_chat_service = __esm({
32199
33153
  CONCIERGE_THREAD_KEY,
32200
33154
  responseMessage
32201
33155
  );
33156
+ if (this.memory) {
33157
+ const threadId = this.ensureActiveMemoryThread();
33158
+ await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
33159
+ });
33160
+ }
32202
33161
  const payload = {
32203
33162
  version: "1.2",
32204
33163
  event_id: makeEventId("conc"),
@@ -32231,6 +33190,105 @@ var init_operator_chat_service = __esm({
32231
33190
  );
32232
33191
  return thread ? thread.messages : [];
32233
33192
  }
33193
+ // ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
33194
+ /**
33195
+ * Whether the foundation memory store is wired. Routes use this to
33196
+ * 503 cleanly when called against an unwired service.
33197
+ */
33198
+ hasConciergeMemory() {
33199
+ return this.memory !== void 0;
33200
+ }
33201
+ /**
33202
+ * List concierge memory threads, newest-first. Emits the
33203
+ * `operator_concierge_history_read` audit event with `thread_id="*"`.
33204
+ */
33205
+ async listConciergeMemoryThreads(opts) {
33206
+ if (!this.memory) {
33207
+ throw new Error("concierge memory store not configured");
33208
+ }
33209
+ const summaries = await this.memory.listThreads(opts);
33210
+ const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
33211
+ const payload = {
33212
+ version: "1.2",
33213
+ event_id: makeEventId("conc-hist"),
33214
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33215
+ identity_id: this.identityId,
33216
+ kind: "operator_concierge_history_read",
33217
+ surface: "concierge",
33218
+ thread_id: "*",
33219
+ turn_count: totalTurns
33220
+ };
33221
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
33222
+ return summaries;
33223
+ }
33224
+ /**
33225
+ * Read a concierge memory thread, oldest turn first. Emits the
33226
+ * `operator_concierge_history_read` audit event with the named
33227
+ * thread_id and the count of turns surfaced.
33228
+ */
33229
+ async readConciergeMemoryThread(threadId, opts) {
33230
+ if (!this.memory) {
33231
+ throw new Error("concierge memory store not configured");
33232
+ }
33233
+ const turns = await this.memory.readThread(threadId, opts);
33234
+ const payload = {
33235
+ version: "1.2",
33236
+ event_id: makeEventId("conc-hist"),
33237
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33238
+ identity_id: this.identityId,
33239
+ kind: "operator_concierge_history_read",
33240
+ surface: "concierge",
33241
+ thread_id: threadId,
33242
+ turn_count: turns.length
33243
+ };
33244
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
33245
+ return turns;
33246
+ }
33247
+ /**
33248
+ * Delete a concierge memory thread. Emits
33249
+ * `operator_concierge_thread_deleted` only when a bundle was actually
33250
+ * removed; absent threads return false without an audit event.
33251
+ */
33252
+ async deleteConciergeMemoryThread(threadId) {
33253
+ if (!this.memory) {
33254
+ throw new Error("concierge memory store not configured");
33255
+ }
33256
+ const turnsBefore = await this.memory.readThread(threadId);
33257
+ if (turnsBefore.length === 0) {
33258
+ return await this.memory.deleteThread(threadId);
33259
+ }
33260
+ const removed = await this.memory.deleteThread(threadId);
33261
+ if (!removed) return false;
33262
+ if (this.activeMemoryThreadId === threadId) {
33263
+ this.activeMemoryThreadId = void 0;
33264
+ }
33265
+ const payload = {
33266
+ version: "1.2",
33267
+ event_id: makeEventId("conc-del"),
33268
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33269
+ identity_id: this.identityId,
33270
+ kind: "operator_concierge_thread_deleted",
33271
+ surface: "concierge",
33272
+ thread_id: threadId,
33273
+ turn_count: turnsBefore.length
33274
+ };
33275
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
33276
+ return true;
33277
+ }
33278
+ /**
33279
+ * Reset the active session memory thread. Subsequent sendConcierge
33280
+ * calls allocate a fresh thread_id. Surfaced for tests + future "new
33281
+ * conversation" affordance; not currently called by the dashboard.
33282
+ */
33283
+ resetConciergeMemoryThread() {
33284
+ this.activeMemoryThreadId = void 0;
33285
+ }
33286
+ ensureActiveMemoryThread() {
33287
+ if (!this.activeMemoryThreadId) {
33288
+ this.activeMemoryThreadId = crypto.randomUUID();
33289
+ }
33290
+ return this.activeMemoryThreadId;
33291
+ }
32234
33292
  /**
32235
33293
  * Stitch fortress state into a single context blob the substrate
32236
33294
  * folds into its summarization prompt.
@@ -32240,6 +33298,9 @@ var init_operator_chat_service = __esm({
32240
33298
  * than nested structures. Format:
32241
33299
  *
32242
33300
  * ```
33301
+ * ## Sanctuary reference
33302
+ * <static domain reference block>
33303
+ *
32243
33304
  * ## Recent activity
32244
33305
  * <recentActivity output>
32245
33306
  *
@@ -32251,15 +33312,28 @@ var init_operator_chat_service = __esm({
32251
33312
  * ```
32252
33313
  */
32253
33314
  async assembleConciergeContext() {
33315
+ const ref = `## Sanctuary reference
33316
+ ${SANCTUARY_DOMAIN_REFERENCE}`;
32254
33317
  if (!this.contextProviders) {
32255
- return "## Recent activity\n(no providers wired)\n\n## Wrapped agents\n(no providers wired)\n\n## Open inbox\n(no providers wired)";
33318
+ return `${ref}
33319
+
33320
+ ## Recent activity
33321
+ (no providers wired)
33322
+
33323
+ ## Wrapped agents
33324
+ (no providers wired)
33325
+
33326
+ ## Open inbox
33327
+ (no providers wired)`;
32256
33328
  }
32257
33329
  const [activity, agents, inbox] = await Promise.all([
32258
33330
  this.contextProviders.recentActivity(),
32259
33331
  this.contextProviders.agentInventory(),
32260
33332
  this.contextProviders.openInbox()
32261
33333
  ]);
32262
- return `## Recent activity
33334
+ return `${ref}
33335
+
33336
+ ## Recent activity
32263
33337
  ${activity}
32264
33338
 
32265
33339
  ## Wrapped agents
@@ -32381,11 +33455,250 @@ var init_operator_chat_store = __esm({
32381
33455
  }
32382
33456
  });
32383
33457
 
33458
+ // src/chat/concierge-memory-store.ts
33459
+ function bundleKey(threadId) {
33460
+ return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
33461
+ }
33462
+ function stripKeyPrefix(key) {
33463
+ if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
33464
+ return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
33465
+ }
33466
+ function lastTurnId(bundle) {
33467
+ let max = 0;
33468
+ for (const t of bundle.turns) {
33469
+ if (t.turn_id > max) max = t.turn_id;
33470
+ }
33471
+ return max;
33472
+ }
33473
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
33474
+ var init_concierge_memory_store = __esm({
33475
+ "src/chat/concierge-memory-store.ts"() {
33476
+ init_encryption();
33477
+ init_key_derivation();
33478
+ init_encoding();
33479
+ CONCIERGE_MEMORY_NAMESPACE = "_chat";
33480
+ CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
33481
+ HKDF_INFO2 = "concierge-memory-store-v1";
33482
+ DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
33483
+ MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
33484
+ ConciergeMemoryStore = class {
33485
+ storage;
33486
+ encryptionKey;
33487
+ fortressId;
33488
+ retentionDays;
33489
+ locks;
33490
+ constructor(opts) {
33491
+ this.storage = opts.storage;
33492
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
33493
+ this.fortressId = opts.fortressId;
33494
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
33495
+ this.locks = /* @__PURE__ */ new Map();
33496
+ }
33497
+ /**
33498
+ * Append a turn to the named thread, creating the bundle if no record
33499
+ * exists. Returns the persisted turn (with assigned turn_id +
33500
+ * retention_until). Per-thread serialisation guarantees turn_id
33501
+ * monotonicity even under concurrent callers.
33502
+ */
33503
+ async appendTurn(threadId, role, content) {
33504
+ return this.withLock(threadId, async () => {
33505
+ const bundle = await this.loadBundle(threadId) ?? null;
33506
+ const now = /* @__PURE__ */ new Date();
33507
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
33508
+ const retentionUntil = new Date(now.getTime() + retentionMs);
33509
+ const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
33510
+ const turn = {
33511
+ thread_id: threadId,
33512
+ fortress_id: this.fortressId,
33513
+ turn_id: nextTurnId,
33514
+ role,
33515
+ content,
33516
+ created_at: now.toISOString(),
33517
+ retention_until: retentionUntil.toISOString()
33518
+ };
33519
+ const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
33520
+ version: 1,
33521
+ thread_id: threadId,
33522
+ fortress_id: this.fortressId,
33523
+ created_at: now.toISOString(),
33524
+ turns: [turn]
33525
+ };
33526
+ await this.saveBundle(next);
33527
+ return turn;
33528
+ });
33529
+ }
33530
+ /**
33531
+ * Read turns from a thread, oldest-first. Returns an empty array if
33532
+ * the thread does not exist or its bundle is corrupt. Does not emit
33533
+ * audit events; the caller (HTTP route handler) owns audit semantics.
33534
+ */
33535
+ async readThread(threadId, opts) {
33536
+ const bundle = await this.loadBundle(threadId);
33537
+ if (!bundle) return [];
33538
+ let turns = bundle.turns;
33539
+ if (opts?.sinceTurnId !== void 0) {
33540
+ const cutoff = opts.sinceTurnId;
33541
+ turns = turns.filter((t) => t.turn_id > cutoff);
33542
+ }
33543
+ if (opts?.limit !== void 0) {
33544
+ turns = turns.slice(0, opts.limit);
33545
+ }
33546
+ return turns;
33547
+ }
33548
+ /**
33549
+ * Enumerate concierge threads in this fortress with summary metadata.
33550
+ * Sorted newest-first by last_turn_at.
33551
+ */
33552
+ async listThreads(opts) {
33553
+ const entries = await this.storage.list(
33554
+ CONCIERGE_MEMORY_NAMESPACE,
33555
+ CONCIERGE_MEMORY_KEY_PREFIX
33556
+ );
33557
+ const summaries = [];
33558
+ for (const meta of entries) {
33559
+ const threadId = stripKeyPrefix(meta.key);
33560
+ if (threadId === null) continue;
33561
+ const bundle = await this.loadBundle(threadId);
33562
+ if (!bundle || bundle.turns.length === 0) continue;
33563
+ const last = bundle.turns[bundle.turns.length - 1];
33564
+ summaries.push({
33565
+ thread_id: bundle.thread_id,
33566
+ created_at: bundle.created_at,
33567
+ last_turn_at: last ? last.created_at : bundle.created_at,
33568
+ turn_count: bundle.turns.length
33569
+ });
33570
+ }
33571
+ summaries.sort(
33572
+ (a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
33573
+ );
33574
+ if (opts?.limit !== void 0) {
33575
+ return summaries.slice(0, opts.limit);
33576
+ }
33577
+ return summaries;
33578
+ }
33579
+ /**
33580
+ * Delete a thread's bundle. Returns true if the bundle existed and
33581
+ * was removed; false if no bundle was present. Audit emission is the
33582
+ * caller's responsibility.
33583
+ */
33584
+ async deleteThread(threadId) {
33585
+ const key = bundleKey(threadId);
33586
+ return this.withLock(threadId, async () => {
33587
+ const existed = await this.storage.exists(
33588
+ CONCIERGE_MEMORY_NAMESPACE,
33589
+ key
33590
+ );
33591
+ if (!existed) return false;
33592
+ try {
33593
+ await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
33594
+ } catch {
33595
+ return false;
33596
+ }
33597
+ return true;
33598
+ });
33599
+ }
33600
+ /**
33601
+ * Drop expired turns across all threads. Threads emptied by pruning
33602
+ * are removed entirely. Returns the count of turns pruned.
33603
+ */
33604
+ async pruneExpired(now) {
33605
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
33606
+ const entries = await this.storage.list(
33607
+ CONCIERGE_MEMORY_NAMESPACE,
33608
+ CONCIERGE_MEMORY_KEY_PREFIX
33609
+ );
33610
+ let pruned = 0;
33611
+ for (const meta of entries) {
33612
+ const threadId = stripKeyPrefix(meta.key);
33613
+ if (threadId === null) continue;
33614
+ pruned += await this.withLock(threadId, async () => {
33615
+ const bundle = await this.loadBundle(threadId);
33616
+ if (!bundle) return 0;
33617
+ const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
33618
+ const dropped = bundle.turns.length - kept.length;
33619
+ if (dropped === 0) return 0;
33620
+ if (kept.length === 0) {
33621
+ await this.storage.delete(
33622
+ CONCIERGE_MEMORY_NAMESPACE,
33623
+ bundleKey(threadId)
33624
+ );
33625
+ } else {
33626
+ await this.saveBundle({ ...bundle, turns: kept });
33627
+ }
33628
+ return dropped;
33629
+ });
33630
+ }
33631
+ return { pruned };
33632
+ }
33633
+ // ── internals ────────────────────────────────────────────────────────
33634
+ async loadBundle(threadId) {
33635
+ const key = bundleKey(threadId);
33636
+ let raw;
33637
+ try {
33638
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
33639
+ } catch {
33640
+ return null;
33641
+ }
33642
+ if (!raw) return null;
33643
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
33644
+ try {
33645
+ const envelope = JSON.parse(bytesToString(raw));
33646
+ const aad = stringToBytes(threadId);
33647
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
33648
+ const parsed = JSON.parse(
33649
+ bytesToString(plaintext)
33650
+ );
33651
+ if (parsed.version !== 1) return null;
33652
+ if (parsed.thread_id !== threadId) return null;
33653
+ return parsed;
33654
+ } catch {
33655
+ return null;
33656
+ }
33657
+ }
33658
+ async saveBundle(bundle) {
33659
+ const key = bundleKey(bundle.thread_id);
33660
+ const aad = stringToBytes(bundle.thread_id);
33661
+ const plaintext = stringToBytes(JSON.stringify(bundle));
33662
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
33663
+ await this.storage.write(
33664
+ CONCIERGE_MEMORY_NAMESPACE,
33665
+ key,
33666
+ stringToBytes(JSON.stringify(envelope))
33667
+ );
33668
+ }
33669
+ /**
33670
+ * Run `task` while holding the per-thread async lock. Lock is released
33671
+ * once the task settles (success or failure). Generic helper so
33672
+ * appendTurn / deleteThread / pruneExpired share serialisation.
33673
+ */
33674
+ async withLock(threadId, task) {
33675
+ const previous = this.locks.get(threadId) ?? Promise.resolve();
33676
+ let release;
33677
+ const next = new Promise((resolve8) => {
33678
+ release = resolve8;
33679
+ });
33680
+ const chained = previous.then(() => next);
33681
+ this.locks.set(threadId, chained);
33682
+ try {
33683
+ await previous;
33684
+ return await task();
33685
+ } finally {
33686
+ release();
33687
+ if (this.locks.get(threadId) === chained) {
33688
+ this.locks.delete(threadId);
33689
+ }
33690
+ }
33691
+ }
33692
+ };
33693
+ }
33694
+ });
33695
+
32384
33696
  // src/chat/operator-chat-index.ts
32385
33697
  var init_operator_chat_index = __esm({
32386
33698
  "src/chat/operator-chat-index.ts"() {
32387
33699
  init_operator_chat_service();
32388
33700
  init_operator_chat_store();
33701
+ init_concierge_memory_store();
32389
33702
  init_operator_chat_audit_events();
32390
33703
  init_operator_chat_types();
32391
33704
  }
@@ -32399,6 +33712,14 @@ function buildV11Bindings(inputs) {
32399
33712
  let operatorChatService;
32400
33713
  if (inputs.storage && inputs.masterKey) {
32401
33714
  const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
33715
+ const conciergeMemory = new ConciergeMemoryStore({
33716
+ storage: inputs.storage,
33717
+ masterKey: inputs.masterKey,
33718
+ fortressId: inputs.fortressId,
33719
+ ...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
33720
+ });
33721
+ void conciergeMemory.pruneExpired().catch(() => {
33722
+ });
32402
33723
  operatorChatService = new OperatorChatService({
32403
33724
  store: chatStore,
32404
33725
  auditLog: inputs.auditLog,
@@ -32409,7 +33730,8 @@ function buildV11Bindings(inputs) {
32409
33730
  identityId: inputs.identityId,
32410
33731
  registry
32411
33732
  }),
32412
- conciergePiiFilter: buildConciergePiiFilter()
33733
+ conciergePiiFilter: buildConciergePiiFilter(),
33734
+ conciergeMemory
32413
33735
  });
32414
33736
  }
32415
33737
  const hubService = new HubService({
@@ -32644,7 +33966,7 @@ var init_defaults = __esm({
32644
33966
  });
32645
33967
 
32646
33968
  // src/intelligence/policy-store.ts
32647
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO2, IntelligenceConfigStore;
33969
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
32648
33970
  var init_policy_store = __esm({
32649
33971
  "src/intelligence/policy-store.ts"() {
32650
33972
  init_encryption();
@@ -32653,13 +33975,13 @@ var init_policy_store = __esm({
32653
33975
  init_defaults();
32654
33976
  INTELLIGENCE_NAMESPACE = "_intelligence";
32655
33977
  SUBSTRATE_CONFIG_KEY = "substrate-config";
32656
- HKDF_INFO2 = "intelligence-substrate-config";
33978
+ HKDF_INFO3 = "intelligence-substrate-config";
32657
33979
  IntelligenceConfigStore = class {
32658
33980
  storage;
32659
33981
  encryptionKey;
32660
33982
  constructor(storage, masterKey) {
32661
33983
  this.storage = storage;
32662
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
33984
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
32663
33985
  }
32664
33986
  /**
32665
33987
  * Load the operator's substrate config from disk. Returns the config
@@ -34681,7 +36003,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
34681
36003
  );
34682
36004
  }
34683
36005
  }
34684
- const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
36006
+ const reputationBundleFailed = reputation?.bundle_signature_valid === false;
36007
+ const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
36008
+ const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
34685
36009
  const identityFailed = identity ? !identity.signature_valid : false;
34686
36010
  const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
34687
36011
  const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
@@ -34690,6 +36014,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
34690
36014
  `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
34691
36015
  );
34692
36016
  }
36017
+ let detailedFailureClass;
36018
+ if (identityFailed) {
36019
+ detailedFailureClass = "identity_signature_invalid";
36020
+ } else if (reputationBundleFailed) {
36021
+ detailedFailureClass = "reputation_bundle_signature_invalid";
36022
+ } else if (reputationAttestationFailed) {
36023
+ detailedFailureClass = "reputation_attestation_signature_invalid";
36024
+ } else if (unverifiableFailed) {
36025
+ detailedFailureClass = "reputation_unverifiable_attestations";
36026
+ }
34693
36027
  return {
34694
36028
  version: "1.1",
34695
36029
  passed: !reputationFailed && !identityFailed && !unverifiableFailed,
@@ -34709,7 +36043,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
34709
36043
  identity,
34710
36044
  audit,
34711
36045
  reputation,
34712
- failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
36046
+ failure_class: detailedFailureClass
34713
36047
  };
34714
36048
  }
34715
36049
  var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
@@ -35291,6 +36625,9 @@ async function importExitBundle(opts) {
35291
36625
  reputationArtifact?.json ?? null,
35292
36626
  manifest
35293
36627
  );
36628
+ if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
36629
+ conflicts.public_identity_exists = true;
36630
+ }
35294
36631
  if (!opts.activate) {
35295
36632
  return {
35296
36633
  verified: true,
@@ -35317,7 +36654,7 @@ async function importExitBundle(opts) {
35317
36654
  if (conflicts.public_identity_exists && !opts.forceRebind) {
35318
36655
  throw new ExitBundleImportError(
35319
36656
  "IDENTITY_OVERWRITE_REFUSED",
35320
- "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
36657
+ "Importing this exit bundle would overwrite an existing fortress public identity (either the same identity already imported, or a different identity is currently active). Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
35321
36658
  );
35322
36659
  }
35323
36660
  if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
@@ -35674,7 +37011,19 @@ async function runExitCommand(args) {
35674
37011
  }
35675
37012
  const config = await loadConfig();
35676
37013
  const ctx = await openExitContext(argv, env);
35677
- const policy = await loadPrincipalPolicy(ctx.storagePath);
37014
+ let policy;
37015
+ try {
37016
+ policy = await loadPrincipalPolicy(ctx.storagePath);
37017
+ } catch (policyErr) {
37018
+ if (policyErr instanceof MalformedPrincipalPolicyError) {
37019
+ write(err, `
37020
+ Sanctuary cannot proceed.
37021
+ ${policyErr.message}
37022
+ `);
37023
+ return 1;
37024
+ }
37025
+ throw policyErr;
37026
+ }
35678
37027
  const result = await exportExitBundle({
35679
37028
  bundleDir: outDir,
35680
37029
  storage: ctx.storage,
@@ -35707,6 +37056,26 @@ async function runExitCommand(args) {
35707
37056
  write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
35708
37057
  return 2;
35709
37058
  }
37059
+ const bundleRoot = path.resolve(dir);
37060
+ try {
37061
+ await promises.access(bundleRoot);
37062
+ } catch {
37063
+ write(err, `Error: bundle directory not found: ${bundleRoot}
37064
+ `);
37065
+ return 1;
37066
+ }
37067
+ const manifestPath = path.join(bundleRoot, "manifest.json");
37068
+ try {
37069
+ const raw = await promises.readFile(manifestPath, "utf8");
37070
+ JSON.parse(raw);
37071
+ } catch {
37072
+ write(
37073
+ err,
37074
+ `Error: bundle manifest missing or malformed at ${manifestPath}
37075
+ `
37076
+ );
37077
+ return 1;
37078
+ }
35710
37079
  const activate = hasFlag(argv, "--activate");
35711
37080
  const forceRebind = hasFlag(argv, "--force-rebind");
35712
37081
  const acceptUnverifiableAttestations = hasFlag(
@@ -35880,11 +37249,11 @@ async function startDashboardServer(options) {
35880
37249
  }
35881
37250
  }
35882
37251
  });
35883
- await new Promise((resolve6, reject) => {
37252
+ await new Promise((resolve8, reject) => {
35884
37253
  server.once("error", reject);
35885
37254
  server.listen(port, host, () => {
35886
37255
  server.off("error", reject);
35887
- resolve6();
37256
+ resolve8();
35888
37257
  });
35889
37258
  });
35890
37259
  const actualPort = (() => {
@@ -35897,8 +37266,8 @@ async function startDashboardServer(options) {
35897
37266
  url,
35898
37267
  port: actualPort,
35899
37268
  host,
35900
- stop: () => new Promise((resolve6, reject) => {
35901
- server.close((err) => err ? reject(err) : resolve6());
37269
+ stop: () => new Promise((resolve8, reject) => {
37270
+ server.close((err) => err ? reject(err) : resolve8());
35902
37271
  }),
35903
37272
  publish,
35904
37273
  publishActivity: (entry) => publish({ type: "activity", data: entry }),
@@ -36354,7 +37723,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36354
37723
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
36355
37724
  await profileStore.load();
36356
37725
  const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
36357
- const policy = await loadPrincipalPolicy(config.storage_path);
37726
+ let policy;
37727
+ try {
37728
+ policy = await loadPrincipalPolicy(config.storage_path);
37729
+ } catch (err) {
37730
+ if (err instanceof MalformedPrincipalPolicyError) {
37731
+ console.error(`
37732
+ Sanctuary cannot start.
37733
+ ${err.message}
37734
+ `);
37735
+ process.exit(1);
37736
+ }
37737
+ throw err;
37738
+ }
36358
37739
  const baseline = new BaselineTracker(storage, masterKey);
36359
37740
  await baseline.load();
36360
37741
  let approvalChannel;
@@ -36452,6 +37833,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36452
37833
  });
36453
37834
  } : void 0;
36454
37835
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37836
+ const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37837
+ const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
37838
+ const approvalAggregator = new ApprovalAggregator({
37839
+ storage,
37840
+ masterKey,
37841
+ auditLog,
37842
+ identityId: aggregatorIdentityId,
37843
+ fortressId: fortressIdForAggregator
37844
+ });
37845
+ gate.setApprovalEventCallback((event) => {
37846
+ void approvalAggregator.ingest(event);
37847
+ });
37848
+ if (dashboard) {
37849
+ dashboard.setApprovalAggregator(approvalAggregator);
37850
+ }
36455
37851
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
36456
37852
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
36457
37853
  config,
@@ -36571,7 +37967,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36571
37967
  clientManager.configure(enabledServers).catch((err) => {
36572
37968
  console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
36573
37969
  });
36574
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
37970
+ await new Promise((resolve8) => setTimeout(resolve8, 2e3));
36575
37971
  const proxiedTools = proxyRouter.getProxiedTools();
36576
37972
  if (proxiedTools.length > 0) {
36577
37973
  allTools.push(...proxiedTools);
@@ -36642,6 +38038,7 @@ var init_src = __esm({
36642
38038
  init_dashboard();
36643
38039
  init_webhook();
36644
38040
  init_gate();
38041
+ init_approval_aggregator();
36645
38042
  init_tools4();
36646
38043
  init_router();
36647
38044
  init_router();
@@ -37410,8 +38807,8 @@ async function runWrap(options, deps = {}) {
37410
38807
  passphraseValue = process.env.SANCTUARY_PASSPHRASE;
37411
38808
  } else {
37412
38809
  try {
37413
- const resolve6 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
37414
- const resolved = await resolve6();
38810
+ const resolve8 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
38811
+ const resolved = await resolve8();
37415
38812
  passphraseLocation = resolved.location;
37416
38813
  passphraseSource = resolved.source;
37417
38814
  passphraseValue = resolved.value;
@@ -37766,12 +39163,12 @@ async function defaultOpenBrowser(url) {
37766
39163
  cmd = "xdg-open";
37767
39164
  args = [url];
37768
39165
  }
37769
- await new Promise((resolve6) => {
39166
+ await new Promise((resolve8) => {
37770
39167
  const child = child_process.spawn(cmd, args, { stdio: "ignore", detached: true });
37771
- child.on("error", () => resolve6());
39168
+ child.on("error", () => resolve8());
37772
39169
  child.on("spawn", () => {
37773
39170
  child.unref();
37774
- resolve6();
39171
+ resolve8();
37775
39172
  });
37776
39173
  });
37777
39174
  }
@@ -38740,32 +40137,32 @@ endstream`;
38740
40137
  const offsets = new Array(totalObjects + 1).fill(0);
38741
40138
  const chunks = [];
38742
40139
  let bytePos = 0;
38743
- const write2 = (s) => {
40140
+ const write3 = (s) => {
38744
40141
  const buf = Buffer.from(s, "latin1");
38745
40142
  chunks.push(buf);
38746
40143
  bytePos += buf.length;
38747
40144
  };
38748
- write2("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
40145
+ write3("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
38749
40146
  for (let i = 1; i <= totalObjects; i++) {
38750
40147
  offsets[i] = bytePos;
38751
- write2(`${i} 0 obj
40148
+ write3(`${i} 0 obj
38752
40149
  ${objectBodies[i]}
38753
40150
  endobj
38754
40151
  `);
38755
40152
  }
38756
40153
  const xrefPos = bytePos;
38757
- write2(`xref
40154
+ write3(`xref
38758
40155
  0 ${totalObjects + 1}
38759
40156
  `);
38760
- write2("0000000000 65535 f \n");
40157
+ write3("0000000000 65535 f \n");
38761
40158
  for (let i = 1; i <= totalObjects; i++) {
38762
- write2(`${offsets[i].toString().padStart(10, "0")} 00000 n
40159
+ write3(`${offsets[i].toString().padStart(10, "0")} 00000 n
38763
40160
  `);
38764
40161
  }
38765
- write2(`trailer
40162
+ write3(`trailer
38766
40163
  << /Size ${totalObjects + 1} /Root 1 0 R >>
38767
40164
  `);
38768
- write2(`startxref
40165
+ write3(`startxref
38769
40166
  ${xrefPos}
38770
40167
  %%EOF
38771
40168
  `);
@@ -39100,7 +40497,7 @@ var init_backend_interface = __esm({
39100
40497
  }
39101
40498
  });
39102
40499
  async function runSecurity(args, input) {
39103
- return new Promise((resolve6, reject) => {
40500
+ return new Promise((resolve8, reject) => {
39104
40501
  const child = child_process.spawn(SECURITY_BIN, args, { stdio: ["pipe", "pipe", "pipe"] });
39105
40502
  let stdout = "";
39106
40503
  let stderr = "";
@@ -39122,7 +40519,7 @@ async function runSecurity(args, input) {
39122
40519
  reject(err);
39123
40520
  });
39124
40521
  child.on("close", (code) => {
39125
- resolve6({ stdout, stderr, code: code ?? -1 });
40522
+ resolve8({ stdout, stderr, code: code ?? -1 });
39126
40523
  });
39127
40524
  if (input !== void 0) {
39128
40525
  child.stdin.write(input);
@@ -40198,7 +41595,7 @@ async function readValue(stdin, prompt2) {
40198
41595
  return await readFirstLine(stdin);
40199
41596
  }
40200
41597
  async function readFirstLine(stdin) {
40201
- return new Promise((resolve6, reject) => {
41598
+ return new Promise((resolve8, reject) => {
40202
41599
  const rl = readline.createInterface({ input: stdin });
40203
41600
  let resolved = false;
40204
41601
  const finish = (value) => {
@@ -40209,7 +41606,7 @@ async function readFirstLine(stdin) {
40209
41606
  rl.close();
40210
41607
  } catch {
40211
41608
  }
40212
- resolve6(value);
41609
+ resolve8(value);
40213
41610
  };
40214
41611
  const deadline = setTimeout(() => {
40215
41612
  finish("");
@@ -40228,7 +41625,7 @@ async function promptSilently(stdin, prompt2) {
40228
41625
  process.stderr.write(`${prompt2}: `);
40229
41626
  stdin.setRawMode?.(true);
40230
41627
  stdin.resume();
40231
- return await new Promise((resolve6) => {
41628
+ return await new Promise((resolve8) => {
40232
41629
  let buf = "";
40233
41630
  const onData = (chunk) => {
40234
41631
  const s = chunk.toString("utf8");
@@ -40238,7 +41635,7 @@ async function promptSilently(stdin, prompt2) {
40238
41635
  stdin.pause();
40239
41636
  stdin.off("data", onData);
40240
41637
  process.stderr.write("\n");
40241
- resolve6(buf);
41638
+ resolve8(buf);
40242
41639
  return;
40243
41640
  }
40244
41641
  if (ch === "") {
@@ -40505,13 +41902,179 @@ var init_cli4 = __esm({
40505
41902
  init_discovery();
40506
41903
  }
40507
41904
  });
41905
+
41906
+ // src/cli/identity.ts
41907
+ var identity_exports2 = {};
41908
+ __export(identity_exports2, {
41909
+ runIdentityCommand: () => runIdentityCommand
41910
+ });
41911
+ function write2(stream, text) {
41912
+ stream.write(text);
41913
+ }
41914
+ function flagValue2(argv, name) {
41915
+ const index = argv.indexOf(name);
41916
+ if (index === -1) return void 0;
41917
+ return argv[index + 1];
41918
+ }
41919
+ function hasFlag2(argv, name) {
41920
+ return argv.includes(name);
41921
+ }
41922
+ function printUsage3(out) {
41923
+ write2(
41924
+ out,
41925
+ `Usage: sanctuary identity <command> [options]
41926
+
41927
+ Commands:
41928
+ show Print the active identity (DID, identity_id, public key).
41929
+
41930
+ Options:
41931
+ --fortress <path> Override the storage path.
41932
+ --passphrase <val> Passphrase for master-key derivation.
41933
+ --json Output as JSON.
41934
+ --help, -h Show this help.
41935
+
41936
+ Environment variables:
41937
+ SANCTUARY_PASSPHRASE Key derivation passphrase.
41938
+ SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary).
41939
+ SANCTUARY_FORTRESS_PATH Operator-friendly alias for STORAGE_PATH.
41940
+ SANCTUARY_RECOVERY_KEY Recovery key (alternative to passphrase).
41941
+
41942
+ Identity data is encrypted at rest. A passphrase or recovery key is
41943
+ required to decrypt and display identity information.
41944
+ `
41945
+ );
41946
+ }
41947
+ async function runIdentityCommand(args) {
41948
+ const argv = args.argv;
41949
+ const out = args.out ?? process.stdout;
41950
+ const err = args.err ?? process.stderr;
41951
+ const env = args.env ?? process.env;
41952
+ if (argv.length === 0 || hasFlag2(argv, "--help") || hasFlag2(argv, "-h")) {
41953
+ printUsage3(out);
41954
+ return 0;
41955
+ }
41956
+ const command = argv[0];
41957
+ if (command === "show") {
41958
+ return await cmdShow(argv.slice(1), out, err, env);
41959
+ }
41960
+ write2(err, `Unknown identity command: ${command}
41961
+ `);
41962
+ write2(err, `Run "sanctuary identity --help" for usage.
41963
+ `);
41964
+ return 2;
41965
+ }
41966
+ async function cmdShow(argv, out, err, env) {
41967
+ const json = hasFlag2(argv, "--json");
41968
+ const fortressFlag = flagValue2(argv, "--fortress");
41969
+ if (fortressFlag) {
41970
+ process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
41971
+ }
41972
+ const passphrase = flagValue2(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
41973
+ const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
41974
+ if (!passphrase && !recoveryKey) {
41975
+ write2(
41976
+ err,
41977
+ "Error: sanctuary identity show requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY.\n"
41978
+ );
41979
+ return 1;
41980
+ }
41981
+ try {
41982
+ const config = await loadConfig();
41983
+ await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
41984
+ const stateStoragePath = path.join(config.storage_path, "state");
41985
+ const storage = new FilesystemStorage(stateStoragePath);
41986
+ let masterKey;
41987
+ if (passphrase) {
41988
+ let existingParams;
41989
+ const raw = await storage.read("_meta", "key-params");
41990
+ if (raw)
41991
+ existingParams = JSON.parse(bytesToString(raw));
41992
+ const derived = await deriveMasterKey(passphrase, existingParams);
41993
+ masterKey = derived.key;
41994
+ } else {
41995
+ masterKey = fromBase64url(recoveryKey);
41996
+ if (masterKey.length !== 32) {
41997
+ write2(err, "Error: SANCTUARY_RECOVERY_KEY must decode to 32 bytes.\n");
41998
+ return 1;
41999
+ }
42000
+ }
42001
+ const identityManager = new IdentityManager(storage, masterKey);
42002
+ const loadResult = await identityManager.load();
42003
+ if (loadResult.loaded === 0) {
42004
+ write2(
42005
+ err,
42006
+ loadResult.total > 0 ? "Error: identity files found but none could be decrypted. Wrong passphrase?\n" : "No identities found in this fortress.\n"
42007
+ );
42008
+ return 1;
42009
+ }
42010
+ const primary = identityManager.getDefault();
42011
+ if (!primary) {
42012
+ write2(err, "No primary identity set.\n");
42013
+ return 1;
42014
+ }
42015
+ if (json) {
42016
+ write2(
42017
+ out,
42018
+ JSON.stringify(
42019
+ {
42020
+ identity_id: primary.identity_id,
42021
+ did: primary.did,
42022
+ public_key: primary.public_key,
42023
+ label: primary.label,
42024
+ key_type: primary.key_type,
42025
+ created_at: primary.created_at,
42026
+ storage_path: config.storage_path,
42027
+ total_identities: loadResult.loaded
42028
+ },
42029
+ null,
42030
+ 2
42031
+ ) + "\n"
42032
+ );
42033
+ } else {
42034
+ write2(out, `identity_id: ${primary.identity_id}
42035
+ `);
42036
+ write2(out, `did: ${primary.did}
42037
+ `);
42038
+ write2(out, `public_key: ${primary.public_key}
42039
+ `);
42040
+ write2(out, `label: ${primary.label}
42041
+ `);
42042
+ write2(out, `key_type: ${primary.key_type}
42043
+ `);
42044
+ write2(out, `created_at: ${primary.created_at}
42045
+ `);
42046
+ write2(out, `storage_path: ${config.storage_path}
42047
+ `);
42048
+ write2(out, `total_identities: ${loadResult.loaded}
42049
+ `);
42050
+ }
42051
+ return 0;
42052
+ } catch (error) {
42053
+ write2(
42054
+ err,
42055
+ error instanceof Error ? `Error: ${error.message}
42056
+ ` : `Error: ${String(error)}
42057
+ `
42058
+ );
42059
+ return 1;
42060
+ }
42061
+ }
42062
+ var init_identity2 = __esm({
42063
+ "src/cli/identity.ts"() {
42064
+ init_filesystem();
42065
+ init_tools();
42066
+ init_key_derivation();
42067
+ init_encoding();
42068
+ init_config();
42069
+ }
42070
+ });
40508
42071
  async function probeTenantDashboard(tenant, options = {}) {
40509
42072
  const rt = tenant.runtime;
40510
42073
  if (!rt) {
40511
42074
  return { running: false, status: null, reason: "no runtime.json" };
40512
42075
  }
40513
42076
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
40514
- return await new Promise((resolve6) => {
42077
+ return await new Promise((resolve8) => {
40515
42078
  const req = http.get(
40516
42079
  {
40517
42080
  host: rt.dashboard_host,
@@ -40523,9 +42086,9 @@ async function probeTenantDashboard(tenant, options = {}) {
40523
42086
  res.resume();
40524
42087
  const status = res.statusCode ?? 0;
40525
42088
  if (status > 0 && status < 500) {
40526
- resolve6({ running: true, status, reason: null });
42089
+ resolve8({ running: true, status, reason: null });
40527
42090
  } else {
40528
- resolve6({
42091
+ resolve8({
40529
42092
  running: false,
40530
42093
  status,
40531
42094
  reason: `dashboard returned ${status}`
@@ -40535,10 +42098,10 @@ async function probeTenantDashboard(tenant, options = {}) {
40535
42098
  );
40536
42099
  req.on("timeout", () => {
40537
42100
  req.destroy();
40538
- resolve6({ running: false, status: null, reason: "timeout" });
42101
+ resolve8({ running: false, status: null, reason: "timeout" });
40539
42102
  });
40540
42103
  req.on("error", (err) => {
40541
- resolve6({
42104
+ resolve8({
40542
42105
  running: false,
40543
42106
  status: null,
40544
42107
  reason: err.code ?? err.message
@@ -40570,10 +42133,17 @@ function resolveCtx(args) {
40570
42133
  };
40571
42134
  }
40572
42135
  async function runAgentsCommand(args) {
42136
+ const fortressIdx = args.argv.indexOf("--fortress");
42137
+ if (fortressIdx !== -1 && args.argv[fortressIdx + 1]) {
42138
+ args = { ...args, root: args.argv[fortressIdx + 1] };
42139
+ const filtered = [...args.argv];
42140
+ filtered.splice(fortressIdx, 2);
42141
+ args = { ...args, argv: filtered };
42142
+ }
40573
42143
  const ctx = resolveCtx(args);
40574
42144
  const [sub, ...rest] = args.argv;
40575
42145
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
40576
- printUsage3(ctx.out);
42146
+ printUsage4(ctx.out);
40577
42147
  return 0;
40578
42148
  }
40579
42149
  try {
@@ -40581,13 +42151,13 @@ async function runAgentsCommand(args) {
40581
42151
  case "list":
40582
42152
  return await cmdList3(rest, ctx);
40583
42153
  case "show":
40584
- return await cmdShow(rest, ctx);
42154
+ return await cmdShow2(rest, ctx);
40585
42155
  case "status":
40586
42156
  return await cmdStatus(rest, ctx);
40587
42157
  default:
40588
42158
  ctx.err.write(`Unknown subcommand: ${sub}
40589
42159
  `);
40590
- printUsage3(ctx.err);
42160
+ printUsage4(ctx.err);
40591
42161
  return 2;
40592
42162
  }
40593
42163
  } catch (e) {
@@ -40597,13 +42167,17 @@ async function runAgentsCommand(args) {
40597
42167
  return 1;
40598
42168
  }
40599
42169
  }
40600
- function printUsage3(s) {
42170
+ function printUsage4(s) {
40601
42171
  s.write(`Usage: sanctuary agents <command> [flags]
40602
42172
 
40603
42173
  list [--json] List every tenant visible on this host.
40604
42174
  show <tenant> [--json] Show details for one tenant.
40605
42175
  status [--json] One-line-per-tenant running/stopped summary.
40606
42176
 
42177
+ Options:
42178
+ --fortress <path> Scope discovery to a specific storage path
42179
+ instead of scanning ~/.sanctuary.
42180
+
40607
42181
  Tenants are discovered by scanning ~/.sanctuary and any storage paths in
40608
42182
  SANCTUARY_AGENTS_EXTRA_PATHS or ~/.sanctuary/agents-extra.json. Tenant
40609
42183
  creation is done via \`sanctuary wrap\` with SANCTUARY_STORAGE_PATH set.
@@ -40687,7 +42261,7 @@ async function cmdList3(argv, ctx) {
40687
42261
  }
40688
42262
  return 0;
40689
42263
  }
40690
- async function cmdShow(argv, ctx) {
42264
+ async function cmdShow2(argv, ctx) {
40691
42265
  const positional = argv.find((a) => !a.startsWith("--"));
40692
42266
  if (!positional) {
40693
42267
  ctx.err.write("Missing tenant. Usage: sanctuary agents show <tenant>\n");
@@ -40828,7 +42402,8 @@ var init_agents = __esm({
40828
42402
  // src/cli/reset-passphrase.ts
40829
42403
  var reset_passphrase_exports = {};
40830
42404
  __export(reset_passphrase_exports, {
40831
- runResetPassphraseCommand: () => runResetPassphraseCommand
42405
+ runResetPassphraseCommand: () => runResetPassphraseCommand,
42406
+ zeroizeBuffers: () => zeroizeBuffers
40832
42407
  });
40833
42408
  async function runResetPassphraseCommand(args) {
40834
42409
  const out = args.out ?? process.stdout;
@@ -40838,10 +42413,10 @@ async function runResetPassphraseCommand(args) {
40838
42413
  const plat = args.platformOverride ?? process.platform;
40839
42414
  const parsed = parseArgs2(args.argv);
40840
42415
  if (parsed.help) {
40841
- printUsage4(out);
42416
+ printUsage5(out);
40842
42417
  return 0;
40843
42418
  }
40844
- const storagePath = parsed.storage ?? args.storagePath ?? resolveStoragePath(process.env, home);
42419
+ const storagePath = parsed.storage ?? parsed.fortress ?? args.storagePath ?? resolveStoragePath(process.env, home);
40845
42420
  out.write(banner(storagePath));
40846
42421
  const runtimeFile = path.join(storagePath, "runtime.json");
40847
42422
  if (await fileExists4(runtimeFile)) {
@@ -40856,34 +42431,42 @@ Then re-run this command.
40856
42431
  return 1;
40857
42432
  }
40858
42433
  const lines = new LineReader(stdin);
42434
+ let code = 1;
42435
+ let nukeSucceeded = false;
40859
42436
  try {
40860
42437
  const availability = await surveyAvailableModes(storagePath);
40861
42438
  const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
40862
42439
  if (!mode) {
40863
42440
  err.write("Aborted: no recovery mode selected.\n");
40864
- return 1;
40865
- }
40866
- if (mode === "shares") {
40867
- return await runSharesPath(out, err, availability);
40868
- }
40869
- if (mode === "guardian") {
40870
- return await runGuardianPath(out, err, availability);
42441
+ code = 1;
42442
+ } else if (mode === "shares") {
42443
+ code = await runSharesPath(out, err, availability);
42444
+ } else if (mode === "guardian") {
42445
+ code = await runGuardianPath(out, err, availability);
42446
+ } else {
42447
+ code = await runNukePath({
42448
+ out,
42449
+ err,
42450
+ lines,
42451
+ storagePath,
42452
+ home,
42453
+ plat,
42454
+ exec: args.exec ?? defaultExec2
42455
+ });
42456
+ nukeSucceeded = mode === "nuke" && code === 0;
40871
42457
  }
40872
- return await runNukePath({
40873
- out,
40874
- err,
40875
- lines,
40876
- storagePath,
40877
- home,
40878
- plat,
40879
- exec: args.exec ?? defaultExec2
40880
- });
40881
42458
  } finally {
42459
+ zeroizeBuffers(args.keyMaterialToZeroize);
40882
42460
  lines.close();
40883
42461
  }
42462
+ if (parsed.exitOnCompletion && nukeSucceeded) {
42463
+ const doExit = args.exitProcess ?? ((c) => process.exit(c));
42464
+ doExit(0);
42465
+ }
42466
+ return code;
40884
42467
  }
40885
42468
  function parseArgs2(argv) {
40886
- const out = { help: false };
42469
+ const out = { exitOnCompletion: false, help: false };
40887
42470
  for (let i = 0; i < argv.length; i++) {
40888
42471
  const a = argv[i];
40889
42472
  if (a === "--help" || a === "-h") {
@@ -40898,13 +42481,17 @@ function parseArgs2(argv) {
40898
42481
  out.mode = v;
40899
42482
  } else if (a === "--storage" && argv[i + 1]) {
40900
42483
  out.storage = argv[++i];
42484
+ } else if (a === "--fortress" && argv[i + 1]) {
42485
+ out.fortress = argv[++i];
42486
+ } else if (a === "--exit-on-completion") {
42487
+ out.exitOnCompletion = true;
40901
42488
  } else if (a && a.startsWith("--")) {
40902
42489
  throw new Error(`Unknown flag: ${a}`);
40903
42490
  }
40904
42491
  }
40905
42492
  return out;
40906
42493
  }
40907
- function printUsage4(out) {
42494
+ function printUsage5(out) {
40908
42495
  out.write(`
40909
42496
  Usage: sanctuary reset-passphrase [options]
40910
42497
 
@@ -40927,9 +42514,19 @@ Recover a fortress whose passphrase has been lost or corrupted. Three modes:
40927
42514
 
40928
42515
  Options:
40929
42516
  --mode <shares|guardian|nuke> Pick a path non-interactively.
40930
- --storage <path> Override the resolved storage path.
40931
- Defaults to SANCTUARY_STORAGE_PATH or
40932
- ~/.sanctuary.
42517
+ --fortress <path> Override the fortress storage path.
42518
+ Consistent with "sanctuary wrap --fortress".
42519
+ --storage <path> Alias for --fortress.
42520
+ --exit-on-completion After a successful nuke, call process.exit(0)
42521
+ immediately so the post-wipe heap is reaped
42522
+ by the OS without re-entering the shell. Use
42523
+ on extreme-threat-model deployments where an
42524
+ attacker-on-host with heap-dump access could
42525
+ recover residual passphrase or key bytes
42526
+ between the wipe and the next operator
42527
+ command. JS strings cannot be explicitly
42528
+ zeroed; this flag is the supported way to
42529
+ bound the heap-dump window.
40933
42530
  --help, -h Show this help.
40934
42531
 
40935
42532
  Without --mode, the command surveys which paths are operationally available
@@ -41199,8 +42796,18 @@ async function prompt(lines, err, question) {
41199
42796
  err.write(question);
41200
42797
  return await lines.next();
41201
42798
  }
42799
+ function zeroizeBuffers(buffers) {
42800
+ if (!buffers) return;
42801
+ for (const b of buffers) {
42802
+ if (!b) continue;
42803
+ try {
42804
+ b.fill(0);
42805
+ } catch {
42806
+ }
42807
+ }
42808
+ }
41202
42809
  async function defaultExec2(cmd, args) {
41203
- return await new Promise((resolve6, reject) => {
42810
+ return await new Promise((resolve8, reject) => {
41204
42811
  const child = child_process.spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
41205
42812
  let stdout = "";
41206
42813
  let stderr = "";
@@ -41211,7 +42818,7 @@ async function defaultExec2(cmd, args) {
41211
42818
  stderr += d.toString();
41212
42819
  });
41213
42820
  child.on("error", reject);
41214
- child.on("close", (code) => resolve6({ stdout, stderr, code }));
42821
+ child.on("close", (code) => resolve8({ stdout, stderr, code }));
41215
42822
  });
41216
42823
  }
41217
42824
  var LineReader;
@@ -41247,8 +42854,8 @@ var init_reset_passphrase = __esm({
41247
42854
  return Promise.resolve(this.queue.shift());
41248
42855
  }
41249
42856
  if (this.closed) return Promise.resolve("");
41250
- return new Promise((resolve6) => {
41251
- this.waiters.push(resolve6);
42857
+ return new Promise((resolve8) => {
42858
+ this.waiters.push(resolve8);
41252
42859
  });
41253
42860
  }
41254
42861
  close() {
@@ -41647,11 +43254,11 @@ async function startMultiDashboardServer(options = {}) {
41647
43254
  }
41648
43255
  }
41649
43256
  });
41650
- await new Promise((resolve6, reject) => {
43257
+ await new Promise((resolve8, reject) => {
41651
43258
  server.once("error", reject);
41652
43259
  server.listen(port, host, () => {
41653
43260
  server.off("error", reject);
41654
- resolve6();
43261
+ resolve8();
41655
43262
  });
41656
43263
  });
41657
43264
  const addr = server.address();
@@ -41660,8 +43267,8 @@ async function startMultiDashboardServer(options = {}) {
41660
43267
  url: `http://${host}:${actualPort}`,
41661
43268
  port: actualPort,
41662
43269
  host,
41663
- stop: () => new Promise((resolve6, reject) => {
41664
- server.close((err) => err ? reject(err) : resolve6());
43270
+ stop: () => new Promise((resolve8, reject) => {
43271
+ server.close((err) => err ? reject(err) : resolve8());
41665
43272
  })
41666
43273
  };
41667
43274
  }
@@ -41879,7 +43486,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
41879
43486
  }
41880
43487
  throw err;
41881
43488
  }
41882
- const policy = await loadPrincipalPolicy(config.storage_path);
43489
+ let policy;
43490
+ try {
43491
+ policy = await loadPrincipalPolicy(config.storage_path);
43492
+ } catch (err) {
43493
+ if (err instanceof MalformedPrincipalPolicyError) {
43494
+ console.error(`
43495
+ Sanctuary cannot start.
43496
+ ${err.message}
43497
+ `);
43498
+ process.exit(1);
43499
+ }
43500
+ throw err;
43501
+ }
41883
43502
  const baseline = new BaselineTracker(storage, masterKey);
41884
43503
  await baseline.load();
41885
43504
  const dashboardPort = options.port ?? config.dashboard.port;
@@ -42061,7 +43680,7 @@ function formatUpdateMessage(current, latest) {
42061
43680
  return `[Sanctuary] Update available: ${current} \u2192 ${latest}. Run: npx @sanctuary-framework/mcp-server@latest`;
42062
43681
  }
42063
43682
  function fetchLatestVersion(currentVersion) {
42064
- return new Promise((resolve6) => {
43683
+ return new Promise((resolve8) => {
42065
43684
  const req = https.get(
42066
43685
  REGISTRY_URL,
42067
43686
  {
@@ -42071,7 +43690,7 @@ function fetchLatestVersion(currentVersion) {
42071
43690
  (res) => {
42072
43691
  if (res.statusCode !== 200) {
42073
43692
  res.resume();
42074
- resolve6(null);
43693
+ resolve8(null);
42075
43694
  return;
42076
43695
  }
42077
43696
  let data = "";
@@ -42080,7 +43699,7 @@ function fetchLatestVersion(currentVersion) {
42080
43699
  data += chunk;
42081
43700
  if (data.length > 32768) {
42082
43701
  res.destroy();
42083
- resolve6(null);
43702
+ resolve8(null);
42084
43703
  }
42085
43704
  });
42086
43705
  res.on("end", () => {
@@ -42088,20 +43707,20 @@ function fetchLatestVersion(currentVersion) {
42088
43707
  const json = JSON.parse(data);
42089
43708
  const latest = json.version;
42090
43709
  if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
42091
- resolve6(latest);
43710
+ resolve8(latest);
42092
43711
  } else {
42093
- resolve6(null);
43712
+ resolve8(null);
42094
43713
  }
42095
43714
  } catch {
42096
- resolve6(null);
43715
+ resolve8(null);
42097
43716
  }
42098
43717
  });
42099
43718
  }
42100
43719
  );
42101
- req.on("error", () => resolve6(null));
43720
+ req.on("error", () => resolve8(null));
42102
43721
  req.on("timeout", () => {
42103
43722
  req.destroy();
42104
- resolve6(null);
43723
+ resolve8(null);
42105
43724
  });
42106
43725
  });
42107
43726
  }
@@ -42179,6 +43798,11 @@ async function main() {
42179
43798
  const code = await runTemplateCommand2({ argv: args.slice(1) });
42180
43799
  process.exit(code);
42181
43800
  }
43801
+ if (args[0] === "identity") {
43802
+ const { runIdentityCommand: runIdentityCommand2 } = await Promise.resolve().then(() => (init_identity2(), identity_exports2));
43803
+ const code = await runIdentityCommand2({ argv: args.slice(1) });
43804
+ process.exit(code);
43805
+ }
42182
43806
  if (args[0] === "agents") {
42183
43807
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
42184
43808
  const code = await runAgentsCommand2({ argv: args.slice(1) });
@@ -42400,6 +44024,9 @@ Subcommands:
42400
44024
  Use "sanctuary dashboard --help" for options.
42401
44025
  Pass --multi to render the multi-tenant overview.
42402
44026
 
44027
+ identity Inspect the active identity (DID, public key).
44028
+ Use "sanctuary identity --help" for options.
44029
+
42403
44030
  template Manage policy templates (list, init).
42404
44031
  Use "sanctuary template --help" for options.
42405
44032