@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.js CHANGED
@@ -4421,13 +4421,23 @@ function parseScalar(value) {
4421
4421
  return value.replace(/^["']|["']$/g, "");
4422
4422
  }
4423
4423
  function validatePolicy(raw) {
4424
+ if (!("tier1_always_approve" in raw)) {
4425
+ throw new Error(
4426
+ "Policy file must include 'tier1_always_approve' as an explicit list (use [] for empty). Remove specific entries instead of removing the whole key."
4427
+ );
4428
+ }
4429
+ if (!("approval_channel" in raw)) {
4430
+ throw new Error(
4431
+ "Policy file must include 'approval_channel' as an explicit object (use {} for defaults). Remove specific entries instead of removing the whole key."
4432
+ );
4433
+ }
4424
4434
  const userTier3 = raw.tier3_always_allow ?? [];
4425
4435
  const mergedTier3 = [
4426
4436
  .../* @__PURE__ */ new Set([...userTier3, ...DEFAULT_POLICY.tier3_always_allow])
4427
4437
  ];
4428
4438
  return {
4429
4439
  version: raw.version ?? 1,
4430
- tier1_always_approve: raw.tier1_always_approve ?? DEFAULT_POLICY.tier1_always_approve,
4440
+ tier1_always_approve: raw.tier1_always_approve,
4431
4441
  tier2_anomaly: {
4432
4442
  ...DEFAULT_TIER2,
4433
4443
  ...raw.tier2_anomaly ?? {}
@@ -4448,6 +4458,11 @@ function generateDefaultPolicyYaml() {
4448
4458
  # This file controls what your agent can do without asking.
4449
4459
  # Edit this file directly. Your agent cannot modify it.
4450
4460
  # Changes take effect on server restart.
4461
+ #
4462
+ # Required keys (must be present; use [] or {} for empty):
4463
+ # tier1_always_approve, approval_channel
4464
+ # Optional keys (omit to use defaults; new defaults merge automatically):
4465
+ # tier2_anomaly, tier3_always_allow
4451
4466
 
4452
4467
  version: 1
4453
4468
 
@@ -4554,21 +4569,39 @@ approval_channel:
4554
4569
  }
4555
4570
  async function loadPrincipalPolicy(storagePath) {
4556
4571
  const policyPath = join(storagePath, "principal-policy.yaml");
4572
+ let content;
4573
+ try {
4574
+ content = await readFile(policyPath, "utf-8");
4575
+ } catch (err) {
4576
+ const code = err?.code;
4577
+ if (code === "ENOENT") {
4578
+ const defaultYaml = generateDefaultPolicyYaml();
4579
+ try {
4580
+ await writeFile(policyPath, defaultYaml, "utf-8");
4581
+ await chmod(policyPath, 384);
4582
+ } catch (writeErr) {
4583
+ console.warn(
4584
+ `Sanctuary: could not write default principal policy to ${policyPath}: ${writeErr.message}. Continuing with in-memory default.`
4585
+ );
4586
+ }
4587
+ return Object.freeze({ ...DEFAULT_POLICY });
4588
+ }
4589
+ throw new MalformedPrincipalPolicyError(
4590
+ policyPath,
4591
+ `read failed: ${err.message}`
4592
+ );
4593
+ }
4557
4594
  try {
4558
- const content = await readFile(policyPath, "utf-8");
4559
4595
  const policy = parsePolicy(content);
4560
4596
  return Object.freeze(policy);
4561
- } catch {
4562
- const defaultYaml = generateDefaultPolicyYaml();
4563
- try {
4564
- await writeFile(policyPath, defaultYaml, "utf-8");
4565
- await chmod(policyPath, 384);
4566
- } catch {
4567
- }
4568
- return Object.freeze({ ...DEFAULT_POLICY });
4597
+ } catch (parseErr) {
4598
+ throw new MalformedPrincipalPolicyError(
4599
+ policyPath,
4600
+ parseErr.message
4601
+ );
4569
4602
  }
4570
4603
  }
4571
- var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY;
4604
+ var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4572
4605
  var init_loader = __esm({
4573
4606
  "src/principal-policy/loader.ts"() {
4574
4607
  DEFAULT_TIER2 = {
@@ -4701,6 +4734,20 @@ var init_loader = __esm({
4701
4734
  ],
4702
4735
  approval_channel: DEFAULT_CHANNEL
4703
4736
  };
4737
+ MalformedPrincipalPolicyError = class extends Error {
4738
+ constructor(policyPath, reason) {
4739
+ super(
4740
+ `Principal policy at ${policyPath} is malformed and cannot be loaded.
4741
+ Reason: ${reason}
4742
+ 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.`
4743
+ );
4744
+ this.policyPath = policyPath;
4745
+ this.reason = reason;
4746
+ this.name = "MalformedPrincipalPolicyError";
4747
+ }
4748
+ policyPath;
4749
+ reason;
4750
+ };
4704
4751
  }
4705
4752
  });
4706
4753
 
@@ -4920,7 +4967,7 @@ function deepSortKeys(obj) {
4920
4967
  return sorted;
4921
4968
  }
4922
4969
  function canonicalizeForSigning(body) {
4923
- return JSON.stringify(deepSortKeys(body));
4970
+ return JSON.stringify(deepSortKeys(body)).normalize("NFC");
4924
4971
  }
4925
4972
  var init_types = __esm({
4926
4973
  "src/shr/types.ts"() {
@@ -11567,6 +11614,7 @@ __export(passphrase_exports, {
11567
11614
  getOrCreatePassphrase: () => getOrCreatePassphrase,
11568
11615
  isOsKeyringLocation: () => isOsKeyringLocation,
11569
11616
  keychainServiceFor: () => keychainServiceFor,
11617
+ legacyKeychainServiceFor: () => legacyKeychainServiceFor,
11570
11618
  persistUserProvidedPassphrase: () => persistUserProvidedPassphrase,
11571
11619
  readStoredPassphrase: () => readStoredPassphrase
11572
11620
  });
@@ -11580,16 +11628,29 @@ async function getOrCreatePassphrase(opts = {}) {
11580
11628
  const plat = opts.platformOverride ?? platform();
11581
11629
  const exec2 = opts.exec ?? defaultExec;
11582
11630
  const derive = opts.deriveMachineKey ?? deriveMachineKey;
11631
+ const legacyService = legacyKeychainServiceFor(storagePath, home);
11583
11632
  if (plat === "darwin") {
11584
11633
  const fromKc = await readFromKeychain(exec2, service);
11585
11634
  if (fromKc) {
11586
11635
  return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11587
11636
  }
11637
+ if (legacyService !== service) {
11638
+ const fromLegacy = await readFromKeychain(exec2, legacyService);
11639
+ if (fromLegacy) {
11640
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11641
+ }
11642
+ }
11588
11643
  } else if (plat === "linux") {
11589
11644
  const fromSs = await readFromSecretService(exec2, service);
11590
11645
  if (fromSs) {
11591
11646
  return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11592
11647
  }
11648
+ if (legacyService !== service) {
11649
+ const fromLegacy = await readFromSecretService(exec2, legacyService);
11650
+ if (fromLegacy) {
11651
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11652
+ }
11653
+ }
11593
11654
  }
11594
11655
  const fallback = fallbackFilePath(home, storagePath);
11595
11656
  const fromFile = await readFromFallbackFile(fallback, home, derive);
@@ -11622,6 +11683,7 @@ async function readStoredPassphrase(opts = {}) {
11622
11683
  const home = opts.home ?? homedir();
11623
11684
  const storagePath = opts.storagePath ?? resolveStoragePath(process.env, home);
11624
11685
  const service = keychainServiceFor(storagePath, home);
11686
+ const legacyService = legacyKeychainServiceFor(storagePath, home);
11625
11687
  const plat = opts.platformOverride ?? platform();
11626
11688
  const exec2 = opts.exec ?? defaultExec;
11627
11689
  const derive = opts.deriveMachineKey ?? deriveMachineKey;
@@ -11630,11 +11692,23 @@ async function readStoredPassphrase(opts = {}) {
11630
11692
  if (fromKc) {
11631
11693
  return { value: fromKc, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11632
11694
  }
11695
+ if (legacyService !== service) {
11696
+ const fromLegacy = await readFromKeychain(exec2, legacyService);
11697
+ if (fromLegacy) {
11698
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_MACOS };
11699
+ }
11700
+ }
11633
11701
  } else if (plat === "linux") {
11634
11702
  const fromSs = await readFromSecretService(exec2, service);
11635
11703
  if (fromSs) {
11636
11704
  return { value: fromSs, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11637
11705
  }
11706
+ if (legacyService !== service) {
11707
+ const fromLegacy = await readFromSecretService(exec2, legacyService);
11708
+ if (fromLegacy) {
11709
+ return { value: fromLegacy, source: "keychain", location: OS_KEYRING_LOCATION_LINUX };
11710
+ }
11711
+ }
11638
11712
  }
11639
11713
  const fallback = fallbackFilePath(home, storagePath);
11640
11714
  const fromFile = await readFromFallbackFile(fallback, home, derive);
@@ -11715,9 +11789,18 @@ async function writeToKeychain(value, exec2, service = KEYCHAIN_SERVICE_DEFAULT)
11715
11789
  }
11716
11790
  }
11717
11791
  function keychainServiceFor(storagePath, home = homedir()) {
11718
- const defaultPath = join(home, DEFAULT_STORAGE_DIR);
11719
- if (storagePath === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11720
- const digest = sha256(Buffer.from(storagePath, "utf-8"));
11792
+ const defaultPath = resolve(join(home, DEFAULT_STORAGE_DIR));
11793
+ const canonicalStorage = resolve(storagePath);
11794
+ if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11795
+ const digest = sha256(Buffer.from(canonicalStorage, "utf-8"));
11796
+ const suffix = Buffer.from(digest).toString("hex").slice(0, 16);
11797
+ return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
11798
+ }
11799
+ function legacyKeychainServiceFor(storagePath, home = homedir()) {
11800
+ const defaultPath = resolve(join(home, DEFAULT_STORAGE_DIR));
11801
+ const canonicalStorage = resolve(storagePath);
11802
+ if (canonicalStorage === defaultPath) return KEYCHAIN_SERVICE_DEFAULT;
11803
+ const digest = sha256(Buffer.from(canonicalStorage, "utf-8"));
11721
11804
  const suffix = Buffer.from(digest).toString("hex").slice(0, 12);
11722
11805
  return `${KEYCHAIN_SERVICE_DEFAULT}-${suffix}`;
11723
11806
  }
@@ -11804,7 +11887,7 @@ function deriveMachineKey(home) {
11804
11887
  return hkdf(sha256, material, void 0, "sanctuary-passphrase-v1", 32);
11805
11888
  }
11806
11889
  async function defaultExec(cmd, args, input) {
11807
- return new Promise((resolve6, reject) => {
11890
+ return new Promise((resolve8, reject) => {
11808
11891
  const child = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
11809
11892
  let stdout = "";
11810
11893
  let stderr = "";
@@ -11815,7 +11898,7 @@ async function defaultExec(cmd, args, input) {
11815
11898
  stderr += d.toString();
11816
11899
  });
11817
11900
  child.on("error", reject);
11818
- child.on("close", (code) => resolve6({ stdout, stderr, code }));
11901
+ child.on("close", (code) => resolve8({ stdout, stderr, code }));
11819
11902
  if (input !== void 0) {
11820
11903
  child.stdin.write(input);
11821
11904
  }
@@ -12009,7 +12092,7 @@ async function discoverTenants(options = {}) {
12009
12092
  for (const child of children) {
12010
12093
  const childPath = join(root, child);
12011
12094
  if (child.startsWith(".")) continue;
12012
- if (child === "state" || child === "backup" || child === "config") continue;
12095
+ if (child === "state" || child === "backup" || child === "config" || child === "default") continue;
12013
12096
  const s = await stat(childPath).catch(() => null);
12014
12097
  if (!s || !s.isDirectory()) continue;
12015
12098
  const desc = await describeTenant(child, childPath, home);
@@ -12021,6 +12104,17 @@ async function discoverTenants(options = {}) {
12021
12104
  const desc = await describeTenant(basename(extra), extra, home);
12022
12105
  if (desc) tenants.push(desc);
12023
12106
  }
12107
+ const seen = /* @__PURE__ */ new Map();
12108
+ for (const t of tenants) {
12109
+ seen.set(t.name, (seen.get(t.name) ?? 0) + 1);
12110
+ }
12111
+ for (const [name, count] of seen) {
12112
+ if (count > 1) {
12113
+ console.error(
12114
+ `[sanctuary] warning: ${count} tenants share the name "${name}". Use --tenant with a unique name or storage path to disambiguate.`
12115
+ );
12116
+ }
12117
+ }
12024
12118
  tenants.sort((a, b) => {
12025
12119
  if (a.name === "default") return -1;
12026
12120
  if (b.name === "default") return 1;
@@ -12372,7 +12466,7 @@ var init_auth_middleware = __esm({
12372
12466
  });
12373
12467
 
12374
12468
  // src/hub/constants.ts
12375
- 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;
12469
+ 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;
12376
12470
  var init_constants3 = __esm({
12377
12471
  "src/hub/constants.ts"() {
12378
12472
  HUB_API_PREFIX = "/api/hub";
@@ -12400,6 +12494,16 @@ var init_constants3 = __esm({
12400
12494
  */
12401
12495
  CHAT_CONCIERGE_SEND: "/api/hub/chat/concierge",
12402
12496
  CHAT_CONCIERGE_HISTORY: "/api/hub/chat/concierge/history",
12497
+ /**
12498
+ * Concierge memory thread routes (WP-V1.3-9 Tau-1). Thread enumeration,
12499
+ * scrollback, and operator-initiated thread delete. Distinct from the
12500
+ * v1.2 `/history` route, which surfaces the active in-session thread
12501
+ * shape; the new routes target persisted multi-thread memory used by
12502
+ * v1.3 conversational sovereignty depth.
12503
+ */
12504
+ CHAT_CONCIERGE_THREADS_LIST: "/api/hub/chat/concierge/threads",
12505
+ CHAT_CONCIERGE_THREAD_READ: "/api/hub/chat/concierge/threads/:thread_id",
12506
+ CHAT_CONCIERGE_THREAD_DELETE: "/api/hub/chat/concierge/threads/:thread_id",
12403
12507
  /**
12404
12508
  * Click-to-inspect panel (WP-V1.2 reshape). Returns the agent's
12405
12509
  * recent activity feed, pending Tier 1 approvals routed through this
@@ -12423,6 +12527,10 @@ var init_constants3 = __esm({
12423
12527
  ];
12424
12528
  HUB_ACTIVITY_DEFAULT_LIMIT = 50;
12425
12529
  HUB_ACTIVITY_MAX_LIMIT = 500;
12530
+ HUB_CHAT_THREADS_DEFAULT_LIMIT = 50;
12531
+ HUB_CHAT_THREADS_MAX_LIMIT = 500;
12532
+ HUB_CHAT_TURNS_DEFAULT_LIMIT = 200;
12533
+ HUB_CHAT_TURNS_MAX_LIMIT = 1e3;
12426
12534
  HUB_INBOX_DEFAULT_LIMIT = 100;
12427
12535
  HUB_INBOX_MAX_LIMIT = 500;
12428
12536
  HUB_AGENTS_DEFAULT_LIMIT = 100;
@@ -12605,6 +12713,23 @@ function checkChatMessage(value) {
12605
12713
  }
12606
12714
  return trimmed;
12607
12715
  }
12716
+ function matchConciergeThreadRoute(path) {
12717
+ const prefix = `${HUB_API_PREFIX}/chat/concierge/threads/`;
12718
+ if (!path.startsWith(prefix)) return null;
12719
+ const rest = path.slice(prefix.length);
12720
+ if (rest.length === 0 || rest.includes("/")) return null;
12721
+ const decoded = decodeURIComponent(rest);
12722
+ if (decoded.length === 0) return null;
12723
+ return { threadId: decoded };
12724
+ }
12725
+ function parseSince(raw) {
12726
+ if (raw === null || raw === "") return void 0;
12727
+ const parsed = Number.parseInt(raw, 10);
12728
+ if (Number.isNaN(parsed) || parsed < 0) {
12729
+ throw new HubValidationError("since must be a non-negative integer");
12730
+ }
12731
+ return parsed;
12732
+ }
12608
12733
  function matchInboxRoute(path) {
12609
12734
  const prefix = `${HUB_API_PREFIX}/inbox/`;
12610
12735
  if (!path.startsWith(prefix)) return null;
@@ -12788,6 +12913,47 @@ async function handleHubRoute(deps, req, res) {
12788
12913
  writeJSON2(res, 200, { ok: true, data: { messages } });
12789
12914
  return true;
12790
12915
  }
12916
+ if (method === "GET" && path === HUB_ROUTES.CHAT_CONCIERGE_THREADS_LIST) {
12917
+ const limit = parseLimit(
12918
+ url.searchParams.get("limit"),
12919
+ HUB_CHAT_THREADS_DEFAULT_LIMIT,
12920
+ HUB_CHAT_THREADS_MAX_LIMIT
12921
+ );
12922
+ const threads = await deps.service.listConciergeMemoryThreads({ limit });
12923
+ writeJSON2(res, 200, { ok: true, data: { threads } });
12924
+ return true;
12925
+ }
12926
+ {
12927
+ const threadMatch = matchConciergeThreadRoute(path);
12928
+ if (threadMatch) {
12929
+ if (method === "GET") {
12930
+ const since = parseSince(url.searchParams.get("since"));
12931
+ const limit = parseLimit(
12932
+ url.searchParams.get("limit"),
12933
+ HUB_CHAT_TURNS_DEFAULT_LIMIT,
12934
+ HUB_CHAT_TURNS_MAX_LIMIT
12935
+ );
12936
+ const readOpts = { limit };
12937
+ if (since !== void 0) readOpts.sinceTurnId = since;
12938
+ const turns = await deps.service.readConciergeMemoryThread(
12939
+ threadMatch.threadId,
12940
+ readOpts
12941
+ );
12942
+ writeJSON2(res, 200, { ok: true, data: { turns } });
12943
+ return true;
12944
+ }
12945
+ if (method === "DELETE") {
12946
+ const removed = await deps.service.deleteConciergeMemoryThread(
12947
+ threadMatch.threadId
12948
+ );
12949
+ writeJSON2(res, removed ? 200 : 404, {
12950
+ ok: removed,
12951
+ data: { thread_id: threadMatch.threadId, removed }
12952
+ });
12953
+ return true;
12954
+ }
12955
+ }
12956
+ }
12791
12957
  writeJSON2(res, 404, { ok: false, error: "not_found", path });
12792
12958
  return true;
12793
12959
  } catch (err) {
@@ -16819,6 +16985,8 @@ var init_intelligence_api_router = __esm({
16819
16985
  this.code = code;
16820
16986
  this.name = "IntelligenceRouterError";
16821
16987
  }
16988
+ statusCode;
16989
+ code;
16822
16990
  };
16823
16991
  }
16824
16992
  });
@@ -16908,6 +17076,168 @@ var init_dispatch = __esm({
16908
17076
  init_intelligence_api_router();
16909
17077
  }
16910
17078
  });
17079
+
17080
+ // src/principal-policy/approval-aggregator-routes.ts
17081
+ function writeJSON4(res, status, payload) {
17082
+ res.writeHead(status, {
17083
+ "Content-Type": "application/json",
17084
+ "Cache-Control": "no-store"
17085
+ });
17086
+ res.end(JSON.stringify(payload));
17087
+ }
17088
+ function parseLimit2(raw, defaultValue, max) {
17089
+ if (raw === null || raw === "") return defaultValue;
17090
+ const parsed = Number.parseInt(raw, 10);
17091
+ if (Number.isNaN(parsed) || parsed < 0) {
17092
+ return defaultValue;
17093
+ }
17094
+ return Math.min(parsed, max);
17095
+ }
17096
+ function isStatusFilter(value) {
17097
+ return value === "pending" || value === "approved" || value === "denied" || value === "timeout" || value === "expired";
17098
+ }
17099
+ function matchEntryRoute(path) {
17100
+ const prefix = `${APPROVAL_INBOX_API_PREFIX}/`;
17101
+ if (!path.startsWith(prefix)) return null;
17102
+ const rest = path.slice(prefix.length);
17103
+ if (rest.length === 0) return null;
17104
+ const slash = rest.indexOf("/");
17105
+ if (slash === -1) {
17106
+ return { aggregatorId: decodeURIComponent(rest), action: null };
17107
+ }
17108
+ return {
17109
+ aggregatorId: decodeURIComponent(rest.slice(0, slash)),
17110
+ action: rest.slice(slash + 1)
17111
+ };
17112
+ }
17113
+ async function handleStream2(deps, res) {
17114
+ res.writeHead(200, {
17115
+ "Content-Type": "text/event-stream",
17116
+ "Cache-Control": "no-cache, no-transform",
17117
+ Connection: "keep-alive",
17118
+ "X-Accel-Buffering": "no"
17119
+ });
17120
+ const initial = await deps.aggregator.list({ status: "pending" });
17121
+ res.write(
17122
+ `event: approval_inbox_snapshot
17123
+ data: ${JSON.stringify({ entries: initial })}
17124
+
17125
+ `
17126
+ );
17127
+ const unsubscribe = deps.aggregator.onEvent((event) => {
17128
+ try {
17129
+ res.write(
17130
+ `event: approval_inbox_${event.type}
17131
+ data: ${JSON.stringify(event.entry)}
17132
+
17133
+ `
17134
+ );
17135
+ } catch {
17136
+ }
17137
+ });
17138
+ const keepAlive = setInterval(() => {
17139
+ try {
17140
+ res.write(": keepalive\n\n");
17141
+ } catch {
17142
+ }
17143
+ }, 25e3);
17144
+ const cleanup = () => {
17145
+ clearInterval(keepAlive);
17146
+ unsubscribe();
17147
+ };
17148
+ res.on("close", cleanup);
17149
+ res.on("error", cleanup);
17150
+ }
17151
+ async function handleApprovalInboxRoute(deps, req, res) {
17152
+ const host = req.headers.host || "localhost";
17153
+ const url = new URL(req.url ?? "/", `http://${host}`);
17154
+ const method = (req.method ?? "GET").toUpperCase();
17155
+ const path = url.pathname;
17156
+ if (path !== APPROVAL_INBOX_API_PREFIX && !path.startsWith(`${APPROVAL_INBOX_API_PREFIX}/`)) {
17157
+ return false;
17158
+ }
17159
+ const checkAuth = authMiddleware(deps.authConfig);
17160
+ if (!checkAuth(req, res, url)) return true;
17161
+ try {
17162
+ if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/stream`) {
17163
+ await handleStream2(deps, res);
17164
+ return true;
17165
+ }
17166
+ if (method === "GET" && path === APPROVAL_INBOX_API_PREFIX) {
17167
+ const limit = parseLimit2(
17168
+ url.searchParams.get("limit"),
17169
+ APPROVAL_INBOX_DEFAULT_LIMIT,
17170
+ APPROVAL_INBOX_MAX_LIMIT
17171
+ );
17172
+ const statusRaw = url.searchParams.get("status");
17173
+ const status = statusRaw && isStatusFilter(statusRaw) ? statusRaw : "pending";
17174
+ const sinceTs = url.searchParams.get("since") ?? void 0;
17175
+ const entries = await deps.aggregator.list({
17176
+ status,
17177
+ limit,
17178
+ ...sinceTs !== void 0 ? { sinceTs } : {}
17179
+ });
17180
+ writeJSON4(res, 200, { ok: true, data: { entries } });
17181
+ return true;
17182
+ }
17183
+ const entryMatch = matchEntryRoute(path);
17184
+ if (entryMatch === null) {
17185
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
17186
+ return true;
17187
+ }
17188
+ if (method === "GET" && entryMatch.action === null) {
17189
+ const entries = await deps.aggregator.list({ limit: APPROVAL_INBOX_MAX_LIMIT });
17190
+ const entry = entries.find(
17191
+ (e) => e.aggregator_id === entryMatch.aggregatorId
17192
+ );
17193
+ if (!entry) {
17194
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17195
+ return true;
17196
+ }
17197
+ const payload = await deps.aggregator.getFullPayload(
17198
+ entryMatch.aggregatorId
17199
+ );
17200
+ writeJSON4(res, 200, { ok: true, data: { entry, request_payload: payload } });
17201
+ return true;
17202
+ }
17203
+ if (method === "POST" && (entryMatch.action === "approve" || entryMatch.action === "deny")) {
17204
+ const decision = entryMatch.action === "approve" ? "approved" : "denied";
17205
+ const operatorId = deps.operatorId ?? APPROVAL_INBOX_OPERATOR_DEFAULT;
17206
+ try {
17207
+ const entry = await deps.aggregator.resolve(
17208
+ entryMatch.aggregatorId,
17209
+ decision,
17210
+ operatorId
17211
+ );
17212
+ writeJSON4(res, 200, { ok: true, data: { entry } });
17213
+ } catch (err) {
17214
+ const msg = err instanceof Error ? err.message : String(err);
17215
+ if (msg === "approval-aggregator: not_found") {
17216
+ writeJSON4(res, 404, { ok: false, error: "not_found" });
17217
+ } else {
17218
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
17219
+ }
17220
+ }
17221
+ return true;
17222
+ }
17223
+ writeJSON4(res, 404, { ok: false, error: "not_found", path });
17224
+ return true;
17225
+ } catch (err) {
17226
+ const msg = err instanceof Error ? err.message : String(err);
17227
+ writeJSON4(res, 500, { ok: false, error: "internal", detail: msg });
17228
+ return true;
17229
+ }
17230
+ }
17231
+ var APPROVAL_INBOX_API_PREFIX, APPROVAL_INBOX_OPERATOR_DEFAULT, APPROVAL_INBOX_DEFAULT_LIMIT, APPROVAL_INBOX_MAX_LIMIT;
17232
+ var init_approval_aggregator_routes = __esm({
17233
+ "src/principal-policy/approval-aggregator-routes.ts"() {
17234
+ init_auth_middleware();
17235
+ APPROVAL_INBOX_API_PREFIX = "/api/approval-inbox";
17236
+ APPROVAL_INBOX_OPERATOR_DEFAULT = "operator_dashboard";
17237
+ APPROVAL_INBOX_DEFAULT_LIMIT = 50;
17238
+ APPROVAL_INBOX_MAX_LIMIT = 200;
17239
+ }
17240
+ });
16911
17241
  function isDashboardViewRoute(method, path) {
16912
17242
  if (method !== "GET") return false;
16913
17243
  return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
@@ -16921,6 +17251,7 @@ var init_dashboard = __esm({
16921
17251
  init_fortress_view();
16922
17252
  init_system_prompt_generator();
16923
17253
  init_dispatch();
17254
+ init_approval_aggregator_routes();
16924
17255
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
16925
17256
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
16926
17257
  MAX_SESSIONS = 1e3;
@@ -16980,6 +17311,14 @@ var init_dashboard = __esm({
16980
17311
  * regardless. Default route flip is deferred to v1.2.
16981
17312
  */
16982
17313
  v11Bindings = null;
17314
+ /**
17315
+ * v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
17316
+ * additively at `/api/approval-inbox/*` when set. Legacy approval
17317
+ * routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
17318
+ * aggregator is a passive subscriber to the gate; the routes here are
17319
+ * the operator-facing query / decision surface.
17320
+ */
17321
+ approvalAggregator = null;
16983
17322
  constructor(config) {
16984
17323
  this.config = config;
16985
17324
  this.authToken = config.auth_token;
@@ -17030,6 +17369,34 @@ var init_dashboard = __esm({
17030
17369
  setV11Bindings(bindings) {
17031
17370
  this.v11Bindings = bindings;
17032
17371
  }
17372
+ /**
17373
+ * v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
17374
+ * aggregator. Once set, requests to `/api/approval-inbox/*` route
17375
+ * through `handleApprovalInboxRoute`. Pass `null` to detach (used by
17376
+ * tests + during shutdown).
17377
+ */
17378
+ setApprovalAggregator(aggregator) {
17379
+ this.approvalAggregator = aggregator;
17380
+ }
17381
+ /**
17382
+ * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
17383
+ * before the legacy approval route table. Returns true when served.
17384
+ */
17385
+ async dispatchApprovalInbox(req, res) {
17386
+ if (!this.approvalAggregator) return false;
17387
+ return handleApprovalInboxRoute(
17388
+ {
17389
+ authConfig: {
17390
+ loopbackAutoAuth: this._autoAuthLocalhost,
17391
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
17392
+ },
17393
+ aggregator: this.approvalAggregator,
17394
+ operatorId: this.identityManager?.getPrimaryIdentityId() ?? void 0
17395
+ },
17396
+ req,
17397
+ res
17398
+ );
17399
+ }
17033
17400
  /**
17034
17401
  * v1.1 dispatch entry point. Called from `handleRequest` before the
17035
17402
  * legacy route table. Returns true when the request was served by v1.1
@@ -17095,7 +17462,7 @@ var init_dashboard = __esm({
17095
17462
  server = createServer$2(handler);
17096
17463
  }
17097
17464
  this.httpServer = server;
17098
- return new Promise((resolve6, reject) => {
17465
+ return new Promise((resolve8, reject) => {
17099
17466
  const protocol = this.useTLS ? "https" : "http";
17100
17467
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
17101
17468
  server.listen(this.config.port, this.config.host, () => {
@@ -17120,7 +17487,7 @@ var init_dashboard = __esm({
17120
17487
  if (shouldAutoOpen) {
17121
17488
  this.openInBrowser(sessionUrl);
17122
17489
  }
17123
- resolve6();
17490
+ resolve8();
17124
17491
  });
17125
17492
  server.on("error", (err) => {
17126
17493
  if (err.code === "EADDRINUSE") {
@@ -17166,8 +17533,8 @@ var init_dashboard = __esm({
17166
17533
  }
17167
17534
  this.rateLimits.clear();
17168
17535
  if (this.httpServer) {
17169
- return new Promise((resolve6) => {
17170
- this.httpServer.close(() => resolve6());
17536
+ return new Promise((resolve8) => {
17537
+ this.httpServer.close(() => resolve8());
17171
17538
  });
17172
17539
  }
17173
17540
  }
@@ -17181,7 +17548,7 @@ var init_dashboard = __esm({
17181
17548
  `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
17182
17549
  `
17183
17550
  );
17184
- return new Promise((resolve6) => {
17551
+ return new Promise((resolve8) => {
17185
17552
  const timer = setTimeout(() => {
17186
17553
  this.pending.delete(id);
17187
17554
  const response = {
@@ -17195,12 +17562,12 @@ var init_dashboard = __esm({
17195
17562
  decision: response.decision,
17196
17563
  decided_by: "timeout"
17197
17564
  });
17198
- resolve6(response);
17565
+ resolve8(response);
17199
17566
  }, this.config.timeout_seconds * 1e3);
17200
17567
  const pending = {
17201
17568
  id,
17202
17569
  request,
17203
- resolve: resolve6,
17570
+ resolve: resolve8,
17204
17571
  timer,
17205
17572
  created_at: (/* @__PURE__ */ new Date()).toISOString()
17206
17573
  };
@@ -17405,6 +17772,18 @@ var init_dashboard = __esm({
17405
17772
  res.end();
17406
17773
  return;
17407
17774
  }
17775
+ if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
17776
+ this.dispatchApprovalInbox(req, res).then((handled) => {
17777
+ if (handled) return;
17778
+ this.handleLegacyRequest(req, res, url, method);
17779
+ }).catch(() => {
17780
+ if (!res.headersSent) {
17781
+ res.writeHead(500, { "Content-Type": "application/json" });
17782
+ res.end(JSON.stringify({ error: "Internal server error" }));
17783
+ }
17784
+ });
17785
+ return;
17786
+ }
17408
17787
  if (this.v11Bindings) {
17409
17788
  this.dispatchV11(req, res, url, method).then((handled) => {
17410
17789
  if (handled) return;
@@ -18066,7 +18445,7 @@ var init_webhook = __esm({
18066
18445
  * Start the callback listener server.
18067
18446
  */
18068
18447
  async start() {
18069
- return new Promise((resolve6, reject) => {
18448
+ return new Promise((resolve8, reject) => {
18070
18449
  this.callbackServer = createServer$2(
18071
18450
  (req, res) => this.handleCallback(req, res)
18072
18451
  );
@@ -18081,7 +18460,7 @@ var init_webhook = __esm({
18081
18460
 
18082
18461
  `
18083
18462
  );
18084
- resolve6();
18463
+ resolve8();
18085
18464
  }
18086
18465
  );
18087
18466
  this.callbackServer.on("error", reject);
@@ -18101,8 +18480,8 @@ var init_webhook = __esm({
18101
18480
  }
18102
18481
  this.pending.clear();
18103
18482
  if (this.callbackServer) {
18104
- return new Promise((resolve6) => {
18105
- this.callbackServer.close(() => resolve6());
18483
+ return new Promise((resolve8) => {
18484
+ this.callbackServer.close(() => resolve8());
18106
18485
  });
18107
18486
  }
18108
18487
  }
@@ -18115,7 +18494,7 @@ var init_webhook = __esm({
18115
18494
  `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
18116
18495
  `
18117
18496
  );
18118
- return new Promise((resolve6) => {
18497
+ return new Promise((resolve8) => {
18119
18498
  const timer = setTimeout(() => {
18120
18499
  this.pending.delete(id);
18121
18500
  const response = {
@@ -18124,12 +18503,12 @@ var init_webhook = __esm({
18124
18503
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
18125
18504
  decided_by: "timeout"
18126
18505
  };
18127
- resolve6(response);
18506
+ resolve8(response);
18128
18507
  }, this.config.timeout_seconds * 1e3);
18129
18508
  const pending = {
18130
18509
  id,
18131
18510
  request,
18132
- resolve: resolve6,
18511
+ resolve: resolve8,
18133
18512
  timer,
18134
18513
  created_at: (/* @__PURE__ */ new Date()).toISOString()
18135
18514
  };
@@ -19422,12 +19801,25 @@ var init_injection_detector = __esm({
19422
19801
  }
19423
19802
  });
19424
19803
 
19804
+ // src/principal-policy/deny-vocabulary.ts
19805
+ var AGENT_VISIBLE_DENY_REASONS;
19806
+ var init_deny_vocabulary = __esm({
19807
+ "src/principal-policy/deny-vocabulary.ts"() {
19808
+ AGENT_VISIBLE_DENY_REASONS = {
19809
+ REQUIRES_APPROVAL: "operation requires operator approval",
19810
+ NOT_PERMITTED: "operation not permitted",
19811
+ REQUIRES_OPERATOR: "operation requires operator action"
19812
+ };
19813
+ }
19814
+ });
19815
+
19425
19816
  // src/principal-policy/gate.ts
19426
19817
  var ApprovalGate;
19427
19818
  var init_gate = __esm({
19428
19819
  "src/principal-policy/gate.ts"() {
19429
19820
  init_loader();
19430
19821
  init_injection_detector();
19822
+ init_deny_vocabulary();
19431
19823
  ApprovalGate = class {
19432
19824
  policy;
19433
19825
  baseline;
@@ -19435,14 +19827,25 @@ var init_gate = __esm({
19435
19827
  auditLog;
19436
19828
  injectionDetector;
19437
19829
  onInjectionAlert;
19830
+ onApprovalEvent;
19438
19831
  proxyTierResolver;
19439
- constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
19832
+ constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert, onApprovalEvent) {
19440
19833
  this.policy = policy;
19441
19834
  this.baseline = baseline;
19442
19835
  this.channel = channel;
19443
19836
  this.auditLog = auditLog;
19444
19837
  this.injectionDetector = injectionDetector ?? new InjectionDetector();
19445
19838
  this.onInjectionAlert = onInjectionAlert;
19839
+ this.onApprovalEvent = onApprovalEvent;
19840
+ }
19841
+ /**
19842
+ * Set the approval-event callback after construction. Used by the
19843
+ * Upsilon-1 wire-up when the aggregator is constructed alongside the
19844
+ * gate. The aggregator subscribes through this setter rather than the
19845
+ * constructor so existing call sites continue to work unchanged.
19846
+ */
19847
+ setApprovalEventCallback(cb) {
19848
+ this.onApprovalEvent = cb;
19446
19849
  }
19447
19850
  /**
19448
19851
  * Set the proxy tier resolver. Called after the proxy router is initialized.
@@ -19479,10 +19882,16 @@ var init_gate = __esm({
19479
19882
  });
19480
19883
  }
19481
19884
  if (injectionResult.recommendation === "block") {
19885
+ this.auditLog.append("l2", `gate_injection_block:${operation}`, "system", {
19886
+ tier: 1,
19887
+ operation,
19888
+ injection_confidence: injectionResult.confidence,
19889
+ signal_count: injectionResult.signals.length
19890
+ });
19482
19891
  return {
19483
19892
  allowed: false,
19484
19893
  tier: 1,
19485
- reason: `Blocked: prompt injection detected in "${operation}" (confidence: ${(injectionResult.confidence * 100).toFixed(0)}%)`,
19894
+ reason: AGENT_VISIBLE_DENY_REASONS.NOT_PERMITTED,
19486
19895
  approval_required: false
19487
19896
  };
19488
19897
  }
@@ -19559,7 +19968,7 @@ var init_gate = __esm({
19559
19968
  this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
19560
19969
  tier: 1,
19561
19970
  operation,
19562
- warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
19971
+ warning: "Operation is not classified in any policy tier, defaulting to Tier 1 (require approval)"
19563
19972
  });
19564
19973
  return this.requestApproval(
19565
19974
  operation,
@@ -19670,25 +20079,109 @@ var init_gate = __esm({
19670
20079
  }
19671
20080
  /**
19672
20081
  * Request approval from the human principal.
20082
+ *
20083
+ * Fail-closed contract (full-sweep #49): if the channel throws (network
20084
+ * down, callback unreachable, dashboard SSE peer dropped, webhook DNS
20085
+ * failure, etc.), the gate denies the operation and audit-logs the cause.
20086
+ * Channel-internal timeouts already resolve with decision: "deny" per
20087
+ * SEC-002; this catch covers the remaining "channel raised" path so an
20088
+ * unhandled rejection cannot turn into an indeterminate state at the gate.
19673
20089
  */
19674
20090
  async requestApproval(operation, tier, reason, context) {
20091
+ const requestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
19675
20092
  const request = {
19676
20093
  operation,
19677
20094
  tier,
19678
20095
  reason,
19679
20096
  context,
19680
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
20097
+ timestamp: requestTimestamp
19681
20098
  };
19682
- const response = await this.channel.requestApproval(request);
20099
+ const correlationId = `${requestTimestamp}:${operation}:${Math.random().toString(16).slice(2, 6)}`;
20100
+ if (this.onApprovalEvent) {
20101
+ try {
20102
+ this.onApprovalEvent({
20103
+ phase: "requested",
20104
+ operation,
20105
+ tier,
20106
+ reason,
20107
+ context,
20108
+ request_timestamp: requestTimestamp,
20109
+ correlation_id: correlationId
20110
+ });
20111
+ } catch {
20112
+ }
20113
+ }
20114
+ let response;
20115
+ try {
20116
+ response = await this.channel.requestApproval(request);
20117
+ } catch (err) {
20118
+ const errMessage = err instanceof Error ? err.message : String(err);
20119
+ const decidedAt = (/* @__PURE__ */ new Date()).toISOString();
20120
+ this.auditLog.append("l2", `gate_deny:${operation}`, "system", {
20121
+ tier,
20122
+ reason,
20123
+ decided_by: "channel_failure",
20124
+ channel_error: errMessage
20125
+ });
20126
+ if (this.onApprovalEvent) {
20127
+ try {
20128
+ this.onApprovalEvent({
20129
+ phase: "resolved",
20130
+ operation,
20131
+ tier,
20132
+ reason,
20133
+ context,
20134
+ request_timestamp: requestTimestamp,
20135
+ resolution: {
20136
+ decision: "deny",
20137
+ decided_at: decidedAt,
20138
+ decided_by: "channel_failure"
20139
+ },
20140
+ correlation_id: correlationId
20141
+ });
20142
+ } catch {
20143
+ }
20144
+ }
20145
+ return {
20146
+ allowed: false,
20147
+ tier,
20148
+ reason: AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
20149
+ approval_required: true,
20150
+ approval_response: {
20151
+ decision: "deny",
20152
+ decided_at: decidedAt,
20153
+ decided_by: "channel_failure"
20154
+ }
20155
+ };
20156
+ }
19683
20157
  this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
19684
20158
  tier,
19685
20159
  reason,
19686
20160
  decided_by: response.decided_by
19687
20161
  });
20162
+ if (this.onApprovalEvent) {
20163
+ try {
20164
+ this.onApprovalEvent({
20165
+ phase: "resolved",
20166
+ operation,
20167
+ tier,
20168
+ reason,
20169
+ context,
20170
+ request_timestamp: requestTimestamp,
20171
+ resolution: {
20172
+ decision: response.decision,
20173
+ decided_at: response.decided_at,
20174
+ decided_by: response.decided_by
20175
+ },
20176
+ correlation_id: correlationId
20177
+ });
20178
+ } catch {
20179
+ }
20180
+ }
19688
20181
  return {
19689
20182
  allowed: response.decision === "approve",
19690
20183
  tier,
19691
- reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : `Tier ${tier} operation requires approval`,
20184
+ reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : AGENT_VISIBLE_DENY_REASONS.REQUIRES_APPROVAL,
19692
20185
  approval_required: true,
19693
20186
  approval_response: response
19694
20187
  };
@@ -19719,6 +20212,356 @@ var init_gate = __esm({
19719
20212
  };
19720
20213
  }
19721
20214
  });
20215
+ 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;
20216
+ var init_approval_aggregator = __esm({
20217
+ "src/principal-policy/approval-aggregator.ts"() {
20218
+ init_encryption();
20219
+ init_key_derivation();
20220
+ init_encoding();
20221
+ APPROVAL_AGGREGATOR_NAMESPACE = "_approval_aggregator";
20222
+ APPROVAL_AGGREGATOR_HKDF_INFO = "l2-approval-aggregator-v1";
20223
+ APPROVAL_AGGREGATOR_AUDIT_OPS = {
20224
+ AGGREGATED: "cross_harness_approval_aggregated",
20225
+ RESOLVED: "cross_harness_approval_resolved",
20226
+ DEDUPED: "cross_harness_approval_deduped"
20227
+ };
20228
+ DEFAULT_PENDING_TTL_MS = 5 * 60 * 1e3;
20229
+ DEFAULT_MAX_LIST_LIMIT = 200;
20230
+ DEFAULT_LIST_PAGE_SIZE = 50;
20231
+ ApprovalAggregator = class {
20232
+ storage;
20233
+ encryptionKey;
20234
+ auditLog;
20235
+ identityId;
20236
+ fortressId;
20237
+ pendingTtlMs;
20238
+ maxListLimit;
20239
+ now;
20240
+ resolveSourceContext;
20241
+ resolveHubInboxItemId;
20242
+ /** Cached entries by `aggregator_id`. */
20243
+ entries = /* @__PURE__ */ new Map();
20244
+ /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
20245
+ dedupIndex = /* @__PURE__ */ new Map();
20246
+ /** Correlation index: gate `correlation_id` -> aggregator_id. */
20247
+ correlationIndex = /* @__PURE__ */ new Map();
20248
+ /** Original request payloads kept in-memory for `getFullPayload()`. */
20249
+ fullPayloads = /* @__PURE__ */ new Map();
20250
+ /** Has the aggregator hydrated persisted entries on this process? */
20251
+ hydrated = false;
20252
+ /** Active SSE listeners. */
20253
+ listeners = /* @__PURE__ */ new Set();
20254
+ constructor(deps) {
20255
+ this.storage = deps.storage;
20256
+ this.encryptionKey = derivePurposeKey(
20257
+ deps.masterKey,
20258
+ APPROVAL_AGGREGATOR_HKDF_INFO
20259
+ );
20260
+ this.auditLog = deps.auditLog;
20261
+ this.identityId = deps.identityId;
20262
+ this.fortressId = deps.fortressId;
20263
+ this.pendingTtlMs = deps.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
20264
+ this.maxListLimit = deps.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
20265
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
20266
+ this.resolveSourceContext = deps.resolveSourceContext ?? ((_event) => ({
20267
+ source_harness: this.fortressId,
20268
+ source_agent_id: this.fortressId
20269
+ }));
20270
+ this.resolveHubInboxItemId = deps.resolveHubInboxItemId ?? ((_event) => void 0);
20271
+ }
20272
+ /**
20273
+ * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
20274
+ * use this to forward aggregator emissions to the dashboard.
20275
+ */
20276
+ onEvent(listener) {
20277
+ this.listeners.add(listener);
20278
+ return () => this.listeners.delete(listener);
20279
+ }
20280
+ /**
20281
+ * Ingest a gate event. Returns the aggregator entry on first sight,
20282
+ * `null` when deduped. Resolution events update the existing record;
20283
+ * unmatched resolutions are dropped silently (caller's gate emitted a
20284
+ * resolved-without-requested pair, which the aggregator does not invent
20285
+ * a record for).
20286
+ */
20287
+ async ingest(event) {
20288
+ await this.hydrate();
20289
+ if (event.phase === "requested") {
20290
+ return this.ingestRequested(event);
20291
+ }
20292
+ if (event.phase === "resolved") {
20293
+ return this.ingestResolved(event);
20294
+ }
20295
+ return null;
20296
+ }
20297
+ /**
20298
+ * List pending or recently resolved entries. Pending entries past TTL
20299
+ * are lazily transitioned to `expired` and persisted before the list
20300
+ * snapshot is returned.
20301
+ */
20302
+ async list(opts) {
20303
+ await this.hydrate();
20304
+ await this.expireStale();
20305
+ const limit = Math.min(
20306
+ opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
20307
+ this.maxListLimit
20308
+ );
20309
+ const sinceMs = opts?.sinceTs ? Date.parse(opts.sinceTs) : Number.NEGATIVE_INFINITY;
20310
+ const matching = [];
20311
+ for (const entry of this.entries.values()) {
20312
+ if (opts?.status && entry.status !== opts.status) continue;
20313
+ if (Date.parse(entry.created_at) < sinceMs) continue;
20314
+ matching.push(entry);
20315
+ }
20316
+ matching.sort((a, b) => b.created_at.localeCompare(a.created_at));
20317
+ return matching.slice(0, limit);
20318
+ }
20319
+ /**
20320
+ * Return the original (unhashed) request payload for the entry. Returns
20321
+ * `null` when the entry is unknown or the payload was evicted (e.g. the
20322
+ * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
20323
+ */
20324
+ async getFullPayload(aggregatorId) {
20325
+ await this.hydrate();
20326
+ if (!this.entries.has(aggregatorId)) return null;
20327
+ return this.fullPayloads.get(aggregatorId) ?? null;
20328
+ }
20329
+ /**
20330
+ * Resolve an entry. Used by both:
20331
+ * 1. The gate wire-up on channel-decision return.
20332
+ * 2. The HTTP `approve`/`deny` routes when an operator clicks.
20333
+ *
20334
+ * Idempotent: resolving an already-resolved entry is a no-op (the record
20335
+ * keeps its first decision and the audit log is not double-fired).
20336
+ * Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
20337
+ * routes return 404.
20338
+ */
20339
+ async resolve(aggregatorId, decision, operatorId) {
20340
+ await this.hydrate();
20341
+ const entry = this.entries.get(aggregatorId);
20342
+ if (!entry) {
20343
+ throw new Error("approval-aggregator: not_found");
20344
+ }
20345
+ if (entry.status !== "pending") {
20346
+ return entry;
20347
+ }
20348
+ entry.status = decision;
20349
+ entry.resolved_at = this.now().toISOString();
20350
+ entry.resolved_by = operatorId;
20351
+ await this.persist(entry);
20352
+ this.auditLog.append(
20353
+ "l2",
20354
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20355
+ this.identityId,
20356
+ {
20357
+ aggregator_id: entry.aggregator_id,
20358
+ source_harness: entry.source_harness,
20359
+ source_agent_id: entry.source_agent_id,
20360
+ audit_log_entry_id: entry.audit_log_entry_id,
20361
+ policy_rule_id: entry.policy_rule_id,
20362
+ decision,
20363
+ decided_by: operatorId,
20364
+ decided_at: entry.resolved_at
20365
+ }
20366
+ );
20367
+ this.emit({ type: "resolved", entry: { ...entry } });
20368
+ return entry;
20369
+ }
20370
+ // ── Internal: ingest paths ─────────────────────────────────────────────
20371
+ async ingestRequested(event) {
20372
+ const ctx = this.resolveSourceContext(event);
20373
+ const auditId = this.auditEntryIdForEvent(event);
20374
+ const dedupKey = `${ctx.source_harness}|${ctx.source_agent_id}|${auditId}`;
20375
+ const existing = this.dedupIndex.get(dedupKey);
20376
+ if (existing) {
20377
+ const existingEntry = this.entries.get(existing);
20378
+ if (existingEntry) {
20379
+ this.correlationIndex.set(event.correlation_id, existing);
20380
+ this.auditLog.append(
20381
+ "l2",
20382
+ APPROVAL_AGGREGATOR_AUDIT_OPS.DEDUPED,
20383
+ this.identityId,
20384
+ {
20385
+ aggregator_id: existing,
20386
+ source_harness: ctx.source_harness,
20387
+ source_agent_id: ctx.source_agent_id,
20388
+ audit_log_entry_id: auditId,
20389
+ policy_rule_id: this.derivePolicyRuleId(event),
20390
+ correlation_id: event.correlation_id
20391
+ }
20392
+ );
20393
+ this.emit({ type: "deduped", entry: { ...existingEntry } });
20394
+ return null;
20395
+ }
20396
+ }
20397
+ const id = randomUUID();
20398
+ const now = this.now();
20399
+ const expires = new Date(now.getTime() + this.pendingTtlMs);
20400
+ const hubInboxId = this.resolveHubInboxItemId(event);
20401
+ const entry = {
20402
+ aggregator_id: id,
20403
+ source_harness: ctx.source_harness,
20404
+ source_agent_id: ctx.source_agent_id,
20405
+ audit_log_entry_id: auditId,
20406
+ policy_rule_id: this.derivePolicyRuleId(event),
20407
+ action_summary: this.deriveActionSummary(event),
20408
+ request_payload_hash: this.hashPayload(event.context),
20409
+ status: "pending",
20410
+ created_at: now.toISOString(),
20411
+ expires_at: expires.toISOString(),
20412
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
20413
+ };
20414
+ this.entries.set(id, entry);
20415
+ this.dedupIndex.set(dedupKey, id);
20416
+ this.correlationIndex.set(event.correlation_id, id);
20417
+ this.fullPayloads.set(id, event.context);
20418
+ await this.persist(entry);
20419
+ this.auditLog.append(
20420
+ "l2",
20421
+ APPROVAL_AGGREGATOR_AUDIT_OPS.AGGREGATED,
20422
+ this.identityId,
20423
+ {
20424
+ aggregator_id: id,
20425
+ source_harness: ctx.source_harness,
20426
+ source_agent_id: ctx.source_agent_id,
20427
+ audit_log_entry_id: auditId,
20428
+ policy_rule_id: entry.policy_rule_id,
20429
+ ...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {}
20430
+ }
20431
+ );
20432
+ this.emit({ type: "aggregated", entry: { ...entry } });
20433
+ return entry;
20434
+ }
20435
+ async ingestResolved(event) {
20436
+ const id = this.correlationIndex.get(event.correlation_id);
20437
+ if (!id) return null;
20438
+ const entry = this.entries.get(id);
20439
+ if (!entry) return null;
20440
+ if (entry.status !== "pending") return entry;
20441
+ if (!event.resolution) return entry;
20442
+ const failClosed = event.resolution.decision === "deny" && event.resolution.decided_by === "channel_failure";
20443
+ const status = failClosed ? "timeout" : event.resolution.decision === "approve" ? "approved" : "denied";
20444
+ entry.status = status;
20445
+ entry.resolved_at = event.resolution.decided_at;
20446
+ entry.resolved_by = event.resolution.decided_by;
20447
+ await this.persist(entry);
20448
+ this.auditLog.append(
20449
+ "l2",
20450
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20451
+ this.identityId,
20452
+ {
20453
+ aggregator_id: id,
20454
+ source_harness: entry.source_harness,
20455
+ source_agent_id: entry.source_agent_id,
20456
+ audit_log_entry_id: entry.audit_log_entry_id,
20457
+ policy_rule_id: entry.policy_rule_id,
20458
+ decision: status,
20459
+ decided_by: entry.resolved_by,
20460
+ decided_at: entry.resolved_at,
20461
+ fail_closed: failClosed
20462
+ }
20463
+ );
20464
+ this.emit({ type: "resolved", entry: { ...entry } });
20465
+ return entry;
20466
+ }
20467
+ // ── Internal: helpers ──────────────────────────────────────────────────
20468
+ /**
20469
+ * Audit-log entry id for the dedup tuple. The audit log itself does not
20470
+ * surface a stable per-entry id (counter-prefixed keys are internal); the
20471
+ * aggregator uses the request timestamp + operation, which together pin
20472
+ * the audit entry the gate appended on the same call.
20473
+ */
20474
+ auditEntryIdForEvent(event) {
20475
+ return `${event.request_timestamp}:${event.operation}`;
20476
+ }
20477
+ derivePolicyRuleId(event) {
20478
+ return `tier${event.tier}:${event.operation}`;
20479
+ }
20480
+ deriveActionSummary(event) {
20481
+ return `${event.operation} (tier ${event.tier})`;
20482
+ }
20483
+ /**
20484
+ * Canonical SHA-256 of the request context. Sorted-keys serialization so
20485
+ * identical payloads always hash the same, even when key insertion order
20486
+ * varies. Defends against payload-replay smuggling (the aggregator can
20487
+ * tell the same payload was seen twice without storing it cleartext).
20488
+ */
20489
+ hashPayload(payload) {
20490
+ const canonical = JSON.stringify(payload, Object.keys(payload).sort());
20491
+ return createHash("sha256").update(canonical).digest("hex");
20492
+ }
20493
+ emit(event) {
20494
+ for (const listener of this.listeners) {
20495
+ try {
20496
+ listener(event);
20497
+ } catch {
20498
+ }
20499
+ }
20500
+ }
20501
+ async expireStale() {
20502
+ const nowMs = this.now().getTime();
20503
+ for (const entry of this.entries.values()) {
20504
+ if (entry.status !== "pending") continue;
20505
+ if (Date.parse(entry.expires_at) > nowMs) continue;
20506
+ entry.status = "expired";
20507
+ entry.resolved_at = this.now().toISOString();
20508
+ entry.resolved_by = "system_ttl";
20509
+ await this.persist(entry);
20510
+ this.auditLog.append(
20511
+ "l2",
20512
+ APPROVAL_AGGREGATOR_AUDIT_OPS.RESOLVED,
20513
+ this.identityId,
20514
+ {
20515
+ aggregator_id: entry.aggregator_id,
20516
+ source_harness: entry.source_harness,
20517
+ source_agent_id: entry.source_agent_id,
20518
+ audit_log_entry_id: entry.audit_log_entry_id,
20519
+ policy_rule_id: entry.policy_rule_id,
20520
+ decision: "expired",
20521
+ decided_by: "system_ttl",
20522
+ decided_at: entry.resolved_at
20523
+ }
20524
+ );
20525
+ this.emit({ type: "resolved", entry: { ...entry } });
20526
+ }
20527
+ }
20528
+ async persist(entry) {
20529
+ const serialized = stringToBytes(JSON.stringify(entry));
20530
+ const encrypted = encrypt(serialized, this.encryptionKey);
20531
+ await this.storage.write(
20532
+ APPROVAL_AGGREGATOR_NAMESPACE,
20533
+ entry.aggregator_id,
20534
+ stringToBytes(JSON.stringify(encrypted))
20535
+ );
20536
+ }
20537
+ async hydrate() {
20538
+ if (this.hydrated) return;
20539
+ this.hydrated = true;
20540
+ try {
20541
+ const metas = await this.storage.list(APPROVAL_AGGREGATOR_NAMESPACE);
20542
+ for (const meta of metas) {
20543
+ const raw = await this.storage.read(
20544
+ APPROVAL_AGGREGATOR_NAMESPACE,
20545
+ meta.key
20546
+ );
20547
+ if (!raw) continue;
20548
+ try {
20549
+ const encrypted = JSON.parse(bytesToString(raw));
20550
+ const decrypted = decrypt(encrypted, this.encryptionKey);
20551
+ const entry = JSON.parse(bytesToString(decrypted));
20552
+ this.entries.set(entry.aggregator_id, entry);
20553
+ const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
20554
+ this.dedupIndex.set(dedupKey, entry.aggregator_id);
20555
+ } catch {
20556
+ }
20557
+ }
20558
+ } catch {
20559
+ this.hydrated = false;
20560
+ }
20561
+ }
20562
+ };
20563
+ }
20564
+ });
19722
20565
 
19723
20566
  // src/principal-policy/tools.ts
19724
20567
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
@@ -20277,7 +21120,11 @@ var init_tools5 = __esm({
20277
21120
 
20278
21121
  // src/handshake/protocol.ts
20279
21122
  function generateNonce() {
20280
- return toBase64url(randomBytes(32));
21123
+ const nonce = randomBytes(32);
21124
+ if (!nonce || nonce.length !== 32) {
21125
+ throw new Error("Nonce generation failed: randomBytes returned unexpected length");
21126
+ }
21127
+ return toBase64url(nonce);
20281
21128
  }
20282
21129
  function initiateHandshake(ourSHR) {
20283
21130
  const nonce = generateNonce();
@@ -20388,6 +21235,18 @@ function completeHandshake(response, session, identityManager, masterKey, identi
20388
21235
  return { completion, result };
20389
21236
  }
20390
21237
  function verifyCompletion(completion, session) {
21238
+ if (completion.protocol_version !== "1.0") {
21239
+ return {
21240
+ counterparty_id: "unknown",
21241
+ counterparty_shr: session.our_shr,
21242
+ verified: false,
21243
+ sovereignty_level: "unverified",
21244
+ trust_tier: "unverified",
21245
+ completed_at: completion.completed_at,
21246
+ expires_at: (/* @__PURE__ */ new Date()).toISOString(),
21247
+ errors: [`Unsupported protocol version: ${completion.protocol_version}`]
21248
+ };
21249
+ }
20391
21250
  const errors = [];
20392
21251
  if (!session.their_shr) {
20393
21252
  return {
@@ -22620,6 +23479,12 @@ function typed(markerPath, lineNumber, field, expected) {
22620
23479
  }
22621
23480
  async function consumeResetHistoryMarker(options) {
22622
23481
  const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
23482
+ const consumedPath = markerPath + ".consumed";
23483
+ if (await fileExists3(consumedPath)) {
23484
+ await rm(markerPath, { force: true });
23485
+ await rm(consumedPath, { force: true });
23486
+ return { emitted: 0, markerPath };
23487
+ }
22623
23488
  if (!await fileExists3(markerPath)) {
22624
23489
  return { emitted: 0, markerPath };
22625
23490
  }
@@ -22644,7 +23509,9 @@ async function consumeResetHistoryMarker(options) {
22644
23509
  });
22645
23510
  }
22646
23511
  await options.auditLog.flush();
23512
+ await writeFile(consumedPath, "", "utf-8");
22647
23513
  await rm(markerPath, { force: true });
23514
+ await rm(consumedPath, { force: true });
22648
23515
  return { emitted: markers.length, markerHash, markerPath };
22649
23516
  }
22650
23517
  async function fileExists3(path) {
@@ -22674,6 +23541,9 @@ Inspect the file and either correct the JSON or delete it manually before re-run
22674
23541
  this.cause = cause;
22675
23542
  this.name = "ResetHistoryMalformedError";
22676
23543
  }
23544
+ markerPath;
23545
+ lineNumber;
23546
+ cause;
22677
23547
  };
22678
23548
  }
22679
23549
  });
@@ -24832,7 +25702,7 @@ async function runOpenAIPrivacyFilter(text, config) {
24832
25702
  return parsed;
24833
25703
  }
24834
25704
  function runCommand(command, input, timeoutMs) {
24835
- return new Promise((resolve6, reject) => {
25705
+ return new Promise((resolve8, reject) => {
24836
25706
  const child = spawn(command, [], {
24837
25707
  stdio: ["pipe", "pipe", "pipe"],
24838
25708
  shell: false
@@ -24863,7 +25733,7 @@ function runCommand(command, input, timeoutMs) {
24863
25733
  ));
24864
25734
  return;
24865
25735
  }
24866
- resolve6(stdout);
25736
+ resolve8(stdout);
24867
25737
  });
24868
25738
  child.stdin.end(input);
24869
25739
  });
@@ -26739,13 +27609,13 @@ var init_proxy_router = __esm({
26739
27609
  * Call an upstream tool with a timeout.
26740
27610
  */
26741
27611
  async callWithTimeout(serverName, toolName, args, timeoutMs) {
26742
- return new Promise((resolve6, reject) => {
27612
+ return new Promise((resolve8, reject) => {
26743
27613
  const timer = setTimeout(() => {
26744
27614
  reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
26745
27615
  }, timeoutMs);
26746
27616
  this.clientManager.callTool(serverName, toolName, args).then((result) => {
26747
27617
  clearTimeout(timer);
26748
- resolve6(result);
27618
+ resolve8(result);
26749
27619
  }).catch((err) => {
26750
27620
  clearTimeout(timer);
26751
27621
  reject(err);
@@ -31924,6 +32794,36 @@ var init_hub_service = __esm({
31924
32794
  const chat = this.requireOperatorChat();
31925
32795
  return chat.getConciergeHistory();
31926
32796
  }
32797
+ // ── Concierge memory threads (WP-V1.3-9 Tau-1) ─────────────────────
32798
+ /**
32799
+ * Whether the operator-chat service has the WP-V1.3-9 memory store
32800
+ * wired. Routes use this to 503 cleanly when the foundation memory
32801
+ * surface is unavailable on a given fortress.
32802
+ */
32803
+ hasConciergeMemory() {
32804
+ return Boolean(this.deps.operatorChat?.hasConciergeMemory());
32805
+ }
32806
+ async listConciergeMemoryThreads(opts) {
32807
+ const chat = this.requireOperatorChat();
32808
+ if (!chat.hasConciergeMemory()) {
32809
+ throw new HubCapabilityError("concierge_memory_not_wired");
32810
+ }
32811
+ return chat.listConciergeMemoryThreads(opts);
32812
+ }
32813
+ async readConciergeMemoryThread(threadId, opts) {
32814
+ const chat = this.requireOperatorChat();
32815
+ if (!chat.hasConciergeMemory()) {
32816
+ throw new HubCapabilityError("concierge_memory_not_wired");
32817
+ }
32818
+ return chat.readConciergeMemoryThread(threadId, opts);
32819
+ }
32820
+ async deleteConciergeMemoryThread(threadId) {
32821
+ const chat = this.requireOperatorChat();
32822
+ if (!chat.hasConciergeMemory()) {
32823
+ throw new HubCapabilityError("concierge_memory_not_wired");
32824
+ }
32825
+ return chat.deleteConciergeMemoryThread(threadId);
32826
+ }
31927
32827
  /**
31928
32828
  * Open the click-to-inspect/approve panel for a wrapped agent. The
31929
32829
  * panel surfaces recent activity routed through this agent, pending
@@ -32062,7 +32962,20 @@ var init_operator_chat_audit_events = __esm({
32062
32962
  * affordance now opens an inspect/approve panel (recent activity +
32063
32963
  * pending approvals + policy summary) instead of a chat session.
32064
32964
  */
32065
- AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened"
32965
+ AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
32966
+ /**
32967
+ * Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
32968
+ * when the operator hits the list-threads or read-thread route. Body
32969
+ * carries the thread_id (or `*` for the list endpoint) and a count;
32970
+ * raw turn content never crosses the audit surface.
32971
+ */
32972
+ CONCIERGE_HISTORY_READ: "operator_concierge_history_read",
32973
+ /**
32974
+ * Operator deleted a concierge thread (WP-V1.3-9 Tau-1). Emitted on
32975
+ * successful thread removal. Body carries thread_id + turn_count of
32976
+ * the deleted bundle.
32977
+ */
32978
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
32066
32979
  };
32067
32980
  }
32068
32981
  });
@@ -32081,7 +32994,7 @@ function makeEventId(prefix) {
32081
32994
  function hashOf(input) {
32082
32995
  return hashToString(sha256(stringToBytes(input)));
32083
32996
  }
32084
- var DEFAULT_CONCIERGE_MAX_TOKENS, OperatorChatService;
32997
+ var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
32085
32998
  var init_operator_chat_service = __esm({
32086
32999
  "src/chat/operator-chat-service.ts"() {
32087
33000
  init_hashing();
@@ -32089,6 +33002,33 @@ var init_operator_chat_service = __esm({
32089
33002
  init_operator_chat_audit_events();
32090
33003
  init_operator_chat_types();
32091
33004
  DEFAULT_CONCIERGE_MAX_TOKENS = 512;
33005
+ SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
33006
+ 1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
33007
+ 2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
33008
+ 3. Charter (Cooperative MCP): the sovereignty surface for compliant agents. Policy gates, approval tiers, audit logging, and encrypted state all live here.
33009
+ 4. Heralds: Concordia receipts and Verascore reputation. Cross-fortress accountability after an action completes.
33010
+
33011
+ Five channel templates (canonical names):
33012
+ - request-approve-act: agent proposes an action, operator approves or denies before execution.
33013
+ - read-then-report: agent reads outputs from a data source and reports summaries to the operator.
33014
+ - scheduled-digest: agent runs on a schedule and delivers a periodic digest.
33015
+ - plan-draft-only: agent drafts plans; operator reviews before any execution step.
33016
+ - fortress-relay: agent relays messages between fortresses under operator-scoped policy.
33017
+
33018
+ Four canonical policy slots:
33019
+ - memory: governs what the agent may persist and retrieve from encrypted state.
33020
+ - credentials: governs access to secrets, API keys, and tokens held in the broker.
33021
+ - plans: governs the agent's ability to create, modify, or execute plans.
33022
+ - outputs: governs what the agent may emit to external surfaces (files, APIs, messages).
33023
+
33024
+ Key concepts:
33025
+ - Fortress: the operator-owned sovereignty harness. All state is encrypted at rest under the cocoon.
33026
+ - Cocoon: master-key-wrapped storage derived from the operator's passphrase via Argon2id.
33027
+ - Identity: Ed25519 keypair with a DID, owned by the operator. Private keys never leave the cocoon.
33028
+ - Audit log: append-only encrypted blobs, sequential, recording every gate decision and tool call.
33029
+ - Wrapped agent: any agent runtime that connects to Sanctuary as an MCP client. Tier A (native), Tier B (adapter-wrapped), Tier C (escape hatch).
33030
+
33031
+ Note: this is a static reference block (v1.2.x). Dynamic context injection (live template list, policy schema) ships in v1.3.`;
32092
33032
  OperatorChatService = class {
32093
33033
  store;
32094
33034
  auditLog;
@@ -32097,6 +33037,14 @@ var init_operator_chat_service = __esm({
32097
33037
  contextProviders;
32098
33038
  piiFilter;
32099
33039
  conciergeMaxTokens;
33040
+ memory;
33041
+ /**
33042
+ * In-memory thread_id assigned to the active concierge session.
33043
+ * The first sendConcierge call after construction allocates a fresh
33044
+ * UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
33045
+ * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
33046
+ */
33047
+ activeMemoryThreadId;
32100
33048
  constructor(deps) {
32101
33049
  this.store = deps.store;
32102
33050
  this.auditLog = deps.auditLog;
@@ -32107,6 +33055,7 @@ var init_operator_chat_service = __esm({
32107
33055
  }
32108
33056
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
32109
33057
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
33058
+ if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
32110
33059
  }
32111
33060
  // ── Concierge ─────────────────────────────────────────────────────────
32112
33061
  /**
@@ -32137,6 +33086,11 @@ var init_operator_chat_service = __esm({
32137
33086
  CONCIERGE_THREAD_KEY,
32138
33087
  operatorMessage
32139
33088
  );
33089
+ if (this.memory) {
33090
+ const threadId = this.ensureActiveMemoryThread();
33091
+ await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
33092
+ });
33093
+ }
32140
33094
  const start = Date.now();
32141
33095
  let conciergeBody;
32142
33096
  let servedBy = "disabled";
@@ -32192,6 +33146,11 @@ var init_operator_chat_service = __esm({
32192
33146
  CONCIERGE_THREAD_KEY,
32193
33147
  responseMessage
32194
33148
  );
33149
+ if (this.memory) {
33150
+ const threadId = this.ensureActiveMemoryThread();
33151
+ await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
33152
+ });
33153
+ }
32195
33154
  const payload = {
32196
33155
  version: "1.2",
32197
33156
  event_id: makeEventId("conc"),
@@ -32224,6 +33183,105 @@ var init_operator_chat_service = __esm({
32224
33183
  );
32225
33184
  return thread ? thread.messages : [];
32226
33185
  }
33186
+ // ── WP-V1.3-9 Tau-1 memory accessors ─────────────────────────────────
33187
+ /**
33188
+ * Whether the foundation memory store is wired. Routes use this to
33189
+ * 503 cleanly when called against an unwired service.
33190
+ */
33191
+ hasConciergeMemory() {
33192
+ return this.memory !== void 0;
33193
+ }
33194
+ /**
33195
+ * List concierge memory threads, newest-first. Emits the
33196
+ * `operator_concierge_history_read` audit event with `thread_id="*"`.
33197
+ */
33198
+ async listConciergeMemoryThreads(opts) {
33199
+ if (!this.memory) {
33200
+ throw new Error("concierge memory store not configured");
33201
+ }
33202
+ const summaries = await this.memory.listThreads(opts);
33203
+ const totalTurns = summaries.reduce((acc, s) => acc + s.turn_count, 0);
33204
+ const payload = {
33205
+ version: "1.2",
33206
+ event_id: makeEventId("conc-hist"),
33207
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33208
+ identity_id: this.identityId,
33209
+ kind: "operator_concierge_history_read",
33210
+ surface: "concierge",
33211
+ thread_id: "*",
33212
+ turn_count: totalTurns
33213
+ };
33214
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
33215
+ return summaries;
33216
+ }
33217
+ /**
33218
+ * Read a concierge memory thread, oldest turn first. Emits the
33219
+ * `operator_concierge_history_read` audit event with the named
33220
+ * thread_id and the count of turns surfaced.
33221
+ */
33222
+ async readConciergeMemoryThread(threadId, opts) {
33223
+ if (!this.memory) {
33224
+ throw new Error("concierge memory store not configured");
33225
+ }
33226
+ const turns = await this.memory.readThread(threadId, opts);
33227
+ const payload = {
33228
+ version: "1.2",
33229
+ event_id: makeEventId("conc-hist"),
33230
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33231
+ identity_id: this.identityId,
33232
+ kind: "operator_concierge_history_read",
33233
+ surface: "concierge",
33234
+ thread_id: threadId,
33235
+ turn_count: turns.length
33236
+ };
33237
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ, payload, "success");
33238
+ return turns;
33239
+ }
33240
+ /**
33241
+ * Delete a concierge memory thread. Emits
33242
+ * `operator_concierge_thread_deleted` only when a bundle was actually
33243
+ * removed; absent threads return false without an audit event.
33244
+ */
33245
+ async deleteConciergeMemoryThread(threadId) {
33246
+ if (!this.memory) {
33247
+ throw new Error("concierge memory store not configured");
33248
+ }
33249
+ const turnsBefore = await this.memory.readThread(threadId);
33250
+ if (turnsBefore.length === 0) {
33251
+ return await this.memory.deleteThread(threadId);
33252
+ }
33253
+ const removed = await this.memory.deleteThread(threadId);
33254
+ if (!removed) return false;
33255
+ if (this.activeMemoryThreadId === threadId) {
33256
+ this.activeMemoryThreadId = void 0;
33257
+ }
33258
+ const payload = {
33259
+ version: "1.2",
33260
+ event_id: makeEventId("conc-del"),
33261
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33262
+ identity_id: this.identityId,
33263
+ kind: "operator_concierge_thread_deleted",
33264
+ surface: "concierge",
33265
+ thread_id: threadId,
33266
+ turn_count: turnsBefore.length
33267
+ };
33268
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED, payload, "success");
33269
+ return true;
33270
+ }
33271
+ /**
33272
+ * Reset the active session memory thread. Subsequent sendConcierge
33273
+ * calls allocate a fresh thread_id. Surfaced for tests + future "new
33274
+ * conversation" affordance; not currently called by the dashboard.
33275
+ */
33276
+ resetConciergeMemoryThread() {
33277
+ this.activeMemoryThreadId = void 0;
33278
+ }
33279
+ ensureActiveMemoryThread() {
33280
+ if (!this.activeMemoryThreadId) {
33281
+ this.activeMemoryThreadId = randomUUID();
33282
+ }
33283
+ return this.activeMemoryThreadId;
33284
+ }
32227
33285
  /**
32228
33286
  * Stitch fortress state into a single context blob the substrate
32229
33287
  * folds into its summarization prompt.
@@ -32233,6 +33291,9 @@ var init_operator_chat_service = __esm({
32233
33291
  * than nested structures. Format:
32234
33292
  *
32235
33293
  * ```
33294
+ * ## Sanctuary reference
33295
+ * <static domain reference block>
33296
+ *
32236
33297
  * ## Recent activity
32237
33298
  * <recentActivity output>
32238
33299
  *
@@ -32244,15 +33305,28 @@ var init_operator_chat_service = __esm({
32244
33305
  * ```
32245
33306
  */
32246
33307
  async assembleConciergeContext() {
33308
+ const ref = `## Sanctuary reference
33309
+ ${SANCTUARY_DOMAIN_REFERENCE}`;
32247
33310
  if (!this.contextProviders) {
32248
- return "## Recent activity\n(no providers wired)\n\n## Wrapped agents\n(no providers wired)\n\n## Open inbox\n(no providers wired)";
33311
+ return `${ref}
33312
+
33313
+ ## Recent activity
33314
+ (no providers wired)
33315
+
33316
+ ## Wrapped agents
33317
+ (no providers wired)
33318
+
33319
+ ## Open inbox
33320
+ (no providers wired)`;
32249
33321
  }
32250
33322
  const [activity, agents, inbox] = await Promise.all([
32251
33323
  this.contextProviders.recentActivity(),
32252
33324
  this.contextProviders.agentInventory(),
32253
33325
  this.contextProviders.openInbox()
32254
33326
  ]);
32255
- return `## Recent activity
33327
+ return `${ref}
33328
+
33329
+ ## Recent activity
32256
33330
  ${activity}
32257
33331
 
32258
33332
  ## Wrapped agents
@@ -32374,11 +33448,250 @@ var init_operator_chat_store = __esm({
32374
33448
  }
32375
33449
  });
32376
33450
 
33451
+ // src/chat/concierge-memory-store.ts
33452
+ function bundleKey(threadId) {
33453
+ return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
33454
+ }
33455
+ function stripKeyPrefix(key) {
33456
+ if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
33457
+ return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
33458
+ }
33459
+ function lastTurnId(bundle) {
33460
+ let max = 0;
33461
+ for (const t of bundle.turns) {
33462
+ if (t.turn_id > max) max = t.turn_id;
33463
+ }
33464
+ return max;
33465
+ }
33466
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO2, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES2, ConciergeMemoryStore;
33467
+ var init_concierge_memory_store = __esm({
33468
+ "src/chat/concierge-memory-store.ts"() {
33469
+ init_encryption();
33470
+ init_key_derivation();
33471
+ init_encoding();
33472
+ CONCIERGE_MEMORY_NAMESPACE = "_chat";
33473
+ CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
33474
+ HKDF_INFO2 = "concierge-memory-store-v1";
33475
+ DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
33476
+ MAX_BUNDLE_BYTES2 = 4 * 1024 * 1024;
33477
+ ConciergeMemoryStore = class {
33478
+ storage;
33479
+ encryptionKey;
33480
+ fortressId;
33481
+ retentionDays;
33482
+ locks;
33483
+ constructor(opts) {
33484
+ this.storage = opts.storage;
33485
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
33486
+ this.fortressId = opts.fortressId;
33487
+ this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
33488
+ this.locks = /* @__PURE__ */ new Map();
33489
+ }
33490
+ /**
33491
+ * Append a turn to the named thread, creating the bundle if no record
33492
+ * exists. Returns the persisted turn (with assigned turn_id +
33493
+ * retention_until). Per-thread serialisation guarantees turn_id
33494
+ * monotonicity even under concurrent callers.
33495
+ */
33496
+ async appendTurn(threadId, role, content) {
33497
+ return this.withLock(threadId, async () => {
33498
+ const bundle = await this.loadBundle(threadId) ?? null;
33499
+ const now = /* @__PURE__ */ new Date();
33500
+ const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
33501
+ const retentionUntil = new Date(now.getTime() + retentionMs);
33502
+ const nextTurnId = bundle ? lastTurnId(bundle) + 1 : 1;
33503
+ const turn = {
33504
+ thread_id: threadId,
33505
+ fortress_id: this.fortressId,
33506
+ turn_id: nextTurnId,
33507
+ role,
33508
+ content,
33509
+ created_at: now.toISOString(),
33510
+ retention_until: retentionUntil.toISOString()
33511
+ };
33512
+ const next = bundle ? { ...bundle, turns: [...bundle.turns, turn] } : {
33513
+ version: 1,
33514
+ thread_id: threadId,
33515
+ fortress_id: this.fortressId,
33516
+ created_at: now.toISOString(),
33517
+ turns: [turn]
33518
+ };
33519
+ await this.saveBundle(next);
33520
+ return turn;
33521
+ });
33522
+ }
33523
+ /**
33524
+ * Read turns from a thread, oldest-first. Returns an empty array if
33525
+ * the thread does not exist or its bundle is corrupt. Does not emit
33526
+ * audit events; the caller (HTTP route handler) owns audit semantics.
33527
+ */
33528
+ async readThread(threadId, opts) {
33529
+ const bundle = await this.loadBundle(threadId);
33530
+ if (!bundle) return [];
33531
+ let turns = bundle.turns;
33532
+ if (opts?.sinceTurnId !== void 0) {
33533
+ const cutoff = opts.sinceTurnId;
33534
+ turns = turns.filter((t) => t.turn_id > cutoff);
33535
+ }
33536
+ if (opts?.limit !== void 0) {
33537
+ turns = turns.slice(0, opts.limit);
33538
+ }
33539
+ return turns;
33540
+ }
33541
+ /**
33542
+ * Enumerate concierge threads in this fortress with summary metadata.
33543
+ * Sorted newest-first by last_turn_at.
33544
+ */
33545
+ async listThreads(opts) {
33546
+ const entries = await this.storage.list(
33547
+ CONCIERGE_MEMORY_NAMESPACE,
33548
+ CONCIERGE_MEMORY_KEY_PREFIX
33549
+ );
33550
+ const summaries = [];
33551
+ for (const meta of entries) {
33552
+ const threadId = stripKeyPrefix(meta.key);
33553
+ if (threadId === null) continue;
33554
+ const bundle = await this.loadBundle(threadId);
33555
+ if (!bundle || bundle.turns.length === 0) continue;
33556
+ const last = bundle.turns[bundle.turns.length - 1];
33557
+ summaries.push({
33558
+ thread_id: bundle.thread_id,
33559
+ created_at: bundle.created_at,
33560
+ last_turn_at: last ? last.created_at : bundle.created_at,
33561
+ turn_count: bundle.turns.length
33562
+ });
33563
+ }
33564
+ summaries.sort(
33565
+ (a, b) => a.last_turn_at < b.last_turn_at ? 1 : a.last_turn_at > b.last_turn_at ? -1 : 0
33566
+ );
33567
+ if (opts?.limit !== void 0) {
33568
+ return summaries.slice(0, opts.limit);
33569
+ }
33570
+ return summaries;
33571
+ }
33572
+ /**
33573
+ * Delete a thread's bundle. Returns true if the bundle existed and
33574
+ * was removed; false if no bundle was present. Audit emission is the
33575
+ * caller's responsibility.
33576
+ */
33577
+ async deleteThread(threadId) {
33578
+ const key = bundleKey(threadId);
33579
+ return this.withLock(threadId, async () => {
33580
+ const existed = await this.storage.exists(
33581
+ CONCIERGE_MEMORY_NAMESPACE,
33582
+ key
33583
+ );
33584
+ if (!existed) return false;
33585
+ try {
33586
+ await this.storage.delete(CONCIERGE_MEMORY_NAMESPACE, key);
33587
+ } catch {
33588
+ return false;
33589
+ }
33590
+ return true;
33591
+ });
33592
+ }
33593
+ /**
33594
+ * Drop expired turns across all threads. Threads emptied by pruning
33595
+ * are removed entirely. Returns the count of turns pruned.
33596
+ */
33597
+ async pruneExpired(now) {
33598
+ const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
33599
+ const entries = await this.storage.list(
33600
+ CONCIERGE_MEMORY_NAMESPACE,
33601
+ CONCIERGE_MEMORY_KEY_PREFIX
33602
+ );
33603
+ let pruned = 0;
33604
+ for (const meta of entries) {
33605
+ const threadId = stripKeyPrefix(meta.key);
33606
+ if (threadId === null) continue;
33607
+ pruned += await this.withLock(threadId, async () => {
33608
+ const bundle = await this.loadBundle(threadId);
33609
+ if (!bundle) return 0;
33610
+ const kept = bundle.turns.filter((t) => t.retention_until > cutoff);
33611
+ const dropped = bundle.turns.length - kept.length;
33612
+ if (dropped === 0) return 0;
33613
+ if (kept.length === 0) {
33614
+ await this.storage.delete(
33615
+ CONCIERGE_MEMORY_NAMESPACE,
33616
+ bundleKey(threadId)
33617
+ );
33618
+ } else {
33619
+ await this.saveBundle({ ...bundle, turns: kept });
33620
+ }
33621
+ return dropped;
33622
+ });
33623
+ }
33624
+ return { pruned };
33625
+ }
33626
+ // ── internals ────────────────────────────────────────────────────────
33627
+ async loadBundle(threadId) {
33628
+ const key = bundleKey(threadId);
33629
+ let raw;
33630
+ try {
33631
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
33632
+ } catch {
33633
+ return null;
33634
+ }
33635
+ if (!raw) return null;
33636
+ if (raw.length > MAX_BUNDLE_BYTES2) return null;
33637
+ try {
33638
+ const envelope = JSON.parse(bytesToString(raw));
33639
+ const aad = stringToBytes(threadId);
33640
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
33641
+ const parsed = JSON.parse(
33642
+ bytesToString(plaintext)
33643
+ );
33644
+ if (parsed.version !== 1) return null;
33645
+ if (parsed.thread_id !== threadId) return null;
33646
+ return parsed;
33647
+ } catch {
33648
+ return null;
33649
+ }
33650
+ }
33651
+ async saveBundle(bundle) {
33652
+ const key = bundleKey(bundle.thread_id);
33653
+ const aad = stringToBytes(bundle.thread_id);
33654
+ const plaintext = stringToBytes(JSON.stringify(bundle));
33655
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
33656
+ await this.storage.write(
33657
+ CONCIERGE_MEMORY_NAMESPACE,
33658
+ key,
33659
+ stringToBytes(JSON.stringify(envelope))
33660
+ );
33661
+ }
33662
+ /**
33663
+ * Run `task` while holding the per-thread async lock. Lock is released
33664
+ * once the task settles (success or failure). Generic helper so
33665
+ * appendTurn / deleteThread / pruneExpired share serialisation.
33666
+ */
33667
+ async withLock(threadId, task) {
33668
+ const previous = this.locks.get(threadId) ?? Promise.resolve();
33669
+ let release;
33670
+ const next = new Promise((resolve8) => {
33671
+ release = resolve8;
33672
+ });
33673
+ const chained = previous.then(() => next);
33674
+ this.locks.set(threadId, chained);
33675
+ try {
33676
+ await previous;
33677
+ return await task();
33678
+ } finally {
33679
+ release();
33680
+ if (this.locks.get(threadId) === chained) {
33681
+ this.locks.delete(threadId);
33682
+ }
33683
+ }
33684
+ }
33685
+ };
33686
+ }
33687
+ });
33688
+
32377
33689
  // src/chat/operator-chat-index.ts
32378
33690
  var init_operator_chat_index = __esm({
32379
33691
  "src/chat/operator-chat-index.ts"() {
32380
33692
  init_operator_chat_service();
32381
33693
  init_operator_chat_store();
33694
+ init_concierge_memory_store();
32382
33695
  init_operator_chat_audit_events();
32383
33696
  init_operator_chat_types();
32384
33697
  }
@@ -32392,6 +33705,14 @@ function buildV11Bindings(inputs) {
32392
33705
  let operatorChatService;
32393
33706
  if (inputs.storage && inputs.masterKey) {
32394
33707
  const chatStore = new OperatorChatStore(inputs.storage, inputs.masterKey);
33708
+ const conciergeMemory = new ConciergeMemoryStore({
33709
+ storage: inputs.storage,
33710
+ masterKey: inputs.masterKey,
33711
+ fortressId: inputs.fortressId,
33712
+ ...inputs.conciergeMemoryRetentionDays !== void 0 ? { retentionDays: inputs.conciergeMemoryRetentionDays } : {}
33713
+ });
33714
+ void conciergeMemory.pruneExpired().catch(() => {
33715
+ });
32395
33716
  operatorChatService = new OperatorChatService({
32396
33717
  store: chatStore,
32397
33718
  auditLog: inputs.auditLog,
@@ -32402,7 +33723,8 @@ function buildV11Bindings(inputs) {
32402
33723
  identityId: inputs.identityId,
32403
33724
  registry
32404
33725
  }),
32405
- conciergePiiFilter: buildConciergePiiFilter()
33726
+ conciergePiiFilter: buildConciergePiiFilter(),
33727
+ conciergeMemory
32406
33728
  });
32407
33729
  }
32408
33730
  const hubService = new HubService({
@@ -32637,7 +33959,7 @@ var init_defaults = __esm({
32637
33959
  });
32638
33960
 
32639
33961
  // src/intelligence/policy-store.ts
32640
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO2, IntelligenceConfigStore;
33962
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO3, IntelligenceConfigStore;
32641
33963
  var init_policy_store = __esm({
32642
33964
  "src/intelligence/policy-store.ts"() {
32643
33965
  init_encryption();
@@ -32646,13 +33968,13 @@ var init_policy_store = __esm({
32646
33968
  init_defaults();
32647
33969
  INTELLIGENCE_NAMESPACE = "_intelligence";
32648
33970
  SUBSTRATE_CONFIG_KEY = "substrate-config";
32649
- HKDF_INFO2 = "intelligence-substrate-config";
33971
+ HKDF_INFO3 = "intelligence-substrate-config";
32650
33972
  IntelligenceConfigStore = class {
32651
33973
  storage;
32652
33974
  encryptionKey;
32653
33975
  constructor(storage, masterKey) {
32654
33976
  this.storage = storage;
32655
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO2);
33977
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
32656
33978
  }
32657
33979
  /**
32658
33980
  * Load the operator's substrate config from disk. Returns the config
@@ -34674,7 +35996,9 @@ async function verifyExitBundle(bundleDir, options = {}) {
34674
35996
  );
34675
35997
  }
34676
35998
  }
34677
- const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
35999
+ const reputationBundleFailed = reputation?.bundle_signature_valid === false;
36000
+ const reputationAttestationFailed = (reputation?.invalid_attestations ?? 0) > 0;
36001
+ const reputationFailed = reputationBundleFailed || reputationAttestationFailed;
34678
36002
  const identityFailed = identity ? !identity.signature_valid : false;
34679
36003
  const unverifiableCount = reputation?.unverifiable_attestations ?? 0;
34680
36004
  const unverifiableFailed = unverifiableCount > 0 && !options.acceptUnverifiableAttestations;
@@ -34683,6 +36007,16 @@ async function verifyExitBundle(bundleDir, options = {}) {
34683
36007
  `${unverifiableCount} reputation attestation(s) have unknown signer public keys; pass --accept-unverifiable-attestations to import anyway`
34684
36008
  );
34685
36009
  }
36010
+ let detailedFailureClass;
36011
+ if (identityFailed) {
36012
+ detailedFailureClass = "identity_signature_invalid";
36013
+ } else if (reputationBundleFailed) {
36014
+ detailedFailureClass = "reputation_bundle_signature_invalid";
36015
+ } else if (reputationAttestationFailed) {
36016
+ detailedFailureClass = "reputation_attestation_signature_invalid";
36017
+ } else if (unverifiableFailed) {
36018
+ detailedFailureClass = "reputation_unverifiable_attestations";
36019
+ }
34686
36020
  return {
34687
36021
  version: "1.1",
34688
36022
  passed: !reputationFailed && !identityFailed && !unverifiableFailed,
@@ -34702,7 +36036,7 @@ async function verifyExitBundle(bundleDir, options = {}) {
34702
36036
  identity,
34703
36037
  audit,
34704
36038
  reputation,
34705
- failure_class: reputationFailed || identityFailed || unverifiableFailed ? "other" : void 0
36039
+ failure_class: detailedFailureClass
34706
36040
  };
34707
36041
  }
34708
36042
  var InvalidExitBundleError, PRIVATE_MATERIAL_KEYS;
@@ -35284,6 +36618,9 @@ async function importExitBundle(opts) {
35284
36618
  reputationArtifact?.json ?? null,
35285
36619
  manifest
35286
36620
  );
36621
+ if (!conflicts.public_identity_exists && identityArtifact?.json && opts.identityManager.getPrimaryIdentityId() !== null && opts.identityManager.getPrimaryIdentityId() !== identityArtifact.json.bundle.identity_id) {
36622
+ conflicts.public_identity_exists = true;
36623
+ }
35287
36624
  if (!opts.activate) {
35288
36625
  return {
35289
36626
  verified: true,
@@ -35310,7 +36647,7 @@ async function importExitBundle(opts) {
35310
36647
  if (conflicts.public_identity_exists && !opts.forceRebind) {
35311
36648
  throw new ExitBundleImportError(
35312
36649
  "IDENTITY_OVERWRITE_REFUSED",
35313
- "Importing this bundle would overwrite an existing fortress public identity. Pass forceRebind: true (CLI: --force-rebind) to confirm explicit replacement."
36650
+ "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."
35314
36651
  );
35315
36652
  }
35316
36653
  if (conflicts.public_identity_exists && opts.forceRebind && identityArtifact) {
@@ -35667,7 +37004,19 @@ async function runExitCommand(args) {
35667
37004
  }
35668
37005
  const config = await loadConfig();
35669
37006
  const ctx = await openExitContext(argv, env);
35670
- const policy = await loadPrincipalPolicy(ctx.storagePath);
37007
+ let policy;
37008
+ try {
37009
+ policy = await loadPrincipalPolicy(ctx.storagePath);
37010
+ } catch (policyErr) {
37011
+ if (policyErr instanceof MalformedPrincipalPolicyError) {
37012
+ write(err, `
37013
+ Sanctuary cannot proceed.
37014
+ ${policyErr.message}
37015
+ `);
37016
+ return 1;
37017
+ }
37018
+ throw policyErr;
37019
+ }
35671
37020
  const result = await exportExitBundle({
35672
37021
  bundleDir: outDir,
35673
37022
  storage: ctx.storage,
@@ -35700,6 +37049,26 @@ async function runExitCommand(args) {
35700
37049
  write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
35701
37050
  return 2;
35702
37051
  }
37052
+ const bundleRoot = resolve(dir);
37053
+ try {
37054
+ await access(bundleRoot);
37055
+ } catch {
37056
+ write(err, `Error: bundle directory not found: ${bundleRoot}
37057
+ `);
37058
+ return 1;
37059
+ }
37060
+ const manifestPath = join(bundleRoot, "manifest.json");
37061
+ try {
37062
+ const raw = await readFile(manifestPath, "utf8");
37063
+ JSON.parse(raw);
37064
+ } catch {
37065
+ write(
37066
+ err,
37067
+ `Error: bundle manifest missing or malformed at ${manifestPath}
37068
+ `
37069
+ );
37070
+ return 1;
37071
+ }
35703
37072
  const activate = hasFlag(argv, "--activate");
35704
37073
  const forceRebind = hasFlag(argv, "--force-rebind");
35705
37074
  const acceptUnverifiableAttestations = hasFlag(
@@ -35873,11 +37242,11 @@ async function startDashboardServer(options) {
35873
37242
  }
35874
37243
  }
35875
37244
  });
35876
- await new Promise((resolve6, reject) => {
37245
+ await new Promise((resolve8, reject) => {
35877
37246
  server.once("error", reject);
35878
37247
  server.listen(port, host, () => {
35879
37248
  server.off("error", reject);
35880
- resolve6();
37249
+ resolve8();
35881
37250
  });
35882
37251
  });
35883
37252
  const actualPort = (() => {
@@ -35890,8 +37259,8 @@ async function startDashboardServer(options) {
35890
37259
  url,
35891
37260
  port: actualPort,
35892
37261
  host,
35893
- stop: () => new Promise((resolve6, reject) => {
35894
- server.close((err) => err ? reject(err) : resolve6());
37262
+ stop: () => new Promise((resolve8, reject) => {
37263
+ server.close((err) => err ? reject(err) : resolve8());
35895
37264
  }),
35896
37265
  publish,
35897
37266
  publishActivity: (entry) => publish({ type: "activity", data: entry }),
@@ -36347,7 +37716,19 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36347
37716
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
36348
37717
  await profileStore.load();
36349
37718
  const { tools: profileTools } = createSovereigntyProfileTools(profileStore, auditLog);
36350
- const policy = await loadPrincipalPolicy(config.storage_path);
37719
+ let policy;
37720
+ try {
37721
+ policy = await loadPrincipalPolicy(config.storage_path);
37722
+ } catch (err) {
37723
+ if (err instanceof MalformedPrincipalPolicyError) {
37724
+ console.error(`
37725
+ Sanctuary cannot start.
37726
+ ${err.message}
37727
+ `);
37728
+ process.exit(1);
37729
+ }
37730
+ throw err;
37731
+ }
36351
37732
  const baseline = new BaselineTracker(storage, masterKey);
36352
37733
  await baseline.load();
36353
37734
  let approvalChannel;
@@ -36445,6 +37826,21 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36445
37826
  });
36446
37827
  } : void 0;
36447
37828
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37829
+ const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37830
+ const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
37831
+ const approvalAggregator = new ApprovalAggregator({
37832
+ storage,
37833
+ masterKey,
37834
+ auditLog,
37835
+ identityId: aggregatorIdentityId,
37836
+ fortressId: fortressIdForAggregator
37837
+ });
37838
+ gate.setApprovalEventCallback((event) => {
37839
+ void approvalAggregator.ingest(event);
37840
+ });
37841
+ if (dashboard) {
37842
+ dashboard.setApprovalAggregator(approvalAggregator);
37843
+ }
36448
37844
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
36449
37845
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
36450
37846
  config,
@@ -36564,7 +37960,7 @@ Refusing to start the cocoon while the reset-history marker is unreadable.`
36564
37960
  clientManager.configure(enabledServers).catch((err) => {
36565
37961
  console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
36566
37962
  });
36567
- await new Promise((resolve6) => setTimeout(resolve6, 2e3));
37963
+ await new Promise((resolve8) => setTimeout(resolve8, 2e3));
36568
37964
  const proxiedTools = proxyRouter.getProxiedTools();
36569
37965
  if (proxiedTools.length > 0) {
36570
37966
  allTools.push(...proxiedTools);
@@ -36635,6 +38031,7 @@ var init_src = __esm({
36635
38031
  init_dashboard();
36636
38032
  init_webhook();
36637
38033
  init_gate();
38034
+ init_approval_aggregator();
36638
38035
  init_tools4();
36639
38036
  init_router();
36640
38037
  init_router();
@@ -37403,8 +38800,8 @@ async function runWrap(options, deps = {}) {
37403
38800
  passphraseValue = process.env.SANCTUARY_PASSPHRASE;
37404
38801
  } else {
37405
38802
  try {
37406
- const resolve6 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
37407
- const resolved = await resolve6();
38803
+ const resolve8 = deps.resolvePassphrase ?? (() => getOrCreatePassphrase({ storagePath }));
38804
+ const resolved = await resolve8();
37408
38805
  passphraseLocation = resolved.location;
37409
38806
  passphraseSource = resolved.source;
37410
38807
  passphraseValue = resolved.value;
@@ -37759,12 +39156,12 @@ async function defaultOpenBrowser(url) {
37759
39156
  cmd = "xdg-open";
37760
39157
  args = [url];
37761
39158
  }
37762
- await new Promise((resolve6) => {
39159
+ await new Promise((resolve8) => {
37763
39160
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
37764
- child.on("error", () => resolve6());
39161
+ child.on("error", () => resolve8());
37765
39162
  child.on("spawn", () => {
37766
39163
  child.unref();
37767
- resolve6();
39164
+ resolve8();
37768
39165
  });
37769
39166
  });
37770
39167
  }
@@ -38733,32 +40130,32 @@ endstream`;
38733
40130
  const offsets = new Array(totalObjects + 1).fill(0);
38734
40131
  const chunks = [];
38735
40132
  let bytePos = 0;
38736
- const write2 = (s) => {
40133
+ const write3 = (s) => {
38737
40134
  const buf = Buffer.from(s, "latin1");
38738
40135
  chunks.push(buf);
38739
40136
  bytePos += buf.length;
38740
40137
  };
38741
- write2("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
40138
+ write3("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
38742
40139
  for (let i = 1; i <= totalObjects; i++) {
38743
40140
  offsets[i] = bytePos;
38744
- write2(`${i} 0 obj
40141
+ write3(`${i} 0 obj
38745
40142
  ${objectBodies[i]}
38746
40143
  endobj
38747
40144
  `);
38748
40145
  }
38749
40146
  const xrefPos = bytePos;
38750
- write2(`xref
40147
+ write3(`xref
38751
40148
  0 ${totalObjects + 1}
38752
40149
  `);
38753
- write2("0000000000 65535 f \n");
40150
+ write3("0000000000 65535 f \n");
38754
40151
  for (let i = 1; i <= totalObjects; i++) {
38755
- write2(`${offsets[i].toString().padStart(10, "0")} 00000 n
40152
+ write3(`${offsets[i].toString().padStart(10, "0")} 00000 n
38756
40153
  `);
38757
40154
  }
38758
- write2(`trailer
40155
+ write3(`trailer
38759
40156
  << /Size ${totalObjects + 1} /Root 1 0 R >>
38760
40157
  `);
38761
- write2(`startxref
40158
+ write3(`startxref
38762
40159
  ${xrefPos}
38763
40160
  %%EOF
38764
40161
  `);
@@ -39093,7 +40490,7 @@ var init_backend_interface = __esm({
39093
40490
  }
39094
40491
  });
39095
40492
  async function runSecurity(args, input) {
39096
- return new Promise((resolve6, reject) => {
40493
+ return new Promise((resolve8, reject) => {
39097
40494
  const child = spawn(SECURITY_BIN, args, { stdio: ["pipe", "pipe", "pipe"] });
39098
40495
  let stdout = "";
39099
40496
  let stderr = "";
@@ -39115,7 +40512,7 @@ async function runSecurity(args, input) {
39115
40512
  reject(err);
39116
40513
  });
39117
40514
  child.on("close", (code) => {
39118
- resolve6({ stdout, stderr, code: code ?? -1 });
40515
+ resolve8({ stdout, stderr, code: code ?? -1 });
39119
40516
  });
39120
40517
  if (input !== void 0) {
39121
40518
  child.stdin.write(input);
@@ -40191,7 +41588,7 @@ async function readValue(stdin, prompt2) {
40191
41588
  return await readFirstLine(stdin);
40192
41589
  }
40193
41590
  async function readFirstLine(stdin) {
40194
- return new Promise((resolve6, reject) => {
41591
+ return new Promise((resolve8, reject) => {
40195
41592
  const rl = createInterface$1({ input: stdin });
40196
41593
  let resolved = false;
40197
41594
  const finish = (value) => {
@@ -40202,7 +41599,7 @@ async function readFirstLine(stdin) {
40202
41599
  rl.close();
40203
41600
  } catch {
40204
41601
  }
40205
- resolve6(value);
41602
+ resolve8(value);
40206
41603
  };
40207
41604
  const deadline = setTimeout(() => {
40208
41605
  finish("");
@@ -40221,7 +41618,7 @@ async function promptSilently(stdin, prompt2) {
40221
41618
  process.stderr.write(`${prompt2}: `);
40222
41619
  stdin.setRawMode?.(true);
40223
41620
  stdin.resume();
40224
- return await new Promise((resolve6) => {
41621
+ return await new Promise((resolve8) => {
40225
41622
  let buf = "";
40226
41623
  const onData = (chunk) => {
40227
41624
  const s = chunk.toString("utf8");
@@ -40231,7 +41628,7 @@ async function promptSilently(stdin, prompt2) {
40231
41628
  stdin.pause();
40232
41629
  stdin.off("data", onData);
40233
41630
  process.stderr.write("\n");
40234
- resolve6(buf);
41631
+ resolve8(buf);
40235
41632
  return;
40236
41633
  }
40237
41634
  if (ch === "") {
@@ -40498,13 +41895,179 @@ var init_cli4 = __esm({
40498
41895
  init_discovery();
40499
41896
  }
40500
41897
  });
41898
+
41899
+ // src/cli/identity.ts
41900
+ var identity_exports2 = {};
41901
+ __export(identity_exports2, {
41902
+ runIdentityCommand: () => runIdentityCommand
41903
+ });
41904
+ function write2(stream, text) {
41905
+ stream.write(text);
41906
+ }
41907
+ function flagValue2(argv, name) {
41908
+ const index = argv.indexOf(name);
41909
+ if (index === -1) return void 0;
41910
+ return argv[index + 1];
41911
+ }
41912
+ function hasFlag2(argv, name) {
41913
+ return argv.includes(name);
41914
+ }
41915
+ function printUsage3(out) {
41916
+ write2(
41917
+ out,
41918
+ `Usage: sanctuary identity <command> [options]
41919
+
41920
+ Commands:
41921
+ show Print the active identity (DID, identity_id, public key).
41922
+
41923
+ Options:
41924
+ --fortress <path> Override the storage path.
41925
+ --passphrase <val> Passphrase for master-key derivation.
41926
+ --json Output as JSON.
41927
+ --help, -h Show this help.
41928
+
41929
+ Environment variables:
41930
+ SANCTUARY_PASSPHRASE Key derivation passphrase.
41931
+ SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary).
41932
+ SANCTUARY_FORTRESS_PATH Operator-friendly alias for STORAGE_PATH.
41933
+ SANCTUARY_RECOVERY_KEY Recovery key (alternative to passphrase).
41934
+
41935
+ Identity data is encrypted at rest. A passphrase or recovery key is
41936
+ required to decrypt and display identity information.
41937
+ `
41938
+ );
41939
+ }
41940
+ async function runIdentityCommand(args) {
41941
+ const argv = args.argv;
41942
+ const out = args.out ?? process.stdout;
41943
+ const err = args.err ?? process.stderr;
41944
+ const env = args.env ?? process.env;
41945
+ if (argv.length === 0 || hasFlag2(argv, "--help") || hasFlag2(argv, "-h")) {
41946
+ printUsage3(out);
41947
+ return 0;
41948
+ }
41949
+ const command = argv[0];
41950
+ if (command === "show") {
41951
+ return await cmdShow(argv.slice(1), out, err, env);
41952
+ }
41953
+ write2(err, `Unknown identity command: ${command}
41954
+ `);
41955
+ write2(err, `Run "sanctuary identity --help" for usage.
41956
+ `);
41957
+ return 2;
41958
+ }
41959
+ async function cmdShow(argv, out, err, env) {
41960
+ const json = hasFlag2(argv, "--json");
41961
+ const fortressFlag = flagValue2(argv, "--fortress");
41962
+ if (fortressFlag) {
41963
+ process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
41964
+ }
41965
+ const passphrase = flagValue2(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
41966
+ const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
41967
+ if (!passphrase && !recoveryKey) {
41968
+ write2(
41969
+ err,
41970
+ "Error: sanctuary identity show requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY.\n"
41971
+ );
41972
+ return 1;
41973
+ }
41974
+ try {
41975
+ const config = await loadConfig();
41976
+ await mkdir(config.storage_path, { recursive: true, mode: 448 });
41977
+ const stateStoragePath = join(config.storage_path, "state");
41978
+ const storage = new FilesystemStorage(stateStoragePath);
41979
+ let masterKey;
41980
+ if (passphrase) {
41981
+ let existingParams;
41982
+ const raw = await storage.read("_meta", "key-params");
41983
+ if (raw)
41984
+ existingParams = JSON.parse(bytesToString(raw));
41985
+ const derived = await deriveMasterKey(passphrase, existingParams);
41986
+ masterKey = derived.key;
41987
+ } else {
41988
+ masterKey = fromBase64url(recoveryKey);
41989
+ if (masterKey.length !== 32) {
41990
+ write2(err, "Error: SANCTUARY_RECOVERY_KEY must decode to 32 bytes.\n");
41991
+ return 1;
41992
+ }
41993
+ }
41994
+ const identityManager = new IdentityManager(storage, masterKey);
41995
+ const loadResult = await identityManager.load();
41996
+ if (loadResult.loaded === 0) {
41997
+ write2(
41998
+ err,
41999
+ loadResult.total > 0 ? "Error: identity files found but none could be decrypted. Wrong passphrase?\n" : "No identities found in this fortress.\n"
42000
+ );
42001
+ return 1;
42002
+ }
42003
+ const primary = identityManager.getDefault();
42004
+ if (!primary) {
42005
+ write2(err, "No primary identity set.\n");
42006
+ return 1;
42007
+ }
42008
+ if (json) {
42009
+ write2(
42010
+ out,
42011
+ JSON.stringify(
42012
+ {
42013
+ identity_id: primary.identity_id,
42014
+ did: primary.did,
42015
+ public_key: primary.public_key,
42016
+ label: primary.label,
42017
+ key_type: primary.key_type,
42018
+ created_at: primary.created_at,
42019
+ storage_path: config.storage_path,
42020
+ total_identities: loadResult.loaded
42021
+ },
42022
+ null,
42023
+ 2
42024
+ ) + "\n"
42025
+ );
42026
+ } else {
42027
+ write2(out, `identity_id: ${primary.identity_id}
42028
+ `);
42029
+ write2(out, `did: ${primary.did}
42030
+ `);
42031
+ write2(out, `public_key: ${primary.public_key}
42032
+ `);
42033
+ write2(out, `label: ${primary.label}
42034
+ `);
42035
+ write2(out, `key_type: ${primary.key_type}
42036
+ `);
42037
+ write2(out, `created_at: ${primary.created_at}
42038
+ `);
42039
+ write2(out, `storage_path: ${config.storage_path}
42040
+ `);
42041
+ write2(out, `total_identities: ${loadResult.loaded}
42042
+ `);
42043
+ }
42044
+ return 0;
42045
+ } catch (error) {
42046
+ write2(
42047
+ err,
42048
+ error instanceof Error ? `Error: ${error.message}
42049
+ ` : `Error: ${String(error)}
42050
+ `
42051
+ );
42052
+ return 1;
42053
+ }
42054
+ }
42055
+ var init_identity2 = __esm({
42056
+ "src/cli/identity.ts"() {
42057
+ init_filesystem();
42058
+ init_tools();
42059
+ init_key_derivation();
42060
+ init_encoding();
42061
+ init_config();
42062
+ }
42063
+ });
40501
42064
  async function probeTenantDashboard(tenant, options = {}) {
40502
42065
  const rt = tenant.runtime;
40503
42066
  if (!rt) {
40504
42067
  return { running: false, status: null, reason: "no runtime.json" };
40505
42068
  }
40506
42069
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
40507
- return await new Promise((resolve6) => {
42070
+ return await new Promise((resolve8) => {
40508
42071
  const req = get$1(
40509
42072
  {
40510
42073
  host: rt.dashboard_host,
@@ -40516,9 +42079,9 @@ async function probeTenantDashboard(tenant, options = {}) {
40516
42079
  res.resume();
40517
42080
  const status = res.statusCode ?? 0;
40518
42081
  if (status > 0 && status < 500) {
40519
- resolve6({ running: true, status, reason: null });
42082
+ resolve8({ running: true, status, reason: null });
40520
42083
  } else {
40521
- resolve6({
42084
+ resolve8({
40522
42085
  running: false,
40523
42086
  status,
40524
42087
  reason: `dashboard returned ${status}`
@@ -40528,10 +42091,10 @@ async function probeTenantDashboard(tenant, options = {}) {
40528
42091
  );
40529
42092
  req.on("timeout", () => {
40530
42093
  req.destroy();
40531
- resolve6({ running: false, status: null, reason: "timeout" });
42094
+ resolve8({ running: false, status: null, reason: "timeout" });
40532
42095
  });
40533
42096
  req.on("error", (err) => {
40534
- resolve6({
42097
+ resolve8({
40535
42098
  running: false,
40536
42099
  status: null,
40537
42100
  reason: err.code ?? err.message
@@ -40563,10 +42126,17 @@ function resolveCtx(args) {
40563
42126
  };
40564
42127
  }
40565
42128
  async function runAgentsCommand(args) {
42129
+ const fortressIdx = args.argv.indexOf("--fortress");
42130
+ if (fortressIdx !== -1 && args.argv[fortressIdx + 1]) {
42131
+ args = { ...args, root: args.argv[fortressIdx + 1] };
42132
+ const filtered = [...args.argv];
42133
+ filtered.splice(fortressIdx, 2);
42134
+ args = { ...args, argv: filtered };
42135
+ }
40566
42136
  const ctx = resolveCtx(args);
40567
42137
  const [sub, ...rest] = args.argv;
40568
42138
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
40569
- printUsage3(ctx.out);
42139
+ printUsage4(ctx.out);
40570
42140
  return 0;
40571
42141
  }
40572
42142
  try {
@@ -40574,13 +42144,13 @@ async function runAgentsCommand(args) {
40574
42144
  case "list":
40575
42145
  return await cmdList3(rest, ctx);
40576
42146
  case "show":
40577
- return await cmdShow(rest, ctx);
42147
+ return await cmdShow2(rest, ctx);
40578
42148
  case "status":
40579
42149
  return await cmdStatus(rest, ctx);
40580
42150
  default:
40581
42151
  ctx.err.write(`Unknown subcommand: ${sub}
40582
42152
  `);
40583
- printUsage3(ctx.err);
42153
+ printUsage4(ctx.err);
40584
42154
  return 2;
40585
42155
  }
40586
42156
  } catch (e) {
@@ -40590,13 +42160,17 @@ async function runAgentsCommand(args) {
40590
42160
  return 1;
40591
42161
  }
40592
42162
  }
40593
- function printUsage3(s) {
42163
+ function printUsage4(s) {
40594
42164
  s.write(`Usage: sanctuary agents <command> [flags]
40595
42165
 
40596
42166
  list [--json] List every tenant visible on this host.
40597
42167
  show <tenant> [--json] Show details for one tenant.
40598
42168
  status [--json] One-line-per-tenant running/stopped summary.
40599
42169
 
42170
+ Options:
42171
+ --fortress <path> Scope discovery to a specific storage path
42172
+ instead of scanning ~/.sanctuary.
42173
+
40600
42174
  Tenants are discovered by scanning ~/.sanctuary and any storage paths in
40601
42175
  SANCTUARY_AGENTS_EXTRA_PATHS or ~/.sanctuary/agents-extra.json. Tenant
40602
42176
  creation is done via \`sanctuary wrap\` with SANCTUARY_STORAGE_PATH set.
@@ -40680,7 +42254,7 @@ async function cmdList3(argv, ctx) {
40680
42254
  }
40681
42255
  return 0;
40682
42256
  }
40683
- async function cmdShow(argv, ctx) {
42257
+ async function cmdShow2(argv, ctx) {
40684
42258
  const positional = argv.find((a) => !a.startsWith("--"));
40685
42259
  if (!positional) {
40686
42260
  ctx.err.write("Missing tenant. Usage: sanctuary agents show <tenant>\n");
@@ -40821,7 +42395,8 @@ var init_agents = __esm({
40821
42395
  // src/cli/reset-passphrase.ts
40822
42396
  var reset_passphrase_exports = {};
40823
42397
  __export(reset_passphrase_exports, {
40824
- runResetPassphraseCommand: () => runResetPassphraseCommand
42398
+ runResetPassphraseCommand: () => runResetPassphraseCommand,
42399
+ zeroizeBuffers: () => zeroizeBuffers
40825
42400
  });
40826
42401
  async function runResetPassphraseCommand(args) {
40827
42402
  const out = args.out ?? process.stdout;
@@ -40831,10 +42406,10 @@ async function runResetPassphraseCommand(args) {
40831
42406
  const plat = args.platformOverride ?? process.platform;
40832
42407
  const parsed = parseArgs2(args.argv);
40833
42408
  if (parsed.help) {
40834
- printUsage4(out);
42409
+ printUsage5(out);
40835
42410
  return 0;
40836
42411
  }
40837
- const storagePath = parsed.storage ?? args.storagePath ?? resolveStoragePath(process.env, home);
42412
+ const storagePath = parsed.storage ?? parsed.fortress ?? args.storagePath ?? resolveStoragePath(process.env, home);
40838
42413
  out.write(banner(storagePath));
40839
42414
  const runtimeFile = join(storagePath, "runtime.json");
40840
42415
  if (await fileExists4(runtimeFile)) {
@@ -40849,34 +42424,42 @@ Then re-run this command.
40849
42424
  return 1;
40850
42425
  }
40851
42426
  const lines = new LineReader(stdin);
42427
+ let code = 1;
42428
+ let nukeSucceeded = false;
40852
42429
  try {
40853
42430
  const availability = await surveyAvailableModes(storagePath);
40854
42431
  const mode = parsed.mode ?? await selectMode(lines, out, err, availability);
40855
42432
  if (!mode) {
40856
42433
  err.write("Aborted: no recovery mode selected.\n");
40857
- return 1;
40858
- }
40859
- if (mode === "shares") {
40860
- return await runSharesPath(out, err, availability);
40861
- }
40862
- if (mode === "guardian") {
40863
- return await runGuardianPath(out, err, availability);
42434
+ code = 1;
42435
+ } else if (mode === "shares") {
42436
+ code = await runSharesPath(out, err, availability);
42437
+ } else if (mode === "guardian") {
42438
+ code = await runGuardianPath(out, err, availability);
42439
+ } else {
42440
+ code = await runNukePath({
42441
+ out,
42442
+ err,
42443
+ lines,
42444
+ storagePath,
42445
+ home,
42446
+ plat,
42447
+ exec: args.exec ?? defaultExec2
42448
+ });
42449
+ nukeSucceeded = mode === "nuke" && code === 0;
40864
42450
  }
40865
- return await runNukePath({
40866
- out,
40867
- err,
40868
- lines,
40869
- storagePath,
40870
- home,
40871
- plat,
40872
- exec: args.exec ?? defaultExec2
40873
- });
40874
42451
  } finally {
42452
+ zeroizeBuffers(args.keyMaterialToZeroize);
40875
42453
  lines.close();
40876
42454
  }
42455
+ if (parsed.exitOnCompletion && nukeSucceeded) {
42456
+ const doExit = args.exitProcess ?? ((c) => process.exit(c));
42457
+ doExit(0);
42458
+ }
42459
+ return code;
40877
42460
  }
40878
42461
  function parseArgs2(argv) {
40879
- const out = { help: false };
42462
+ const out = { exitOnCompletion: false, help: false };
40880
42463
  for (let i = 0; i < argv.length; i++) {
40881
42464
  const a = argv[i];
40882
42465
  if (a === "--help" || a === "-h") {
@@ -40891,13 +42474,17 @@ function parseArgs2(argv) {
40891
42474
  out.mode = v;
40892
42475
  } else if (a === "--storage" && argv[i + 1]) {
40893
42476
  out.storage = argv[++i];
42477
+ } else if (a === "--fortress" && argv[i + 1]) {
42478
+ out.fortress = argv[++i];
42479
+ } else if (a === "--exit-on-completion") {
42480
+ out.exitOnCompletion = true;
40894
42481
  } else if (a && a.startsWith("--")) {
40895
42482
  throw new Error(`Unknown flag: ${a}`);
40896
42483
  }
40897
42484
  }
40898
42485
  return out;
40899
42486
  }
40900
- function printUsage4(out) {
42487
+ function printUsage5(out) {
40901
42488
  out.write(`
40902
42489
  Usage: sanctuary reset-passphrase [options]
40903
42490
 
@@ -40920,9 +42507,19 @@ Recover a fortress whose passphrase has been lost or corrupted. Three modes:
40920
42507
 
40921
42508
  Options:
40922
42509
  --mode <shares|guardian|nuke> Pick a path non-interactively.
40923
- --storage <path> Override the resolved storage path.
40924
- Defaults to SANCTUARY_STORAGE_PATH or
40925
- ~/.sanctuary.
42510
+ --fortress <path> Override the fortress storage path.
42511
+ Consistent with "sanctuary wrap --fortress".
42512
+ --storage <path> Alias for --fortress.
42513
+ --exit-on-completion After a successful nuke, call process.exit(0)
42514
+ immediately so the post-wipe heap is reaped
42515
+ by the OS without re-entering the shell. Use
42516
+ on extreme-threat-model deployments where an
42517
+ attacker-on-host with heap-dump access could
42518
+ recover residual passphrase or key bytes
42519
+ between the wipe and the next operator
42520
+ command. JS strings cannot be explicitly
42521
+ zeroed; this flag is the supported way to
42522
+ bound the heap-dump window.
40926
42523
  --help, -h Show this help.
40927
42524
 
40928
42525
  Without --mode, the command surveys which paths are operationally available
@@ -41192,8 +42789,18 @@ async function prompt(lines, err, question) {
41192
42789
  err.write(question);
41193
42790
  return await lines.next();
41194
42791
  }
42792
+ function zeroizeBuffers(buffers) {
42793
+ if (!buffers) return;
42794
+ for (const b of buffers) {
42795
+ if (!b) continue;
42796
+ try {
42797
+ b.fill(0);
42798
+ } catch {
42799
+ }
42800
+ }
42801
+ }
41195
42802
  async function defaultExec2(cmd, args) {
41196
- return await new Promise((resolve6, reject) => {
42803
+ return await new Promise((resolve8, reject) => {
41197
42804
  const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
41198
42805
  let stdout = "";
41199
42806
  let stderr = "";
@@ -41204,7 +42811,7 @@ async function defaultExec2(cmd, args) {
41204
42811
  stderr += d.toString();
41205
42812
  });
41206
42813
  child.on("error", reject);
41207
- child.on("close", (code) => resolve6({ stdout, stderr, code }));
42814
+ child.on("close", (code) => resolve8({ stdout, stderr, code }));
41208
42815
  });
41209
42816
  }
41210
42817
  var LineReader;
@@ -41240,8 +42847,8 @@ var init_reset_passphrase = __esm({
41240
42847
  return Promise.resolve(this.queue.shift());
41241
42848
  }
41242
42849
  if (this.closed) return Promise.resolve("");
41243
- return new Promise((resolve6) => {
41244
- this.waiters.push(resolve6);
42850
+ return new Promise((resolve8) => {
42851
+ this.waiters.push(resolve8);
41245
42852
  });
41246
42853
  }
41247
42854
  close() {
@@ -41640,11 +43247,11 @@ async function startMultiDashboardServer(options = {}) {
41640
43247
  }
41641
43248
  }
41642
43249
  });
41643
- await new Promise((resolve6, reject) => {
43250
+ await new Promise((resolve8, reject) => {
41644
43251
  server.once("error", reject);
41645
43252
  server.listen(port, host, () => {
41646
43253
  server.off("error", reject);
41647
- resolve6();
43254
+ resolve8();
41648
43255
  });
41649
43256
  });
41650
43257
  const addr = server.address();
@@ -41653,8 +43260,8 @@ async function startMultiDashboardServer(options = {}) {
41653
43260
  url: `http://${host}:${actualPort}`,
41654
43261
  port: actualPort,
41655
43262
  host,
41656
- stop: () => new Promise((resolve6, reject) => {
41657
- server.close((err) => err ? reject(err) : resolve6());
43263
+ stop: () => new Promise((resolve8, reject) => {
43264
+ server.close((err) => err ? reject(err) : resolve8());
41658
43265
  })
41659
43266
  };
41660
43267
  }
@@ -41872,7 +43479,19 @@ Refusing to start the dashboard while the reset-history marker is unreadable.`
41872
43479
  }
41873
43480
  throw err;
41874
43481
  }
41875
- const policy = await loadPrincipalPolicy(config.storage_path);
43482
+ let policy;
43483
+ try {
43484
+ policy = await loadPrincipalPolicy(config.storage_path);
43485
+ } catch (err) {
43486
+ if (err instanceof MalformedPrincipalPolicyError) {
43487
+ console.error(`
43488
+ Sanctuary cannot start.
43489
+ ${err.message}
43490
+ `);
43491
+ process.exit(1);
43492
+ }
43493
+ throw err;
43494
+ }
41876
43495
  const baseline = new BaselineTracker(storage, masterKey);
41877
43496
  await baseline.load();
41878
43497
  const dashboardPort = options.port ?? config.dashboard.port;
@@ -42054,7 +43673,7 @@ function formatUpdateMessage(current, latest) {
42054
43673
  return `[Sanctuary] Update available: ${current} \u2192 ${latest}. Run: npx @sanctuary-framework/mcp-server@latest`;
42055
43674
  }
42056
43675
  function fetchLatestVersion(currentVersion) {
42057
- return new Promise((resolve6) => {
43676
+ return new Promise((resolve8) => {
42058
43677
  const req = get(
42059
43678
  REGISTRY_URL,
42060
43679
  {
@@ -42064,7 +43683,7 @@ function fetchLatestVersion(currentVersion) {
42064
43683
  (res) => {
42065
43684
  if (res.statusCode !== 200) {
42066
43685
  res.resume();
42067
- resolve6(null);
43686
+ resolve8(null);
42068
43687
  return;
42069
43688
  }
42070
43689
  let data = "";
@@ -42073,7 +43692,7 @@ function fetchLatestVersion(currentVersion) {
42073
43692
  data += chunk;
42074
43693
  if (data.length > 32768) {
42075
43694
  res.destroy();
42076
- resolve6(null);
43695
+ resolve8(null);
42077
43696
  }
42078
43697
  });
42079
43698
  res.on("end", () => {
@@ -42081,20 +43700,20 @@ function fetchLatestVersion(currentVersion) {
42081
43700
  const json = JSON.parse(data);
42082
43701
  const latest = json.version;
42083
43702
  if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
42084
- resolve6(latest);
43703
+ resolve8(latest);
42085
43704
  } else {
42086
- resolve6(null);
43705
+ resolve8(null);
42087
43706
  }
42088
43707
  } catch {
42089
- resolve6(null);
43708
+ resolve8(null);
42090
43709
  }
42091
43710
  });
42092
43711
  }
42093
43712
  );
42094
- req.on("error", () => resolve6(null));
43713
+ req.on("error", () => resolve8(null));
42095
43714
  req.on("timeout", () => {
42096
43715
  req.destroy();
42097
- resolve6(null);
43716
+ resolve8(null);
42098
43717
  });
42099
43718
  });
42100
43719
  }
@@ -42172,6 +43791,11 @@ async function main() {
42172
43791
  const code = await runTemplateCommand2({ argv: args.slice(1) });
42173
43792
  process.exit(code);
42174
43793
  }
43794
+ if (args[0] === "identity") {
43795
+ const { runIdentityCommand: runIdentityCommand2 } = await Promise.resolve().then(() => (init_identity2(), identity_exports2));
43796
+ const code = await runIdentityCommand2({ argv: args.slice(1) });
43797
+ process.exit(code);
43798
+ }
42175
43799
  if (args[0] === "agents") {
42176
43800
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
42177
43801
  const code = await runAgentsCommand2({ argv: args.slice(1) });
@@ -42393,6 +44017,9 @@ Subcommands:
42393
44017
  Use "sanctuary dashboard --help" for options.
42394
44018
  Pass --multi to render the multi-tenant overview.
42395
44019
 
44020
+ identity Inspect the active identity (DID, public key).
44021
+ Use "sanctuary identity --help" for options.
44022
+
42396
44023
  template Manage policy templates (list, init).
42397
44024
  Use "sanctuary template --help" for options.
42398
44025