@sanctuary-framework/mcp-server 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { randomBytes as randomBytes$1, createHmac, createHash, randomUUID } from 'crypto';
1
+ import { randomBytes as randomBytes$1, createHmac, randomUUID, createHash } from 'crypto';
2
2
  import { gcm } from '@noble/ciphers/aes.js';
3
3
  import { sha256 } from '@noble/hashes/sha256';
4
4
  import { hmac } from '@noble/hashes/hmac';
@@ -17442,6 +17442,535 @@ async function handleCoordinationRoute(deps, req, res) {
17442
17442
  }
17443
17443
  }
17444
17444
 
17445
+ // src/honeypot/types.ts
17446
+ var FILESYSTEM_OPS = [
17447
+ "read",
17448
+ "write",
17449
+ "delete",
17450
+ "list"
17451
+ ];
17452
+ var HONEYPOT_AUDIT_OPS = {
17453
+ DRAFTED: "honeypot_drafted",
17454
+ COMPILED: "honeypot_compiled",
17455
+ DEPLOYED: "honeypot_deployed",
17456
+ TRIGGERED: "honeypot_triggered",
17457
+ UNDEPLOYED: "honeypot_undeployed",
17458
+ LOADED: "honeypot_loaded"
17459
+ };
17460
+ var HONEYPOT_SENTINEL_ID_PREFIX = "honeypot:";
17461
+ function honeypotSentinelId(trapId) {
17462
+ return `${HONEYPOT_SENTINEL_ID_PREFIX}${trapId}`;
17463
+ }
17464
+ var COMPILE_SURFACE = "template-suggestion";
17465
+ var COMPILE_MAX_TOKENS = 800;
17466
+ var DEFAULT_SEVERITY = "alert";
17467
+ var COMPILE_PROMPT = `You are compiling a Sanctuary honeypot from an operator's plain-English description.
17468
+ Return STRICT JSON with the following shape (no markdown, no commentary):
17469
+ {
17470
+ "trap_class": "http_endpoint" | "filesystem",
17471
+ "path_pattern": "string (glob with * or **)",
17472
+ "method": "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY",
17473
+ "ops": ["read", "write", "delete", "list"],
17474
+ "expected_caller_types": ["wrapped_agent" | "operator" | "external"],
17475
+ "finding_severity": "warn" | "alert",
17476
+ "explanation_paragraph": "one-sentence operator-friendly explanation"
17477
+ }
17478
+ Default trap_class to "http_endpoint" unless the operator clearly describes filesystem access (file reads/writes, directory access, path-on-disk monitoring). The "method" field applies only to http_endpoint traps; the "ops" field applies only to filesystem traps. Match the operator's stated severity if they gave one; otherwise default to "alert". If method is unspecified for http_endpoint, return "ANY". If ops is unspecified for filesystem, return ["read","write","delete","list"]. If caller-type is unspecified, return ["wrapped_agent"]. Keep the explanation_paragraph under 200 characters.`;
17479
+ async function compileHoneypot(draft, opts) {
17480
+ const now = opts?.now ?? (() => /* @__PURE__ */ new Date());
17481
+ const trapIdFactory = opts?.trapIdFactory ?? (() => randomUUID());
17482
+ const warnings = [];
17483
+ let trigger = null;
17484
+ let trapClass = "http_endpoint";
17485
+ let severity = DEFAULT_SEVERITY;
17486
+ let explanation = "";
17487
+ let source = "heuristic";
17488
+ if (opts?.selector) {
17489
+ try {
17490
+ const handle = await opts.selector.getSubstrate(COMPILE_SURFACE);
17491
+ if (handle.capability.summarize) {
17492
+ const response = await opts.selector.invokeSummarize(
17493
+ COMPILE_SURFACE,
17494
+ {
17495
+ kind: "summarize",
17496
+ context: COMPILE_PROMPT,
17497
+ query: draft.english_text,
17498
+ maxTokens: COMPILE_MAX_TOKENS
17499
+ }
17500
+ );
17501
+ if (response.body.kind === "summarize" && !response.failureClass) {
17502
+ const parsed = tryParseLlmResponse(response.body.text);
17503
+ if (parsed.ok) {
17504
+ trigger = parsed.trigger;
17505
+ trapClass = parsed.trapClass;
17506
+ severity = parsed.severity;
17507
+ explanation = parsed.explanation;
17508
+ source = "llm";
17509
+ } else {
17510
+ warnings.push(
17511
+ `LLM response failed validation (${parsed.failure}); falling back to heuristic compile`
17512
+ );
17513
+ }
17514
+ } else {
17515
+ warnings.push(
17516
+ `LLM compile failed (${response.failureClass ?? "non_summarize_body"}); falling back to heuristic compile`
17517
+ );
17518
+ }
17519
+ } else {
17520
+ warnings.push(
17521
+ "Substrate at template-suggestion surface does not support summarize; falling back to heuristic compile"
17522
+ );
17523
+ }
17524
+ } catch (err) {
17525
+ const message = err instanceof Error ? err.message : String(err);
17526
+ warnings.push(
17527
+ `LLM compile threw (${message}); falling back to heuristic compile`
17528
+ );
17529
+ }
17530
+ }
17531
+ if (trigger === null) {
17532
+ const heuristic = heuristicCompile(draft.english_text);
17533
+ trigger = heuristic.trigger;
17534
+ trapClass = heuristic.trapClass;
17535
+ if (heuristic.severity) severity = heuristic.severity;
17536
+ explanation = heuristic.explanation;
17537
+ if (heuristic.warning) warnings.push(heuristic.warning);
17538
+ }
17539
+ const spec = {
17540
+ trap_id: trapIdFactory(),
17541
+ trap_class: trapClass,
17542
+ trigger,
17543
+ finding_severity: severity,
17544
+ english_text: draft.english_text,
17545
+ explanation_paragraph: explanation,
17546
+ compiled_at: now().toISOString()
17547
+ };
17548
+ return { spec, source, warnings };
17549
+ }
17550
+ function tryParseLlmResponse(text) {
17551
+ let body;
17552
+ try {
17553
+ const stripped = text.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
17554
+ body = JSON.parse(stripped);
17555
+ } catch {
17556
+ return { ok: false, failure: "invalid_json" };
17557
+ }
17558
+ if (!body || typeof body !== "object") {
17559
+ return { ok: false, failure: "invalid_json" };
17560
+ }
17561
+ const obj = body;
17562
+ const pathPattern = obj["path_pattern"];
17563
+ if (typeof pathPattern !== "string" || pathPattern.length === 0) {
17564
+ return { ok: false, failure: "missing_path_pattern" };
17565
+ }
17566
+ const callerTypes = Array.isArray(obj["expected_caller_types"]) ? obj["expected_caller_types"].filter(
17567
+ (v) => typeof v === "string" && v.length > 0
17568
+ ) : ["wrapped_agent"];
17569
+ if (callerTypes.length === 0) {
17570
+ return { ok: false, failure: "invalid_caller_types" };
17571
+ }
17572
+ const severityRaw = obj["finding_severity"];
17573
+ const severity = severityRaw === "warn" ? "warn" : severityRaw === "alert" ? "alert" : DEFAULT_SEVERITY;
17574
+ const explanationRaw = obj["explanation_paragraph"];
17575
+ const explanation = typeof explanationRaw === "string" && explanationRaw.length > 0 ? explanationRaw : "Honeypot compiled from operator draft via LLM-assisted compile path.";
17576
+ const trapClassRaw = obj["trap_class"];
17577
+ const trapClass = trapClassRaw === "filesystem" ? "filesystem" : "http_endpoint";
17578
+ if (trapClass === "filesystem") {
17579
+ const opsParsed = parseFilesystemOps(obj["ops"]);
17580
+ if (opsParsed === null) {
17581
+ return { ok: false, failure: "invalid_filesystem_ops" };
17582
+ }
17583
+ const trigger2 = {
17584
+ kind: "filesystem",
17585
+ path_pattern: pathPattern,
17586
+ ops: opsParsed,
17587
+ expected_caller_types: callerTypes
17588
+ };
17589
+ return { ok: true, trapClass, trigger: trigger2, severity, explanation };
17590
+ }
17591
+ const method = typeof obj["method"] === "string" ? obj["method"] : "ANY";
17592
+ const trigger = {
17593
+ kind: "http_endpoint",
17594
+ path_pattern: pathPattern,
17595
+ ...method !== "ANY" ? { method: method.toUpperCase() } : {},
17596
+ expected_caller_types: callerTypes
17597
+ };
17598
+ return { ok: true, trapClass, trigger, severity, explanation };
17599
+ }
17600
+ function parseFilesystemOps(raw) {
17601
+ if (raw === void 0 || raw === null) {
17602
+ return [...FILESYSTEM_OPS];
17603
+ }
17604
+ if (!Array.isArray(raw)) return null;
17605
+ if (raw.length === 0) return [...FILESYSTEM_OPS];
17606
+ const out = [];
17607
+ for (const entry of raw) {
17608
+ if (typeof entry !== "string") return null;
17609
+ if (!FILESYSTEM_OPS.includes(entry)) return null;
17610
+ if (!out.includes(entry)) out.push(entry);
17611
+ }
17612
+ return out;
17613
+ }
17614
+ var HEURISTIC_PATH_PATTERNS = [
17615
+ /honeypot\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
17616
+ /trap\s+(?:at|on|for)\s+([\/][\w\/\-:*\.]+)/i,
17617
+ /deploy\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
17618
+ /catch\s+(?:requests?\s+to|callers?\s+at)\s+([\/][\w\/\-:*\.]+)/i,
17619
+ /watch\s+(?:for\s+)?(?:requests?\s+(?:to|on))\s+([\/][\w\/\-:*\.]+)/i,
17620
+ /([\/][\w\/\-:*\.]+)\s+(?:endpoint|path|route)/i
17621
+ ];
17622
+ var SEVERITY_HINTS = [
17623
+ { phrase: /\b(?:warn|warning|low\s+severity)\b/i, severity: "warn" },
17624
+ { phrase: /\b(?:alert|critical|high\s+severity)\b/i, severity: "alert" }
17625
+ ];
17626
+ var FILESYSTEM_CLASS_HINTS = [
17627
+ /\bfilesystem\b/i,
17628
+ /\bfile[-\s]?system\b/i,
17629
+ /\bfile\s+(?:read|write|delete|list|access|trap|honeypot)/i,
17630
+ /\b(?:read|write|delete|list)\s+file/i,
17631
+ /\bdirectory\b/i,
17632
+ /\bon[-\s]?disk\b/i,
17633
+ /\bpath\s+on\s+disk\b/i
17634
+ ];
17635
+ var FILESYSTEM_OP_HINTS = [
17636
+ { phrase: /\b(?:read|reads|reading|access(?:es|ed)?)\b/i, op: "read" },
17637
+ { phrase: /\b(?:write|writes|writing|modif(?:y|ies|ied)|edit)/i, op: "write" },
17638
+ { phrase: /\b(?:delete|deletes|deletion|remove|removal|unlink)/i, op: "delete" },
17639
+ { phrase: /\b(?:list|listing|enumerate|enumeration|directory\s+listing)/i, op: "list" }
17640
+ ];
17641
+ function heuristicCompile(english) {
17642
+ let pathPattern = null;
17643
+ for (const re of HEURISTIC_PATH_PATTERNS) {
17644
+ const match = english.match(re);
17645
+ if (match && match[1]) {
17646
+ pathPattern = match[1];
17647
+ break;
17648
+ }
17649
+ }
17650
+ const fallbackUsed = pathPattern === null;
17651
+ if (pathPattern === null) {
17652
+ pathPattern = "/honeypot-stub";
17653
+ }
17654
+ let severity;
17655
+ for (const hint of SEVERITY_HINTS) {
17656
+ if (hint.phrase.test(english)) {
17657
+ severity = hint.severity;
17658
+ break;
17659
+ }
17660
+ }
17661
+ const isFilesystem = FILESYSTEM_CLASS_HINTS.some((re) => re.test(english));
17662
+ if (isFilesystem) {
17663
+ const ops = [];
17664
+ for (const hint of FILESYSTEM_OP_HINTS) {
17665
+ if (hint.phrase.test(english) && !ops.includes(hint.op)) {
17666
+ ops.push(hint.op);
17667
+ }
17668
+ }
17669
+ const resolvedOps = ops.length > 0 ? ops : [...FILESYSTEM_OPS];
17670
+ const trigger2 = {
17671
+ kind: "filesystem",
17672
+ path_pattern: pathPattern,
17673
+ ops: resolvedOps,
17674
+ expected_caller_types: ["wrapped_agent"]
17675
+ };
17676
+ const opsRendered = resolvedOps.join(",");
17677
+ const explanation2 = fallbackUsed ? `Filesystem honeypot compiled from operator draft via heuristic fallback; the English description did not yield a clear path pattern, so the trap is stubbed at ${pathPattern} (ops=${opsRendered}). Operator should edit the path_pattern before deploy.` : `Filesystem honeypot compiled from operator draft via heuristic compile; trap fires on ${opsRendered} operations against ${pathPattern}.`;
17678
+ return {
17679
+ trapClass: "filesystem",
17680
+ trigger: trigger2,
17681
+ ...severity !== void 0 ? { severity } : {},
17682
+ explanation: explanation2,
17683
+ ...fallbackUsed ? {
17684
+ warning: "heuristic compile could not extract a path pattern from the filesystem draft; trap is stubbed at /honeypot-stub. Either rewrite the draft (e.g., 'filesystem honeypot at /etc/secrets for reads') or edit the spec's path_pattern before deploying"
17685
+ } : {}
17686
+ };
17687
+ }
17688
+ const trigger = {
17689
+ kind: "http_endpoint",
17690
+ path_pattern: pathPattern,
17691
+ expected_caller_types: ["wrapped_agent"]
17692
+ };
17693
+ const explanation = fallbackUsed ? `Honeypot compiled from operator draft via heuristic fallback; the English description did not yield a clear path pattern, so the trap is stubbed at ${pathPattern}. Operator should edit the path_pattern before deploy.` : `Honeypot compiled from operator draft via heuristic compile; trap fires on requests to ${pathPattern}.`;
17694
+ return {
17695
+ trapClass: "http_endpoint",
17696
+ trigger,
17697
+ ...severity !== void 0 ? { severity } : {},
17698
+ explanation,
17699
+ ...fallbackUsed ? {
17700
+ warning: "heuristic compile could not extract a path pattern from the draft; trap is stubbed at /honeypot-stub. Either rewrite the draft (e.g., 'honeypot at /admin/secrets') or edit the spec's path_pattern before deploying"
17701
+ } : {}
17702
+ };
17703
+ }
17704
+ function hashOfEnglishDraft(text) {
17705
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32);
17706
+ }
17707
+
17708
+ // src/honeypot/runtime-trap-handler.ts
17709
+ var HONEYPOT_API_PREFIX = "/api/honeypot";
17710
+ async function handleHoneypotTriggerIfMatch(deps, req, res) {
17711
+ const url = req.url ?? "/";
17712
+ const path = url.split("?")[0] ?? "/";
17713
+ const method = (req.method ?? "GET").toUpperCase();
17714
+ if (path.startsWith(HONEYPOT_API_PREFIX)) return false;
17715
+ if (path.startsWith("/api/sentinels")) return false;
17716
+ if (path.startsWith("/api/coordination")) return false;
17717
+ const match = deps.registry.findMatching({ path, method });
17718
+ if (!match) return false;
17719
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
17720
+ const callerIdentity = extractCallerIdentity(req);
17721
+ const payloadHash = await safeReadAndHashBody(req);
17722
+ const findingId = randomUUID();
17723
+ const finding = {
17724
+ finding_id: findingId,
17725
+ sentinel_id: honeypotSentinelId(match.trap_id),
17726
+ severity: match.finding_severity,
17727
+ summary: buildSummary(match, callerIdentity, path, method),
17728
+ details: {
17729
+ trap_id: match.trap_id,
17730
+ trap_class: match.trap_class,
17731
+ path_matched: path,
17732
+ method,
17733
+ caller_identity: callerIdentity,
17734
+ payload_hash: payloadHash
17735
+ },
17736
+ observed_at: now.toISOString(),
17737
+ evidence_audit_ids: [],
17738
+ fortress_id: deps.fortressId
17739
+ };
17740
+ await deps.findingStore.saveFinding(finding).catch(() => void 0);
17741
+ deps.auditLog.append(
17742
+ "l2",
17743
+ HONEYPOT_AUDIT_OPS.TRIGGERED,
17744
+ deps.operatorId,
17745
+ {
17746
+ trap_id: match.trap_id,
17747
+ trap_class: match.trap_class,
17748
+ path_matched: path,
17749
+ method,
17750
+ caller_identity: callerIdentity,
17751
+ payload_hash: payloadHash,
17752
+ finding_id: findingId,
17753
+ severity: match.finding_severity
17754
+ }
17755
+ );
17756
+ res.writeHead(404, { "Content-Type": "application/json" });
17757
+ res.end(JSON.stringify({ error: "not_found", path }));
17758
+ return true;
17759
+ }
17760
+ function buildSummary(spec, callerIdentity, path, method) {
17761
+ return `honeypot ${spec.trap_id} triggered: ${method} ${path} from ${callerIdentity} (severity ${spec.finding_severity}, pattern ${spec.trigger.path_pattern})`;
17762
+ }
17763
+ function extractCallerIdentity(req) {
17764
+ const headers = req.headers;
17765
+ const agent = headers["x-sanctuary-agent"];
17766
+ if (typeof agent === "string" && agent.length > 0) return `agent:${agent}`;
17767
+ const xff = headers["x-forwarded-for"];
17768
+ if (typeof xff === "string" && xff.length > 0) {
17769
+ const first = xff.split(",")[0]?.trim();
17770
+ if (first) return `ip:${first}`;
17771
+ }
17772
+ const ip = req.socket.remoteAddress;
17773
+ return ip ? `ip:${ip}` : "ip:unknown";
17774
+ }
17775
+ async function safeReadAndHashBody(req) {
17776
+ const MAX_BYTES = 64 * 1024;
17777
+ try {
17778
+ const chunks = [];
17779
+ let total = 0;
17780
+ for await (const chunk of req) {
17781
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
17782
+ total += buf.length;
17783
+ if (total > MAX_BYTES) {
17784
+ return "unhashed";
17785
+ }
17786
+ chunks.push(buf);
17787
+ }
17788
+ if (chunks.length === 0) return "empty";
17789
+ const body = Buffer.concat(chunks);
17790
+ return createHash("sha256").update(body).digest("hex").slice(0, 32);
17791
+ } catch {
17792
+ return "unhashed";
17793
+ }
17794
+ }
17795
+ function writeJSON7(res, status, payload) {
17796
+ res.writeHead(status, {
17797
+ "Content-Type": "application/json",
17798
+ "Cache-Control": "no-store"
17799
+ });
17800
+ res.end(JSON.stringify(payload));
17801
+ }
17802
+ async function readJSONBody4(req) {
17803
+ const chunks = [];
17804
+ for await (const chunk of req) {
17805
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
17806
+ }
17807
+ if (chunks.length === 0) return void 0;
17808
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
17809
+ }
17810
+ function matchTrapIdRoute(path) {
17811
+ const prefix = `${HONEYPOT_API_PREFIX}/traps/`;
17812
+ if (!path.startsWith(prefix)) return null;
17813
+ const rest = path.slice(prefix.length);
17814
+ if (rest.length === 0 || rest.includes("/")) return null;
17815
+ return { trapId: decodeURIComponent(rest) };
17816
+ }
17817
+ async function handleHoneypotRoute(deps, req, res) {
17818
+ const host = req.headers.host || "localhost";
17819
+ const url = new URL(req.url ?? "/", `http://${host}`);
17820
+ const method = (req.method ?? "GET").toUpperCase();
17821
+ const path = url.pathname;
17822
+ if (path !== HONEYPOT_API_PREFIX && !path.startsWith(`${HONEYPOT_API_PREFIX}/`)) {
17823
+ return false;
17824
+ }
17825
+ const checkAuth = authMiddleware(deps.authConfig);
17826
+ if (!checkAuth(req, res, url)) return true;
17827
+ try {
17828
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/compile`) {
17829
+ const body = await readJSONBody4(req);
17830
+ const englishText = body && typeof body === "object" && typeof body["english_text"] === "string" ? body["english_text"] : "";
17831
+ if (englishText.length === 0) {
17832
+ writeJSON7(res, 400, {
17833
+ ok: false,
17834
+ error: "english_text required"
17835
+ });
17836
+ return true;
17837
+ }
17838
+ const draft = {
17839
+ english_text: englishText,
17840
+ operator_id: deps.operatorId,
17841
+ observed_at: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
17842
+ };
17843
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DRAFTED, deps.operatorId, {
17844
+ fortress_id: deps.fortressId,
17845
+ english_hash: hashOfEnglishDraft(englishText)
17846
+ });
17847
+ const result = await compileHoneypot(draft, {
17848
+ ...deps.selector !== void 0 ? { selector: deps.selector } : {},
17849
+ ...deps.now !== void 0 ? { now: deps.now } : {}
17850
+ });
17851
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.COMPILED, deps.operatorId, {
17852
+ fortress_id: deps.fortressId,
17853
+ trap_id: result.spec.trap_id,
17854
+ source: result.source,
17855
+ warning_count: result.warnings.length
17856
+ });
17857
+ writeJSON7(res, 200, {
17858
+ ok: true,
17859
+ data: { spec: result.spec, source: result.source, warnings: result.warnings }
17860
+ });
17861
+ return true;
17862
+ }
17863
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/deploy`) {
17864
+ const body = await readJSONBody4(req);
17865
+ const spec = body && typeof body === "object" && body["spec"] ? body["spec"] : null;
17866
+ if (!spec || typeof spec.trap_id !== "string" || spec.trap_id.length === 0) {
17867
+ writeJSON7(res, 400, { ok: false, error: "spec.trap_id required" });
17868
+ return true;
17869
+ }
17870
+ const isNew = deps.registry.deploy(spec);
17871
+ let persistError = null;
17872
+ if (deps.store) {
17873
+ try {
17874
+ await deps.store.save(spec);
17875
+ } catch (err) {
17876
+ persistError = err instanceof Error ? err.message : String(err);
17877
+ }
17878
+ }
17879
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DEPLOYED, deps.operatorId, {
17880
+ fortress_id: deps.fortressId,
17881
+ trap_id: spec.trap_id,
17882
+ trap_class: spec.trap_class,
17883
+ path_pattern: spec.trigger.path_pattern,
17884
+ was_new: isNew,
17885
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
17886
+ });
17887
+ writeJSON7(res, 200, {
17888
+ ok: true,
17889
+ data: {
17890
+ trap_id: spec.trap_id,
17891
+ was_new: isNew,
17892
+ ...deps.store ? { persisted: persistError === null } : {},
17893
+ ...persistError !== null ? { persist_error: persistError } : {}
17894
+ }
17895
+ });
17896
+ return true;
17897
+ }
17898
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/traps`) {
17899
+ const traps = deps.registry.list();
17900
+ writeJSON7(res, 200, { ok: true, data: { traps } });
17901
+ return true;
17902
+ }
17903
+ const trapMatch = matchTrapIdRoute(path);
17904
+ if (method === "DELETE" && trapMatch) {
17905
+ const removed = deps.registry.undeploy(trapMatch.trapId);
17906
+ let persistError = null;
17907
+ if (deps.store) {
17908
+ try {
17909
+ await deps.store.delete(trapMatch.trapId);
17910
+ } catch (err) {
17911
+ persistError = err instanceof Error ? err.message : String(err);
17912
+ }
17913
+ }
17914
+ if (removed) {
17915
+ deps.auditLog.append(
17916
+ "l2",
17917
+ HONEYPOT_AUDIT_OPS.UNDEPLOYED,
17918
+ deps.operatorId,
17919
+ {
17920
+ fortress_id: deps.fortressId,
17921
+ trap_id: trapMatch.trapId,
17922
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
17923
+ }
17924
+ );
17925
+ }
17926
+ writeJSON7(res, removed ? 200 : 404, {
17927
+ ok: removed,
17928
+ data: {
17929
+ trap_id: trapMatch.trapId,
17930
+ removed,
17931
+ ...deps.store ? { persisted: persistError === null } : {},
17932
+ ...persistError !== null ? { persist_error: persistError } : {}
17933
+ }
17934
+ });
17935
+ return true;
17936
+ }
17937
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/findings`) {
17938
+ const since = url.searchParams.get("since") ?? void 0;
17939
+ const severityRaw = url.searchParams.get("severity") ?? void 0;
17940
+ const severity = isValidSeverity(severityRaw) ? severityRaw : void 0;
17941
+ const limit = parseLimit5(url.searchParams.get("limit"), 50, 500);
17942
+ const all = await deps.findingStore.listFindings({
17943
+ ...since !== void 0 ? { since } : {},
17944
+ ...severity !== void 0 ? { severity } : {},
17945
+ limit: 500
17946
+ });
17947
+ const honeypotFindings = all.filter(
17948
+ (f) => f.sentinel_id.startsWith(HONEYPOT_SENTINEL_ID_PREFIX)
17949
+ );
17950
+ writeJSON7(res, 200, {
17951
+ ok: true,
17952
+ data: { findings: honeypotFindings.slice(0, limit) }
17953
+ });
17954
+ return true;
17955
+ }
17956
+ writeJSON7(res, 404, { ok: false, error: "not_found", path });
17957
+ return true;
17958
+ } catch (err) {
17959
+ const msg = err instanceof Error ? err.message : String(err);
17960
+ writeJSON7(res, 500, { ok: false, error: "internal", detail: msg });
17961
+ return true;
17962
+ }
17963
+ }
17964
+ function isValidSeverity(value) {
17965
+ return value === "info" || value === "warn" || value === "alert";
17966
+ }
17967
+ function parseLimit5(raw, defaultValue, max) {
17968
+ if (raw === null || raw === "") return defaultValue;
17969
+ const parsed = Number.parseInt(raw, 10);
17970
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
17971
+ return Math.min(parsed, max);
17972
+ }
17973
+
17445
17974
  // src/principal-policy/dashboard.ts
17446
17975
  var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
17447
17976
  var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
@@ -17534,6 +18063,22 @@ var DashboardApprovalChannel = class {
17534
18063
  workflowStateTracker = null;
17535
18064
  handoffAuditLog = null;
17536
18065
  handoffOperatorId = null;
18066
+ // v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: per-fortress trap registry
18067
+ // + finding store + audit log + operator id. Front-of-dispatch hook
18068
+ // consults the registry on every request; management routes at
18069
+ // /api/honeypot/* go through the dispatch path.
18070
+ honeypotRegistry = null;
18071
+ honeypotFindingStore = null;
18072
+ honeypotAuditLog = null;
18073
+ honeypotOperatorId = null;
18074
+ honeypotFortressId = null;
18075
+ honeypotSelector = null;
18076
+ // Pi-2: encrypted at-rest persistence for deployed honeypot traps.
18077
+ // When present, the management API's deploy + undeploy handlers
18078
+ // write through to the store; on fortress boot the host code calls
18079
+ // `store.loadAll()` and re-deploys the persisted specs into the
18080
+ // in-memory registry before this dashboard begins serving.
18081
+ honeypotStore = null;
17537
18082
  constructor(config) {
17538
18083
  this.config = config;
17539
18084
  this.authToken = config.auth_token;
@@ -17615,6 +18160,30 @@ var DashboardApprovalChannel = class {
17615
18160
  this.handoffContextTransfer = opts.contextTransfer ?? null;
17616
18161
  this.workflowStateTracker = opts.workflowStateTracker ?? null;
17617
18162
  }
18163
+ /**
18164
+ * v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: bind the per-fortress
18165
+ * trap registry + finding store + audit log + operator id. Once
18166
+ * set, two surfaces activate:
18167
+ * 1. Front-of-dispatch trap-trigger hook: every request runs
18168
+ * through `handleHoneypotTriggerIfMatch` BEFORE legacy/v1.1/
18169
+ * sentinel/coordination routing. Matching traps return 404
18170
+ * and the request never reaches the regular dispatcher.
18171
+ * 2. Management API at /api/honeypot/* routes through
18172
+ * `handleHoneypotRoute`.
18173
+ *
18174
+ * The optional `selector` opt wires the LLM compile path; absent
18175
+ * selector forces the heuristic compile path (which still produces
18176
+ * a usable TrapSpec with warnings).
18177
+ */
18178
+ setHoneypotRegistry(opts) {
18179
+ this.honeypotRegistry = opts.registry;
18180
+ this.honeypotFindingStore = opts.findingStore ?? null;
18181
+ this.honeypotAuditLog = opts.auditLog ?? null;
18182
+ this.honeypotOperatorId = opts.operatorId ?? null;
18183
+ this.honeypotFortressId = opts.fortressId ?? null;
18184
+ this.honeypotSelector = opts.selector ?? null;
18185
+ this.honeypotStore = opts.store ?? null;
18186
+ }
17618
18187
  /**
17619
18188
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
17620
18189
  * before the legacy approval route table. Returns true when served.
@@ -17679,6 +18248,57 @@ var DashboardApprovalChannel = class {
17679
18248
  res
17680
18249
  );
17681
18250
  }
18251
+ /**
18252
+ * v1.3 WP-V1.3-5 Pi-1 dispatch entry point. Routes
18253
+ * `/api/honeypot/*` requests through the honeypot management
18254
+ * router when a registry has been bound. Returns true when served.
18255
+ */
18256
+ async dispatchHoneypot(req, res) {
18257
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
18258
+ return false;
18259
+ }
18260
+ return handleHoneypotRoute(
18261
+ {
18262
+ authConfig: {
18263
+ loopbackAutoAuth: this._autoAuthLocalhost,
18264
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
18265
+ },
18266
+ registry: this.honeypotRegistry,
18267
+ findingStore: this.honeypotFindingStore,
18268
+ auditLog: this.honeypotAuditLog,
18269
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18270
+ fortressId: this.honeypotFortressId ?? "fortress_default",
18271
+ ...this.honeypotSelector !== null ? { selector: this.honeypotSelector } : {},
18272
+ ...this.honeypotStore !== null ? { store: this.honeypotStore } : {}
18273
+ },
18274
+ req,
18275
+ res
18276
+ );
18277
+ }
18278
+ /**
18279
+ * v1.3 WP-V1.3-5 Pi-1 front-of-dispatch trap-trigger hook. Examines
18280
+ * every request BEFORE legacy/v1.1/sentinel/coordination routing.
18281
+ * Returns true when a deployed trap matched the request and the
18282
+ * handler emitted the audit event + sentinel finding + plausible
18283
+ * 404 response. Returns false when no trap matched; caller
18284
+ * continues with normal routing.
18285
+ */
18286
+ async dispatchHoneypotTrap(req, res) {
18287
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
18288
+ return false;
18289
+ }
18290
+ return handleHoneypotTriggerIfMatch(
18291
+ {
18292
+ registry: this.honeypotRegistry,
18293
+ findingStore: this.honeypotFindingStore,
18294
+ auditLog: this.honeypotAuditLog,
18295
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18296
+ fortressId: this.honeypotFortressId ?? "fortress_default"
18297
+ },
18298
+ req,
18299
+ res
18300
+ );
18301
+ }
17682
18302
  /**
17683
18303
  * v1.1 dispatch entry point. Called from `handleRequest` before the
17684
18304
  * legacy route table. Returns true when the request was served by v1.1
@@ -18054,6 +18674,40 @@ var DashboardApprovalChannel = class {
18054
18674
  res.end();
18055
18675
  return;
18056
18676
  }
18677
+ if (this.honeypotRegistry) {
18678
+ this.dispatchHoneypotTrap(req, res).then((handled) => {
18679
+ if (handled) return;
18680
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
18681
+ }).catch(() => {
18682
+ if (!res.headersSent) {
18683
+ res.writeHead(500, { "Content-Type": "application/json" });
18684
+ res.end(JSON.stringify({ error: "Internal server error" }));
18685
+ }
18686
+ });
18687
+ return;
18688
+ }
18689
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
18690
+ }
18691
+ /**
18692
+ * v1.3 WP-V1.3-5 Pi-1: post-honeypot-trap request continuation. The
18693
+ * front-of-dispatch trap-trigger hook may short-circuit a request;
18694
+ * when it does not, this method runs the original dispatch ladder.
18695
+ * Pulled out as a helper so the trap-hook + non-trap paths share
18696
+ * one code path through every downstream dispatcher.
18697
+ */
18698
+ continueHandleRequest(req, res, url, method, _origin, _selfOrigin) {
18699
+ if (this.honeypotRegistry && url.pathname.startsWith(HONEYPOT_API_PREFIX)) {
18700
+ this.dispatchHoneypot(req, res).then((handled) => {
18701
+ if (handled) return;
18702
+ this.handleLegacyRequest(req, res, url, method);
18703
+ }).catch(() => {
18704
+ if (!res.headersSent) {
18705
+ res.writeHead(500, { "Content-Type": "application/json" });
18706
+ res.end(JSON.stringify({ error: "Internal server error" }));
18707
+ }
18708
+ });
18709
+ return;
18710
+ }
18057
18711
  if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
18058
18712
  this.dispatchApprovalInbox(req, res).then((handled) => {
18059
18713
  if (handled) return;
@@ -22270,6 +22924,187 @@ var WorkflowStateTracker = class {
22270
22924
  }
22271
22925
  };
22272
22926
 
22927
+ // src/honeypot/trap-registry.ts
22928
+ var TrapRegistry = class {
22929
+ traps = /* @__PURE__ */ new Map();
22930
+ /**
22931
+ * Deploy a trap. Idempotent on `trap_id`: re-deploying replaces the
22932
+ * previous spec for that id. Returns true on first deploy, false on
22933
+ * re-deploy (so callers can branch audit emission).
22934
+ */
22935
+ deploy(spec) {
22936
+ const isNew = !this.traps.has(spec.trap_id);
22937
+ this.traps.set(spec.trap_id, spec);
22938
+ return isNew;
22939
+ }
22940
+ /**
22941
+ * Undeploy by trap_id. Returns true when a trap was removed, false
22942
+ * when no trap had that id (idempotent).
22943
+ */
22944
+ undeploy(trapId) {
22945
+ return this.traps.delete(trapId);
22946
+ }
22947
+ /** List deployed traps. Returns a fresh array; mutation is safe. */
22948
+ list() {
22949
+ return [...this.traps.values()];
22950
+ }
22951
+ /** Look up a single trap by id. */
22952
+ get(trapId) {
22953
+ return this.traps.get(trapId);
22954
+ }
22955
+ /**
22956
+ * Find the first trap matching the request. Iteration order is
22957
+ * insertion order; operators who deploy multiple overlapping traps
22958
+ * see the earliest-deployed one fire. Tests cover this contract.
22959
+ */
22960
+ findMatching(input) {
22961
+ for (const spec of this.traps.values()) {
22962
+ if (matchesTrap(spec, input)) return spec;
22963
+ }
22964
+ return void 0;
22965
+ }
22966
+ /** Drop every trap. Tests use this between runs; not surfaced via API. */
22967
+ clear() {
22968
+ this.traps.clear();
22969
+ }
22970
+ };
22971
+ function matchesTrap(spec, input) {
22972
+ if (spec.trigger.kind !== "http_endpoint") return false;
22973
+ const trigger = spec.trigger;
22974
+ if (trigger.method && trigger.method.toUpperCase() !== input.method.toUpperCase()) {
22975
+ return false;
22976
+ }
22977
+ const re = compileGlob(trigger.path_pattern);
22978
+ return re.test(input.path);
22979
+ }
22980
+ function compileGlob(pattern) {
22981
+ let out = "";
22982
+ let i = 0;
22983
+ while (i < pattern.length) {
22984
+ const ch = pattern[i];
22985
+ if (ch === "*" && pattern[i + 1] === "*") {
22986
+ out += ".*";
22987
+ i += 2;
22988
+ continue;
22989
+ }
22990
+ if (ch === "*") {
22991
+ out += "[^/]*";
22992
+ i += 1;
22993
+ continue;
22994
+ }
22995
+ if ("\\^$.|?+()[]{}".includes(ch)) {
22996
+ out += `\\${ch}`;
22997
+ } else {
22998
+ out += ch;
22999
+ }
23000
+ i += 1;
23001
+ }
23002
+ return new RegExp(`^${out}$`);
23003
+ }
23004
+
23005
+ // src/honeypot/trap-store.ts
23006
+ init_encryption();
23007
+ init_encoding();
23008
+ var TRAP_STORE_NAMESPACE = "_honeypot_traps";
23009
+ var TRAP_STORE_KEY_PREFIX = "trap.";
23010
+ var HKDF_INFO3 = "l2-honeypot-trap-v1";
23011
+ var MAX_TRAP_BYTES = 64 * 1024;
23012
+ var TrapStore = class {
23013
+ storage;
23014
+ encryptionKey;
23015
+ fortressId;
23016
+ constructor(opts) {
23017
+ this.storage = opts.storage;
23018
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
23019
+ this.fortressId = opts.fortressId;
23020
+ }
23021
+ /** Persist (or overwrite) one trap. Returns the trap_id on success. */
23022
+ async save(spec) {
23023
+ const persisted = { version: 1, spec };
23024
+ const aad = stringToBytes(spec.trap_id);
23025
+ const plaintext = stringToBytes(JSON.stringify(persisted));
23026
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
23027
+ await this.storage.write(
23028
+ TRAP_STORE_NAMESPACE,
23029
+ trapKey(spec.trap_id),
23030
+ stringToBytes(JSON.stringify(envelope))
23031
+ );
23032
+ return spec.trap_id;
23033
+ }
23034
+ /**
23035
+ * Remove one trap by id. Returns true when a record was removed,
23036
+ * false when no record existed (idempotent).
23037
+ */
23038
+ async delete(trapId) {
23039
+ try {
23040
+ const raw = await this.storage.read(
23041
+ TRAP_STORE_NAMESPACE,
23042
+ trapKey(trapId)
23043
+ );
23044
+ if (!raw) return false;
23045
+ await this.storage.delete(TRAP_STORE_NAMESPACE, trapKey(trapId));
23046
+ return true;
23047
+ } catch {
23048
+ return false;
23049
+ }
23050
+ }
23051
+ /**
23052
+ * Load every persisted trap. Used at boot to repopulate the
23053
+ * in-memory TrapRegistry. Corrupted records are silently skipped
23054
+ * so one malformed entry never blocks the rest of the fortress's
23055
+ * traps from rehydrating.
23056
+ *
23057
+ * Returns the specs sorted by `compiled_at` ascending so the
23058
+ * in-memory registry's insertion order matches the original
23059
+ * deploy order (relevant for Pi-1's "first-deployed wins on
23060
+ * overlapping match" contract).
23061
+ */
23062
+ async loadAll() {
23063
+ const metas = await this.storage.list(
23064
+ TRAP_STORE_NAMESPACE,
23065
+ TRAP_STORE_KEY_PREFIX
23066
+ );
23067
+ const out = [];
23068
+ for (const meta of metas) {
23069
+ const trapId = stripKeyPrefix3(meta.key);
23070
+ if (trapId === null) continue;
23071
+ const raw = await this.storage.read(TRAP_STORE_NAMESPACE, meta.key);
23072
+ if (!raw) continue;
23073
+ if (raw.length > MAX_TRAP_BYTES) continue;
23074
+ const spec = this.decode(trapId, raw);
23075
+ if (spec !== null) out.push(spec);
23076
+ }
23077
+ out.sort(
23078
+ (a, b) => a.compiled_at < b.compiled_at ? -1 : a.compiled_at > b.compiled_at ? 1 : 0
23079
+ );
23080
+ return out;
23081
+ }
23082
+ /** Read-only fortress-id getter. */
23083
+ getFortressId() {
23084
+ return this.fortressId;
23085
+ }
23086
+ decode(trapId, raw) {
23087
+ try {
23088
+ const aad = stringToBytes(trapId);
23089
+ const envelope = JSON.parse(bytesToString(raw));
23090
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
23091
+ const persisted = JSON.parse(bytesToString(plaintext));
23092
+ if (persisted.version !== 1) return null;
23093
+ if (persisted.spec.trap_id !== trapId) return null;
23094
+ return persisted.spec;
23095
+ } catch {
23096
+ return null;
23097
+ }
23098
+ }
23099
+ };
23100
+ function trapKey(trapId) {
23101
+ return `${TRAP_STORE_KEY_PREFIX}${trapId}`;
23102
+ }
23103
+ function stripKeyPrefix3(key) {
23104
+ if (!key.startsWith(TRAP_STORE_KEY_PREFIX)) return null;
23105
+ return key.slice(TRAP_STORE_KEY_PREFIX.length);
23106
+ }
23107
+
22273
23108
  // src/sentinel/sentinel.ts
22274
23109
  var Sentinel = class {
22275
23110
  /**
@@ -37427,7 +38262,7 @@ function hashOf(input) {
37427
38262
  init_encryption();
37428
38263
  init_encoding();
37429
38264
  var OPERATOR_CHAT_NAMESPACE = "_chat";
37430
- var HKDF_INFO3 = "operator-chat-store-v1";
38265
+ var HKDF_INFO4 = "operator-chat-store-v1";
37431
38266
  function chatStorageKey(surface, threadKey) {
37432
38267
  return `${surface}.${threadKey}`;
37433
38268
  }
@@ -37436,7 +38271,7 @@ var OperatorChatStore = class {
37436
38271
  encryptionKey;
37437
38272
  constructor(storage, masterKey) {
37438
38273
  this.storage = storage;
37439
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
38274
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
37440
38275
  }
37441
38276
  /**
37442
38277
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -37520,7 +38355,7 @@ init_encryption();
37520
38355
  init_encoding();
37521
38356
  var CONCIERGE_MEMORY_NAMESPACE = "_chat";
37522
38357
  var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
37523
- var HKDF_INFO4 = "concierge-memory-store-v1";
38358
+ var HKDF_INFO5 = "concierge-memory-store-v1";
37524
38359
  var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
37525
38360
  var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
37526
38361
  var ConciergeMemoryStore = class {
@@ -37531,7 +38366,7 @@ var ConciergeMemoryStore = class {
37531
38366
  locks;
37532
38367
  constructor(opts) {
37533
38368
  this.storage = opts.storage;
37534
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
38369
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO5);
37535
38370
  this.fortressId = opts.fortressId;
37536
38371
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
37537
38372
  this.locks = /* @__PURE__ */ new Map();
@@ -37657,7 +38492,7 @@ var ConciergeMemoryStore = class {
37657
38492
  );
37658
38493
  const summaries = [];
37659
38494
  for (const meta of entries) {
37660
- const threadId = stripKeyPrefix3(meta.key);
38495
+ const threadId = stripKeyPrefix4(meta.key);
37661
38496
  if (threadId === null) continue;
37662
38497
  const bundle = await this.loadBundle(threadId);
37663
38498
  if (!bundle || bundle.turns.length === 0) continue;
@@ -37710,7 +38545,7 @@ var ConciergeMemoryStore = class {
37710
38545
  );
37711
38546
  let pruned = 0;
37712
38547
  for (const meta of entries) {
37713
- const threadId = stripKeyPrefix3(meta.key);
38548
+ const threadId = stripKeyPrefix4(meta.key);
37714
38549
  if (threadId === null) continue;
37715
38550
  pruned += await this.withLock(threadId, async () => {
37716
38551
  const bundle = await this.loadBundle(threadId);
@@ -37794,7 +38629,7 @@ var ConciergeMemoryStore = class {
37794
38629
  function bundleKey(threadId) {
37795
38630
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
37796
38631
  }
37797
- function stripKeyPrefix3(key) {
38632
+ function stripKeyPrefix4(key) {
37798
38633
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
37799
38634
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
37800
38635
  }
@@ -38167,13 +39002,13 @@ init_encryption();
38167
39002
  init_encoding();
38168
39003
  var INTELLIGENCE_NAMESPACE = "_intelligence";
38169
39004
  var SUBSTRATE_CONFIG_KEY = "substrate-config";
38170
- var HKDF_INFO5 = "intelligence-substrate-config";
39005
+ var HKDF_INFO6 = "intelligence-substrate-config";
38171
39006
  var IntelligenceConfigStore = class {
38172
39007
  storage;
38173
39008
  encryptionKey;
38174
39009
  constructor(storage, masterKey) {
38175
39010
  this.storage = storage;
38176
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
39011
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO6);
38177
39012
  }
38178
39013
  /**
38179
39014
  * Load the operator's substrate config from disk. Returns the config
@@ -40221,6 +41056,55 @@ async function defaultFetcher(url, init) {
40221
41056
  json: () => response.json()
40222
41057
  };
40223
41058
  }
41059
+ var FORTRESS_DID_WEB_REGISTRY_PATH = "recognition/did-web.json";
41060
+ async function loadFortressDidWebRecord(storagePath) {
41061
+ const persistPath = join(storagePath, FORTRESS_DID_WEB_REGISTRY_PATH);
41062
+ let raw;
41063
+ try {
41064
+ raw = await readFile(persistPath, "utf-8");
41065
+ } catch (err) {
41066
+ const code = err.code;
41067
+ if (code === "ENOENT") return null;
41068
+ throw err;
41069
+ }
41070
+ let parsed;
41071
+ try {
41072
+ parsed = JSON.parse(raw);
41073
+ } catch (e) {
41074
+ const message = e instanceof Error ? e.message : String(e);
41075
+ throw new Error(
41076
+ `did-web: fortress-config record at ${persistPath} is not valid JSON: ${message}`
41077
+ );
41078
+ }
41079
+ if (!isFortressDidWebRecord(parsed)) {
41080
+ throw new Error(
41081
+ `did-web: fortress-config record at ${persistPath} is malformed (expected version: 1 with identifier.did + identifier.authority_host)`
41082
+ );
41083
+ }
41084
+ return parsed;
41085
+ }
41086
+ function isFortressDidWebRecord(value) {
41087
+ if (!value || typeof value !== "object") return false;
41088
+ const v = value;
41089
+ if (v["version"] !== 1) return false;
41090
+ const id = v["identifier"];
41091
+ if (!id || typeof id !== "object") return false;
41092
+ if (typeof id["did"] !== "string" || !id["did"].startsWith("did:web:")) {
41093
+ return false;
41094
+ }
41095
+ if (typeof id["authority_host"] !== "string") return false;
41096
+ if (typeof id["fortress_id"] !== "string") return false;
41097
+ if (typeof id["created_at"] !== "string") return false;
41098
+ if (!id["did_document"] || typeof id["did_document"] !== "object") {
41099
+ return false;
41100
+ }
41101
+ const artifact = v["artifact"];
41102
+ if (!artifact || typeof artifact !== "object") return false;
41103
+ if (typeof artifact["url"] !== "string") return false;
41104
+ if (typeof artifact["publish_path"] !== "string") return false;
41105
+ if (typeof artifact["sha256"] !== "string") return false;
41106
+ return true;
41107
+ }
40224
41108
 
40225
41109
  // src/exit/bundle.ts
40226
41110
  init_hashing();
@@ -41669,9 +42553,31 @@ Options:
41669
42553
  --accept-unverifiable-attestations
41670
42554
  On import: accept reputation attestations whose
41671
42555
  signer DID is not in the bundle (Tier 1 confirmation)
42556
+ --did-web <identifier> Embed a specific did:web identifier in the export
42557
+ manifest. Requires --did-web-authority-host.
42558
+ Overrides fortress-config auto-inclusion.
42559
+ --did-web-authority-host <host> Authority host for --did-web (required with it).
42560
+ --did-web-published-at <iso8601> Operator's claimed publication time for the DID
42561
+ Document (optional; ISO 8601).
42562
+ --no-did-web Explicit opt-out: skip did:web inclusion even if
42563
+ a fortress-config record exists. (Alias for
42564
+ --include-did-web=false.)
42565
+ --did-web-allowed-host <host> On import: host allowed for outbound did:web
42566
+ resolution; repeatable. Empty means refuse to
42567
+ resolve (no-outbound-by-default).
42568
+ --skip-did-web-verify On import: skip did:web resolution entirely.
41672
42569
  --json
41673
42570
  --yes, -y Explicit non-interactive Tier 1 approval
41674
42571
  --help, -h
42572
+
42573
+ did:web auto-inclusion (build 3):
42574
+ Running "sanctuary did-web issue --authority-host <host>" registers the
42575
+ operator's did:web identifier at <storage>/recognition/did-web.json.
42576
+ Subsequent "sanctuary exit export" runs auto-include this identifier in
42577
+ the manifest's identity_binding without requiring any --did-web flag.
42578
+ Per-fortress isolation is structural: the record lives under the
42579
+ fortress's storage_path, so different fortresses carry different
42580
+ registered identifiers.
41675
42581
  `);
41676
42582
  }
41677
42583
  async function runExitCommand(args) {
@@ -41770,12 +42676,15 @@ ${policyErr.message}
41770
42676
  throw policyErr;
41771
42677
  }
41772
42678
  const includeDidWebFlag = flagValue(argv, "--include-did-web");
41773
- const includeDidWebDisabled = includeDidWebFlag === "false";
42679
+ const explicitOptOut = hasFlag(argv, "--no-did-web") || includeDidWebFlag === "false";
41774
42680
  const didWebIdentifier = flagValue(argv, "--did-web");
41775
42681
  const didWebAuthorityHost = flagValue(argv, "--did-web-authority-host");
41776
42682
  const didWebPublishedAt = flagValue(argv, "--did-web-published-at");
41777
42683
  let exportDidWeb;
41778
- if (!includeDidWebDisabled && didWebIdentifier !== void 0) {
42684
+ let didWebSource;
42685
+ if (explicitOptOut) {
42686
+ didWebSource = "opted-out";
42687
+ } else if (didWebIdentifier !== void 0) {
41779
42688
  if (didWebAuthorityHost === void 0) {
41780
42689
  write(
41781
42690
  err,
@@ -41788,6 +42697,19 @@ ${policyErr.message}
41788
42697
  authority_host: didWebAuthorityHost,
41789
42698
  ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
41790
42699
  };
42700
+ didWebSource = "cli-override";
42701
+ } else {
42702
+ const record = await loadFortressDidWebRecord(ctx.storagePath);
42703
+ if (record !== null) {
42704
+ exportDidWeb = {
42705
+ identifier: record.identifier.did,
42706
+ authority_host: record.identifier.authority_host,
42707
+ ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
42708
+ };
42709
+ didWebSource = "fortress-config";
42710
+ } else {
42711
+ didWebSource = "no-record";
42712
+ }
41791
42713
  }
41792
42714
  const result = await exportExitBundle({
41793
42715
  bundleDir: outDir,
@@ -41803,12 +42725,42 @@ ${policyErr.message}
41803
42725
  keySource: ctx.keySource,
41804
42726
  ...exportDidWeb !== void 0 ? { didWeb: exportDidWeb } : {}
41805
42727
  });
41806
- if (json) write(out, JSON.stringify(result, null, 2) + "\n");
41807
- else {
42728
+ if (json) {
42729
+ write(
42730
+ out,
42731
+ JSON.stringify(
42732
+ { ...result, did_web_source: didWebSource },
42733
+ null,
42734
+ 2
42735
+ ) + "\n"
42736
+ );
42737
+ } else {
41808
42738
  write(out, `exported: ${result.bundle_dir}
41809
42739
  `);
41810
42740
  write(out, `manifest_hash: ${result.manifest_hash}
41811
42741
  `);
42742
+ if (didWebSource === "fortress-config" && exportDidWeb) {
42743
+ write(
42744
+ out,
42745
+ `did:web: auto-included from fortress config (${exportDidWeb.identifier})
42746
+ `
42747
+ );
42748
+ } else if (didWebSource === "cli-override" && exportDidWeb) {
42749
+ write(
42750
+ out,
42751
+ `did:web: included via CLI override (${exportDidWeb.identifier})
42752
+ `
42753
+ );
42754
+ } else if (didWebSource === "opted-out") {
42755
+ write(out, `did:web: skipped (operator opt-out via --no-did-web)
42756
+ `);
42757
+ } else if (didWebSource === "no-record") {
42758
+ write(
42759
+ out,
42760
+ `did:web: not included (no fortress config; run "sanctuary did-web issue" to register)
42761
+ `
42762
+ );
42763
+ }
41812
42764
  for (const item of result.unsupported_artifacts) {
41813
42765
  write(out, `unsupported: ${item}
41814
42766
  `);
@@ -42465,6 +43417,7 @@ ${err.message}
42465
43417
  await baseline.load();
42466
43418
  let approvalChannel;
42467
43419
  let dashboard;
43420
+ let intelligenceSelector;
42468
43421
  if (config.dashboard.enabled) {
42469
43422
  let authToken = config.dashboard.auth_token;
42470
43423
  if (authToken === "auto") {
@@ -42491,7 +43444,6 @@ ${err.message}
42491
43444
  profileStore
42492
43445
  });
42493
43446
  const embeddedHubIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
42494
- let intelligenceSelector;
42495
43447
  try {
42496
43448
  intelligenceSelector = new SubstrateSelector({
42497
43449
  storage,
@@ -42648,6 +43600,44 @@ ${err.message}
42648
43600
  workflowStateTracker
42649
43601
  });
42650
43602
  }
43603
+ const honeypotRegistry = new TrapRegistry();
43604
+ const honeypotStore = new TrapStore({
43605
+ storage,
43606
+ masterKey,
43607
+ fortressId: fortressIdForAggregator
43608
+ });
43609
+ try {
43610
+ const persistedSpecs = await honeypotStore.loadAll();
43611
+ for (const spec of persistedSpecs) {
43612
+ honeypotRegistry.deploy(spec);
43613
+ }
43614
+ if (persistedSpecs.length > 0) {
43615
+ auditLog.append(
43616
+ "l2",
43617
+ HONEYPOT_AUDIT_OPS.LOADED,
43618
+ aggregatorIdentityId,
43619
+ {
43620
+ fortress_id: fortressIdForAggregator,
43621
+ trap_count: persistedSpecs.length
43622
+ }
43623
+ );
43624
+ }
43625
+ } catch (err) {
43626
+ console.error(
43627
+ ` Note: honeypot trap store unavailable (${err.message}). Deployed traps from prior runs will not be restored; re-deploy via the management API.`
43628
+ );
43629
+ }
43630
+ if (dashboard) {
43631
+ dashboard.setHoneypotRegistry({
43632
+ registry: honeypotRegistry,
43633
+ findingStore: sentinelFindingStore,
43634
+ auditLog,
43635
+ operatorId: aggregatorIdentityId,
43636
+ fortressId: fortressIdForAggregator,
43637
+ ...intelligenceSelector ? { selector: intelligenceSelector } : {},
43638
+ store: honeypotStore
43639
+ });
43640
+ }
42651
43641
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
42652
43642
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
42653
43643
  config,