@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.cjs CHANGED
@@ -17449,6 +17449,535 @@ async function handleCoordinationRoute(deps, req, res) {
17449
17449
  }
17450
17450
  }
17451
17451
 
17452
+ // src/honeypot/types.ts
17453
+ var FILESYSTEM_OPS = [
17454
+ "read",
17455
+ "write",
17456
+ "delete",
17457
+ "list"
17458
+ ];
17459
+ var HONEYPOT_AUDIT_OPS = {
17460
+ DRAFTED: "honeypot_drafted",
17461
+ COMPILED: "honeypot_compiled",
17462
+ DEPLOYED: "honeypot_deployed",
17463
+ TRIGGERED: "honeypot_triggered",
17464
+ UNDEPLOYED: "honeypot_undeployed",
17465
+ LOADED: "honeypot_loaded"
17466
+ };
17467
+ var HONEYPOT_SENTINEL_ID_PREFIX = "honeypot:";
17468
+ function honeypotSentinelId(trapId) {
17469
+ return `${HONEYPOT_SENTINEL_ID_PREFIX}${trapId}`;
17470
+ }
17471
+ var COMPILE_SURFACE = "template-suggestion";
17472
+ var COMPILE_MAX_TOKENS = 800;
17473
+ var DEFAULT_SEVERITY = "alert";
17474
+ var COMPILE_PROMPT = `You are compiling a Sanctuary honeypot from an operator's plain-English description.
17475
+ Return STRICT JSON with the following shape (no markdown, no commentary):
17476
+ {
17477
+ "trap_class": "http_endpoint" | "filesystem",
17478
+ "path_pattern": "string (glob with * or **)",
17479
+ "method": "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY",
17480
+ "ops": ["read", "write", "delete", "list"],
17481
+ "expected_caller_types": ["wrapped_agent" | "operator" | "external"],
17482
+ "finding_severity": "warn" | "alert",
17483
+ "explanation_paragraph": "one-sentence operator-friendly explanation"
17484
+ }
17485
+ 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.`;
17486
+ async function compileHoneypot(draft, opts) {
17487
+ const now = opts?.now ?? (() => /* @__PURE__ */ new Date());
17488
+ const trapIdFactory = opts?.trapIdFactory ?? (() => crypto.randomUUID());
17489
+ const warnings = [];
17490
+ let trigger = null;
17491
+ let trapClass = "http_endpoint";
17492
+ let severity = DEFAULT_SEVERITY;
17493
+ let explanation = "";
17494
+ let source = "heuristic";
17495
+ if (opts?.selector) {
17496
+ try {
17497
+ const handle = await opts.selector.getSubstrate(COMPILE_SURFACE);
17498
+ if (handle.capability.summarize) {
17499
+ const response = await opts.selector.invokeSummarize(
17500
+ COMPILE_SURFACE,
17501
+ {
17502
+ kind: "summarize",
17503
+ context: COMPILE_PROMPT,
17504
+ query: draft.english_text,
17505
+ maxTokens: COMPILE_MAX_TOKENS
17506
+ }
17507
+ );
17508
+ if (response.body.kind === "summarize" && !response.failureClass) {
17509
+ const parsed = tryParseLlmResponse(response.body.text);
17510
+ if (parsed.ok) {
17511
+ trigger = parsed.trigger;
17512
+ trapClass = parsed.trapClass;
17513
+ severity = parsed.severity;
17514
+ explanation = parsed.explanation;
17515
+ source = "llm";
17516
+ } else {
17517
+ warnings.push(
17518
+ `LLM response failed validation (${parsed.failure}); falling back to heuristic compile`
17519
+ );
17520
+ }
17521
+ } else {
17522
+ warnings.push(
17523
+ `LLM compile failed (${response.failureClass ?? "non_summarize_body"}); falling back to heuristic compile`
17524
+ );
17525
+ }
17526
+ } else {
17527
+ warnings.push(
17528
+ "Substrate at template-suggestion surface does not support summarize; falling back to heuristic compile"
17529
+ );
17530
+ }
17531
+ } catch (err) {
17532
+ const message = err instanceof Error ? err.message : String(err);
17533
+ warnings.push(
17534
+ `LLM compile threw (${message}); falling back to heuristic compile`
17535
+ );
17536
+ }
17537
+ }
17538
+ if (trigger === null) {
17539
+ const heuristic = heuristicCompile(draft.english_text);
17540
+ trigger = heuristic.trigger;
17541
+ trapClass = heuristic.trapClass;
17542
+ if (heuristic.severity) severity = heuristic.severity;
17543
+ explanation = heuristic.explanation;
17544
+ if (heuristic.warning) warnings.push(heuristic.warning);
17545
+ }
17546
+ const spec = {
17547
+ trap_id: trapIdFactory(),
17548
+ trap_class: trapClass,
17549
+ trigger,
17550
+ finding_severity: severity,
17551
+ english_text: draft.english_text,
17552
+ explanation_paragraph: explanation,
17553
+ compiled_at: now().toISOString()
17554
+ };
17555
+ return { spec, source, warnings };
17556
+ }
17557
+ function tryParseLlmResponse(text) {
17558
+ let body;
17559
+ try {
17560
+ const stripped = text.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
17561
+ body = JSON.parse(stripped);
17562
+ } catch {
17563
+ return { ok: false, failure: "invalid_json" };
17564
+ }
17565
+ if (!body || typeof body !== "object") {
17566
+ return { ok: false, failure: "invalid_json" };
17567
+ }
17568
+ const obj = body;
17569
+ const pathPattern = obj["path_pattern"];
17570
+ if (typeof pathPattern !== "string" || pathPattern.length === 0) {
17571
+ return { ok: false, failure: "missing_path_pattern" };
17572
+ }
17573
+ const callerTypes = Array.isArray(obj["expected_caller_types"]) ? obj["expected_caller_types"].filter(
17574
+ (v) => typeof v === "string" && v.length > 0
17575
+ ) : ["wrapped_agent"];
17576
+ if (callerTypes.length === 0) {
17577
+ return { ok: false, failure: "invalid_caller_types" };
17578
+ }
17579
+ const severityRaw = obj["finding_severity"];
17580
+ const severity = severityRaw === "warn" ? "warn" : severityRaw === "alert" ? "alert" : DEFAULT_SEVERITY;
17581
+ const explanationRaw = obj["explanation_paragraph"];
17582
+ const explanation = typeof explanationRaw === "string" && explanationRaw.length > 0 ? explanationRaw : "Honeypot compiled from operator draft via LLM-assisted compile path.";
17583
+ const trapClassRaw = obj["trap_class"];
17584
+ const trapClass = trapClassRaw === "filesystem" ? "filesystem" : "http_endpoint";
17585
+ if (trapClass === "filesystem") {
17586
+ const opsParsed = parseFilesystemOps(obj["ops"]);
17587
+ if (opsParsed === null) {
17588
+ return { ok: false, failure: "invalid_filesystem_ops" };
17589
+ }
17590
+ const trigger2 = {
17591
+ kind: "filesystem",
17592
+ path_pattern: pathPattern,
17593
+ ops: opsParsed,
17594
+ expected_caller_types: callerTypes
17595
+ };
17596
+ return { ok: true, trapClass, trigger: trigger2, severity, explanation };
17597
+ }
17598
+ const method = typeof obj["method"] === "string" ? obj["method"] : "ANY";
17599
+ const trigger = {
17600
+ kind: "http_endpoint",
17601
+ path_pattern: pathPattern,
17602
+ ...method !== "ANY" ? { method: method.toUpperCase() } : {},
17603
+ expected_caller_types: callerTypes
17604
+ };
17605
+ return { ok: true, trapClass, trigger, severity, explanation };
17606
+ }
17607
+ function parseFilesystemOps(raw) {
17608
+ if (raw === void 0 || raw === null) {
17609
+ return [...FILESYSTEM_OPS];
17610
+ }
17611
+ if (!Array.isArray(raw)) return null;
17612
+ if (raw.length === 0) return [...FILESYSTEM_OPS];
17613
+ const out = [];
17614
+ for (const entry of raw) {
17615
+ if (typeof entry !== "string") return null;
17616
+ if (!FILESYSTEM_OPS.includes(entry)) return null;
17617
+ if (!out.includes(entry)) out.push(entry);
17618
+ }
17619
+ return out;
17620
+ }
17621
+ var HEURISTIC_PATH_PATTERNS = [
17622
+ /honeypot\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
17623
+ /trap\s+(?:at|on|for)\s+([\/][\w\/\-:*\.]+)/i,
17624
+ /deploy\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
17625
+ /catch\s+(?:requests?\s+to|callers?\s+at)\s+([\/][\w\/\-:*\.]+)/i,
17626
+ /watch\s+(?:for\s+)?(?:requests?\s+(?:to|on))\s+([\/][\w\/\-:*\.]+)/i,
17627
+ /([\/][\w\/\-:*\.]+)\s+(?:endpoint|path|route)/i
17628
+ ];
17629
+ var SEVERITY_HINTS = [
17630
+ { phrase: /\b(?:warn|warning|low\s+severity)\b/i, severity: "warn" },
17631
+ { phrase: /\b(?:alert|critical|high\s+severity)\b/i, severity: "alert" }
17632
+ ];
17633
+ var FILESYSTEM_CLASS_HINTS = [
17634
+ /\bfilesystem\b/i,
17635
+ /\bfile[-\s]?system\b/i,
17636
+ /\bfile\s+(?:read|write|delete|list|access|trap|honeypot)/i,
17637
+ /\b(?:read|write|delete|list)\s+file/i,
17638
+ /\bdirectory\b/i,
17639
+ /\bon[-\s]?disk\b/i,
17640
+ /\bpath\s+on\s+disk\b/i
17641
+ ];
17642
+ var FILESYSTEM_OP_HINTS = [
17643
+ { phrase: /\b(?:read|reads|reading|access(?:es|ed)?)\b/i, op: "read" },
17644
+ { phrase: /\b(?:write|writes|writing|modif(?:y|ies|ied)|edit)/i, op: "write" },
17645
+ { phrase: /\b(?:delete|deletes|deletion|remove|removal|unlink)/i, op: "delete" },
17646
+ { phrase: /\b(?:list|listing|enumerate|enumeration|directory\s+listing)/i, op: "list" }
17647
+ ];
17648
+ function heuristicCompile(english) {
17649
+ let pathPattern = null;
17650
+ for (const re of HEURISTIC_PATH_PATTERNS) {
17651
+ const match = english.match(re);
17652
+ if (match && match[1]) {
17653
+ pathPattern = match[1];
17654
+ break;
17655
+ }
17656
+ }
17657
+ const fallbackUsed = pathPattern === null;
17658
+ if (pathPattern === null) {
17659
+ pathPattern = "/honeypot-stub";
17660
+ }
17661
+ let severity;
17662
+ for (const hint of SEVERITY_HINTS) {
17663
+ if (hint.phrase.test(english)) {
17664
+ severity = hint.severity;
17665
+ break;
17666
+ }
17667
+ }
17668
+ const isFilesystem = FILESYSTEM_CLASS_HINTS.some((re) => re.test(english));
17669
+ if (isFilesystem) {
17670
+ const ops = [];
17671
+ for (const hint of FILESYSTEM_OP_HINTS) {
17672
+ if (hint.phrase.test(english) && !ops.includes(hint.op)) {
17673
+ ops.push(hint.op);
17674
+ }
17675
+ }
17676
+ const resolvedOps = ops.length > 0 ? ops : [...FILESYSTEM_OPS];
17677
+ const trigger2 = {
17678
+ kind: "filesystem",
17679
+ path_pattern: pathPattern,
17680
+ ops: resolvedOps,
17681
+ expected_caller_types: ["wrapped_agent"]
17682
+ };
17683
+ const opsRendered = resolvedOps.join(",");
17684
+ 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}.`;
17685
+ return {
17686
+ trapClass: "filesystem",
17687
+ trigger: trigger2,
17688
+ ...severity !== void 0 ? { severity } : {},
17689
+ explanation: explanation2,
17690
+ ...fallbackUsed ? {
17691
+ 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"
17692
+ } : {}
17693
+ };
17694
+ }
17695
+ const trigger = {
17696
+ kind: "http_endpoint",
17697
+ path_pattern: pathPattern,
17698
+ expected_caller_types: ["wrapped_agent"]
17699
+ };
17700
+ 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}.`;
17701
+ return {
17702
+ trapClass: "http_endpoint",
17703
+ trigger,
17704
+ ...severity !== void 0 ? { severity } : {},
17705
+ explanation,
17706
+ ...fallbackUsed ? {
17707
+ 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"
17708
+ } : {}
17709
+ };
17710
+ }
17711
+ function hashOfEnglishDraft(text) {
17712
+ return crypto.createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32);
17713
+ }
17714
+
17715
+ // src/honeypot/runtime-trap-handler.ts
17716
+ var HONEYPOT_API_PREFIX = "/api/honeypot";
17717
+ async function handleHoneypotTriggerIfMatch(deps, req, res) {
17718
+ const url = req.url ?? "/";
17719
+ const path = url.split("?")[0] ?? "/";
17720
+ const method = (req.method ?? "GET").toUpperCase();
17721
+ if (path.startsWith(HONEYPOT_API_PREFIX)) return false;
17722
+ if (path.startsWith("/api/sentinels")) return false;
17723
+ if (path.startsWith("/api/coordination")) return false;
17724
+ const match = deps.registry.findMatching({ path, method });
17725
+ if (!match) return false;
17726
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
17727
+ const callerIdentity = extractCallerIdentity(req);
17728
+ const payloadHash = await safeReadAndHashBody(req);
17729
+ const findingId = crypto.randomUUID();
17730
+ const finding = {
17731
+ finding_id: findingId,
17732
+ sentinel_id: honeypotSentinelId(match.trap_id),
17733
+ severity: match.finding_severity,
17734
+ summary: buildSummary(match, callerIdentity, path, method),
17735
+ details: {
17736
+ trap_id: match.trap_id,
17737
+ trap_class: match.trap_class,
17738
+ path_matched: path,
17739
+ method,
17740
+ caller_identity: callerIdentity,
17741
+ payload_hash: payloadHash
17742
+ },
17743
+ observed_at: now.toISOString(),
17744
+ evidence_audit_ids: [],
17745
+ fortress_id: deps.fortressId
17746
+ };
17747
+ await deps.findingStore.saveFinding(finding).catch(() => void 0);
17748
+ deps.auditLog.append(
17749
+ "l2",
17750
+ HONEYPOT_AUDIT_OPS.TRIGGERED,
17751
+ deps.operatorId,
17752
+ {
17753
+ trap_id: match.trap_id,
17754
+ trap_class: match.trap_class,
17755
+ path_matched: path,
17756
+ method,
17757
+ caller_identity: callerIdentity,
17758
+ payload_hash: payloadHash,
17759
+ finding_id: findingId,
17760
+ severity: match.finding_severity
17761
+ }
17762
+ );
17763
+ res.writeHead(404, { "Content-Type": "application/json" });
17764
+ res.end(JSON.stringify({ error: "not_found", path }));
17765
+ return true;
17766
+ }
17767
+ function buildSummary(spec, callerIdentity, path, method) {
17768
+ return `honeypot ${spec.trap_id} triggered: ${method} ${path} from ${callerIdentity} (severity ${spec.finding_severity}, pattern ${spec.trigger.path_pattern})`;
17769
+ }
17770
+ function extractCallerIdentity(req) {
17771
+ const headers = req.headers;
17772
+ const agent = headers["x-sanctuary-agent"];
17773
+ if (typeof agent === "string" && agent.length > 0) return `agent:${agent}`;
17774
+ const xff = headers["x-forwarded-for"];
17775
+ if (typeof xff === "string" && xff.length > 0) {
17776
+ const first = xff.split(",")[0]?.trim();
17777
+ if (first) return `ip:${first}`;
17778
+ }
17779
+ const ip = req.socket.remoteAddress;
17780
+ return ip ? `ip:${ip}` : "ip:unknown";
17781
+ }
17782
+ async function safeReadAndHashBody(req) {
17783
+ const MAX_BYTES = 64 * 1024;
17784
+ try {
17785
+ const chunks = [];
17786
+ let total = 0;
17787
+ for await (const chunk of req) {
17788
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
17789
+ total += buf.length;
17790
+ if (total > MAX_BYTES) {
17791
+ return "unhashed";
17792
+ }
17793
+ chunks.push(buf);
17794
+ }
17795
+ if (chunks.length === 0) return "empty";
17796
+ const body = Buffer.concat(chunks);
17797
+ return crypto.createHash("sha256").update(body).digest("hex").slice(0, 32);
17798
+ } catch {
17799
+ return "unhashed";
17800
+ }
17801
+ }
17802
+ function writeJSON7(res, status, payload) {
17803
+ res.writeHead(status, {
17804
+ "Content-Type": "application/json",
17805
+ "Cache-Control": "no-store"
17806
+ });
17807
+ res.end(JSON.stringify(payload));
17808
+ }
17809
+ async function readJSONBody4(req) {
17810
+ const chunks = [];
17811
+ for await (const chunk of req) {
17812
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
17813
+ }
17814
+ if (chunks.length === 0) return void 0;
17815
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
17816
+ }
17817
+ function matchTrapIdRoute(path) {
17818
+ const prefix = `${HONEYPOT_API_PREFIX}/traps/`;
17819
+ if (!path.startsWith(prefix)) return null;
17820
+ const rest = path.slice(prefix.length);
17821
+ if (rest.length === 0 || rest.includes("/")) return null;
17822
+ return { trapId: decodeURIComponent(rest) };
17823
+ }
17824
+ async function handleHoneypotRoute(deps, req, res) {
17825
+ const host = req.headers.host || "localhost";
17826
+ const url = new URL(req.url ?? "/", `http://${host}`);
17827
+ const method = (req.method ?? "GET").toUpperCase();
17828
+ const path = url.pathname;
17829
+ if (path !== HONEYPOT_API_PREFIX && !path.startsWith(`${HONEYPOT_API_PREFIX}/`)) {
17830
+ return false;
17831
+ }
17832
+ const checkAuth = authMiddleware(deps.authConfig);
17833
+ if (!checkAuth(req, res, url)) return true;
17834
+ try {
17835
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/compile`) {
17836
+ const body = await readJSONBody4(req);
17837
+ const englishText = body && typeof body === "object" && typeof body["english_text"] === "string" ? body["english_text"] : "";
17838
+ if (englishText.length === 0) {
17839
+ writeJSON7(res, 400, {
17840
+ ok: false,
17841
+ error: "english_text required"
17842
+ });
17843
+ return true;
17844
+ }
17845
+ const draft = {
17846
+ english_text: englishText,
17847
+ operator_id: deps.operatorId,
17848
+ observed_at: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
17849
+ };
17850
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DRAFTED, deps.operatorId, {
17851
+ fortress_id: deps.fortressId,
17852
+ english_hash: hashOfEnglishDraft(englishText)
17853
+ });
17854
+ const result = await compileHoneypot(draft, {
17855
+ ...deps.selector !== void 0 ? { selector: deps.selector } : {},
17856
+ ...deps.now !== void 0 ? { now: deps.now } : {}
17857
+ });
17858
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.COMPILED, deps.operatorId, {
17859
+ fortress_id: deps.fortressId,
17860
+ trap_id: result.spec.trap_id,
17861
+ source: result.source,
17862
+ warning_count: result.warnings.length
17863
+ });
17864
+ writeJSON7(res, 200, {
17865
+ ok: true,
17866
+ data: { spec: result.spec, source: result.source, warnings: result.warnings }
17867
+ });
17868
+ return true;
17869
+ }
17870
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/deploy`) {
17871
+ const body = await readJSONBody4(req);
17872
+ const spec = body && typeof body === "object" && body["spec"] ? body["spec"] : null;
17873
+ if (!spec || typeof spec.trap_id !== "string" || spec.trap_id.length === 0) {
17874
+ writeJSON7(res, 400, { ok: false, error: "spec.trap_id required" });
17875
+ return true;
17876
+ }
17877
+ const isNew = deps.registry.deploy(spec);
17878
+ let persistError = null;
17879
+ if (deps.store) {
17880
+ try {
17881
+ await deps.store.save(spec);
17882
+ } catch (err) {
17883
+ persistError = err instanceof Error ? err.message : String(err);
17884
+ }
17885
+ }
17886
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DEPLOYED, deps.operatorId, {
17887
+ fortress_id: deps.fortressId,
17888
+ trap_id: spec.trap_id,
17889
+ trap_class: spec.trap_class,
17890
+ path_pattern: spec.trigger.path_pattern,
17891
+ was_new: isNew,
17892
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
17893
+ });
17894
+ writeJSON7(res, 200, {
17895
+ ok: true,
17896
+ data: {
17897
+ trap_id: spec.trap_id,
17898
+ was_new: isNew,
17899
+ ...deps.store ? { persisted: persistError === null } : {},
17900
+ ...persistError !== null ? { persist_error: persistError } : {}
17901
+ }
17902
+ });
17903
+ return true;
17904
+ }
17905
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/traps`) {
17906
+ const traps = deps.registry.list();
17907
+ writeJSON7(res, 200, { ok: true, data: { traps } });
17908
+ return true;
17909
+ }
17910
+ const trapMatch = matchTrapIdRoute(path);
17911
+ if (method === "DELETE" && trapMatch) {
17912
+ const removed = deps.registry.undeploy(trapMatch.trapId);
17913
+ let persistError = null;
17914
+ if (deps.store) {
17915
+ try {
17916
+ await deps.store.delete(trapMatch.trapId);
17917
+ } catch (err) {
17918
+ persistError = err instanceof Error ? err.message : String(err);
17919
+ }
17920
+ }
17921
+ if (removed) {
17922
+ deps.auditLog.append(
17923
+ "l2",
17924
+ HONEYPOT_AUDIT_OPS.UNDEPLOYED,
17925
+ deps.operatorId,
17926
+ {
17927
+ fortress_id: deps.fortressId,
17928
+ trap_id: trapMatch.trapId,
17929
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
17930
+ }
17931
+ );
17932
+ }
17933
+ writeJSON7(res, removed ? 200 : 404, {
17934
+ ok: removed,
17935
+ data: {
17936
+ trap_id: trapMatch.trapId,
17937
+ removed,
17938
+ ...deps.store ? { persisted: persistError === null } : {},
17939
+ ...persistError !== null ? { persist_error: persistError } : {}
17940
+ }
17941
+ });
17942
+ return true;
17943
+ }
17944
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/findings`) {
17945
+ const since = url.searchParams.get("since") ?? void 0;
17946
+ const severityRaw = url.searchParams.get("severity") ?? void 0;
17947
+ const severity = isValidSeverity(severityRaw) ? severityRaw : void 0;
17948
+ const limit = parseLimit5(url.searchParams.get("limit"), 50, 500);
17949
+ const all = await deps.findingStore.listFindings({
17950
+ ...since !== void 0 ? { since } : {},
17951
+ ...severity !== void 0 ? { severity } : {},
17952
+ limit: 500
17953
+ });
17954
+ const honeypotFindings = all.filter(
17955
+ (f) => f.sentinel_id.startsWith(HONEYPOT_SENTINEL_ID_PREFIX)
17956
+ );
17957
+ writeJSON7(res, 200, {
17958
+ ok: true,
17959
+ data: { findings: honeypotFindings.slice(0, limit) }
17960
+ });
17961
+ return true;
17962
+ }
17963
+ writeJSON7(res, 404, { ok: false, error: "not_found", path });
17964
+ return true;
17965
+ } catch (err) {
17966
+ const msg = err instanceof Error ? err.message : String(err);
17967
+ writeJSON7(res, 500, { ok: false, error: "internal", detail: msg });
17968
+ return true;
17969
+ }
17970
+ }
17971
+ function isValidSeverity(value) {
17972
+ return value === "info" || value === "warn" || value === "alert";
17973
+ }
17974
+ function parseLimit5(raw, defaultValue, max) {
17975
+ if (raw === null || raw === "") return defaultValue;
17976
+ const parsed = Number.parseInt(raw, 10);
17977
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
17978
+ return Math.min(parsed, max);
17979
+ }
17980
+
17452
17981
  // src/principal-policy/dashboard.ts
17453
17982
  var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
17454
17983
  var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
@@ -17541,6 +18070,22 @@ var DashboardApprovalChannel = class {
17541
18070
  workflowStateTracker = null;
17542
18071
  handoffAuditLog = null;
17543
18072
  handoffOperatorId = null;
18073
+ // v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: per-fortress trap registry
18074
+ // + finding store + audit log + operator id. Front-of-dispatch hook
18075
+ // consults the registry on every request; management routes at
18076
+ // /api/honeypot/* go through the dispatch path.
18077
+ honeypotRegistry = null;
18078
+ honeypotFindingStore = null;
18079
+ honeypotAuditLog = null;
18080
+ honeypotOperatorId = null;
18081
+ honeypotFortressId = null;
18082
+ honeypotSelector = null;
18083
+ // Pi-2: encrypted at-rest persistence for deployed honeypot traps.
18084
+ // When present, the management API's deploy + undeploy handlers
18085
+ // write through to the store; on fortress boot the host code calls
18086
+ // `store.loadAll()` and re-deploys the persisted specs into the
18087
+ // in-memory registry before this dashboard begins serving.
18088
+ honeypotStore = null;
17544
18089
  constructor(config) {
17545
18090
  this.config = config;
17546
18091
  this.authToken = config.auth_token;
@@ -17622,6 +18167,30 @@ var DashboardApprovalChannel = class {
17622
18167
  this.handoffContextTransfer = opts.contextTransfer ?? null;
17623
18168
  this.workflowStateTracker = opts.workflowStateTracker ?? null;
17624
18169
  }
18170
+ /**
18171
+ * v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: bind the per-fortress
18172
+ * trap registry + finding store + audit log + operator id. Once
18173
+ * set, two surfaces activate:
18174
+ * 1. Front-of-dispatch trap-trigger hook: every request runs
18175
+ * through `handleHoneypotTriggerIfMatch` BEFORE legacy/v1.1/
18176
+ * sentinel/coordination routing. Matching traps return 404
18177
+ * and the request never reaches the regular dispatcher.
18178
+ * 2. Management API at /api/honeypot/* routes through
18179
+ * `handleHoneypotRoute`.
18180
+ *
18181
+ * The optional `selector` opt wires the LLM compile path; absent
18182
+ * selector forces the heuristic compile path (which still produces
18183
+ * a usable TrapSpec with warnings).
18184
+ */
18185
+ setHoneypotRegistry(opts) {
18186
+ this.honeypotRegistry = opts.registry;
18187
+ this.honeypotFindingStore = opts.findingStore ?? null;
18188
+ this.honeypotAuditLog = opts.auditLog ?? null;
18189
+ this.honeypotOperatorId = opts.operatorId ?? null;
18190
+ this.honeypotFortressId = opts.fortressId ?? null;
18191
+ this.honeypotSelector = opts.selector ?? null;
18192
+ this.honeypotStore = opts.store ?? null;
18193
+ }
17625
18194
  /**
17626
18195
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
17627
18196
  * before the legacy approval route table. Returns true when served.
@@ -17686,6 +18255,57 @@ var DashboardApprovalChannel = class {
17686
18255
  res
17687
18256
  );
17688
18257
  }
18258
+ /**
18259
+ * v1.3 WP-V1.3-5 Pi-1 dispatch entry point. Routes
18260
+ * `/api/honeypot/*` requests through the honeypot management
18261
+ * router when a registry has been bound. Returns true when served.
18262
+ */
18263
+ async dispatchHoneypot(req, res) {
18264
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
18265
+ return false;
18266
+ }
18267
+ return handleHoneypotRoute(
18268
+ {
18269
+ authConfig: {
18270
+ loopbackAutoAuth: this._autoAuthLocalhost,
18271
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
18272
+ },
18273
+ registry: this.honeypotRegistry,
18274
+ findingStore: this.honeypotFindingStore,
18275
+ auditLog: this.honeypotAuditLog,
18276
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18277
+ fortressId: this.honeypotFortressId ?? "fortress_default",
18278
+ ...this.honeypotSelector !== null ? { selector: this.honeypotSelector } : {},
18279
+ ...this.honeypotStore !== null ? { store: this.honeypotStore } : {}
18280
+ },
18281
+ req,
18282
+ res
18283
+ );
18284
+ }
18285
+ /**
18286
+ * v1.3 WP-V1.3-5 Pi-1 front-of-dispatch trap-trigger hook. Examines
18287
+ * every request BEFORE legacy/v1.1/sentinel/coordination routing.
18288
+ * Returns true when a deployed trap matched the request and the
18289
+ * handler emitted the audit event + sentinel finding + plausible
18290
+ * 404 response. Returns false when no trap matched; caller
18291
+ * continues with normal routing.
18292
+ */
18293
+ async dispatchHoneypotTrap(req, res) {
18294
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
18295
+ return false;
18296
+ }
18297
+ return handleHoneypotTriggerIfMatch(
18298
+ {
18299
+ registry: this.honeypotRegistry,
18300
+ findingStore: this.honeypotFindingStore,
18301
+ auditLog: this.honeypotAuditLog,
18302
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18303
+ fortressId: this.honeypotFortressId ?? "fortress_default"
18304
+ },
18305
+ req,
18306
+ res
18307
+ );
18308
+ }
17689
18309
  /**
17690
18310
  * v1.1 dispatch entry point. Called from `handleRequest` before the
17691
18311
  * legacy route table. Returns true when the request was served by v1.1
@@ -18061,6 +18681,40 @@ var DashboardApprovalChannel = class {
18061
18681
  res.end();
18062
18682
  return;
18063
18683
  }
18684
+ if (this.honeypotRegistry) {
18685
+ this.dispatchHoneypotTrap(req, res).then((handled) => {
18686
+ if (handled) return;
18687
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
18688
+ }).catch(() => {
18689
+ if (!res.headersSent) {
18690
+ res.writeHead(500, { "Content-Type": "application/json" });
18691
+ res.end(JSON.stringify({ error: "Internal server error" }));
18692
+ }
18693
+ });
18694
+ return;
18695
+ }
18696
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
18697
+ }
18698
+ /**
18699
+ * v1.3 WP-V1.3-5 Pi-1: post-honeypot-trap request continuation. The
18700
+ * front-of-dispatch trap-trigger hook may short-circuit a request;
18701
+ * when it does not, this method runs the original dispatch ladder.
18702
+ * Pulled out as a helper so the trap-hook + non-trap paths share
18703
+ * one code path through every downstream dispatcher.
18704
+ */
18705
+ continueHandleRequest(req, res, url, method, _origin, _selfOrigin) {
18706
+ if (this.honeypotRegistry && url.pathname.startsWith(HONEYPOT_API_PREFIX)) {
18707
+ this.dispatchHoneypot(req, res).then((handled) => {
18708
+ if (handled) return;
18709
+ this.handleLegacyRequest(req, res, url, method);
18710
+ }).catch(() => {
18711
+ if (!res.headersSent) {
18712
+ res.writeHead(500, { "Content-Type": "application/json" });
18713
+ res.end(JSON.stringify({ error: "Internal server error" }));
18714
+ }
18715
+ });
18716
+ return;
18717
+ }
18064
18718
  if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
18065
18719
  this.dispatchApprovalInbox(req, res).then((handled) => {
18066
18720
  if (handled) return;
@@ -22277,6 +22931,187 @@ var WorkflowStateTracker = class {
22277
22931
  }
22278
22932
  };
22279
22933
 
22934
+ // src/honeypot/trap-registry.ts
22935
+ var TrapRegistry = class {
22936
+ traps = /* @__PURE__ */ new Map();
22937
+ /**
22938
+ * Deploy a trap. Idempotent on `trap_id`: re-deploying replaces the
22939
+ * previous spec for that id. Returns true on first deploy, false on
22940
+ * re-deploy (so callers can branch audit emission).
22941
+ */
22942
+ deploy(spec) {
22943
+ const isNew = !this.traps.has(spec.trap_id);
22944
+ this.traps.set(spec.trap_id, spec);
22945
+ return isNew;
22946
+ }
22947
+ /**
22948
+ * Undeploy by trap_id. Returns true when a trap was removed, false
22949
+ * when no trap had that id (idempotent).
22950
+ */
22951
+ undeploy(trapId) {
22952
+ return this.traps.delete(trapId);
22953
+ }
22954
+ /** List deployed traps. Returns a fresh array; mutation is safe. */
22955
+ list() {
22956
+ return [...this.traps.values()];
22957
+ }
22958
+ /** Look up a single trap by id. */
22959
+ get(trapId) {
22960
+ return this.traps.get(trapId);
22961
+ }
22962
+ /**
22963
+ * Find the first trap matching the request. Iteration order is
22964
+ * insertion order; operators who deploy multiple overlapping traps
22965
+ * see the earliest-deployed one fire. Tests cover this contract.
22966
+ */
22967
+ findMatching(input) {
22968
+ for (const spec of this.traps.values()) {
22969
+ if (matchesTrap(spec, input)) return spec;
22970
+ }
22971
+ return void 0;
22972
+ }
22973
+ /** Drop every trap. Tests use this between runs; not surfaced via API. */
22974
+ clear() {
22975
+ this.traps.clear();
22976
+ }
22977
+ };
22978
+ function matchesTrap(spec, input) {
22979
+ if (spec.trigger.kind !== "http_endpoint") return false;
22980
+ const trigger = spec.trigger;
22981
+ if (trigger.method && trigger.method.toUpperCase() !== input.method.toUpperCase()) {
22982
+ return false;
22983
+ }
22984
+ const re = compileGlob(trigger.path_pattern);
22985
+ return re.test(input.path);
22986
+ }
22987
+ function compileGlob(pattern) {
22988
+ let out = "";
22989
+ let i = 0;
22990
+ while (i < pattern.length) {
22991
+ const ch = pattern[i];
22992
+ if (ch === "*" && pattern[i + 1] === "*") {
22993
+ out += ".*";
22994
+ i += 2;
22995
+ continue;
22996
+ }
22997
+ if (ch === "*") {
22998
+ out += "[^/]*";
22999
+ i += 1;
23000
+ continue;
23001
+ }
23002
+ if ("\\^$.|?+()[]{}".includes(ch)) {
23003
+ out += `\\${ch}`;
23004
+ } else {
23005
+ out += ch;
23006
+ }
23007
+ i += 1;
23008
+ }
23009
+ return new RegExp(`^${out}$`);
23010
+ }
23011
+
23012
+ // src/honeypot/trap-store.ts
23013
+ init_encryption();
23014
+ init_encoding();
23015
+ var TRAP_STORE_NAMESPACE = "_honeypot_traps";
23016
+ var TRAP_STORE_KEY_PREFIX = "trap.";
23017
+ var HKDF_INFO3 = "l2-honeypot-trap-v1";
23018
+ var MAX_TRAP_BYTES = 64 * 1024;
23019
+ var TrapStore = class {
23020
+ storage;
23021
+ encryptionKey;
23022
+ fortressId;
23023
+ constructor(opts) {
23024
+ this.storage = opts.storage;
23025
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO3);
23026
+ this.fortressId = opts.fortressId;
23027
+ }
23028
+ /** Persist (or overwrite) one trap. Returns the trap_id on success. */
23029
+ async save(spec) {
23030
+ const persisted = { version: 1, spec };
23031
+ const aad = stringToBytes(spec.trap_id);
23032
+ const plaintext = stringToBytes(JSON.stringify(persisted));
23033
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
23034
+ await this.storage.write(
23035
+ TRAP_STORE_NAMESPACE,
23036
+ trapKey(spec.trap_id),
23037
+ stringToBytes(JSON.stringify(envelope))
23038
+ );
23039
+ return spec.trap_id;
23040
+ }
23041
+ /**
23042
+ * Remove one trap by id. Returns true when a record was removed,
23043
+ * false when no record existed (idempotent).
23044
+ */
23045
+ async delete(trapId) {
23046
+ try {
23047
+ const raw = await this.storage.read(
23048
+ TRAP_STORE_NAMESPACE,
23049
+ trapKey(trapId)
23050
+ );
23051
+ if (!raw) return false;
23052
+ await this.storage.delete(TRAP_STORE_NAMESPACE, trapKey(trapId));
23053
+ return true;
23054
+ } catch {
23055
+ return false;
23056
+ }
23057
+ }
23058
+ /**
23059
+ * Load every persisted trap. Used at boot to repopulate the
23060
+ * in-memory TrapRegistry. Corrupted records are silently skipped
23061
+ * so one malformed entry never blocks the rest of the fortress's
23062
+ * traps from rehydrating.
23063
+ *
23064
+ * Returns the specs sorted by `compiled_at` ascending so the
23065
+ * in-memory registry's insertion order matches the original
23066
+ * deploy order (relevant for Pi-1's "first-deployed wins on
23067
+ * overlapping match" contract).
23068
+ */
23069
+ async loadAll() {
23070
+ const metas = await this.storage.list(
23071
+ TRAP_STORE_NAMESPACE,
23072
+ TRAP_STORE_KEY_PREFIX
23073
+ );
23074
+ const out = [];
23075
+ for (const meta of metas) {
23076
+ const trapId = stripKeyPrefix3(meta.key);
23077
+ if (trapId === null) continue;
23078
+ const raw = await this.storage.read(TRAP_STORE_NAMESPACE, meta.key);
23079
+ if (!raw) continue;
23080
+ if (raw.length > MAX_TRAP_BYTES) continue;
23081
+ const spec = this.decode(trapId, raw);
23082
+ if (spec !== null) out.push(spec);
23083
+ }
23084
+ out.sort(
23085
+ (a, b) => a.compiled_at < b.compiled_at ? -1 : a.compiled_at > b.compiled_at ? 1 : 0
23086
+ );
23087
+ return out;
23088
+ }
23089
+ /** Read-only fortress-id getter. */
23090
+ getFortressId() {
23091
+ return this.fortressId;
23092
+ }
23093
+ decode(trapId, raw) {
23094
+ try {
23095
+ const aad = stringToBytes(trapId);
23096
+ const envelope = JSON.parse(bytesToString(raw));
23097
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
23098
+ const persisted = JSON.parse(bytesToString(plaintext));
23099
+ if (persisted.version !== 1) return null;
23100
+ if (persisted.spec.trap_id !== trapId) return null;
23101
+ return persisted.spec;
23102
+ } catch {
23103
+ return null;
23104
+ }
23105
+ }
23106
+ };
23107
+ function trapKey(trapId) {
23108
+ return `${TRAP_STORE_KEY_PREFIX}${trapId}`;
23109
+ }
23110
+ function stripKeyPrefix3(key) {
23111
+ if (!key.startsWith(TRAP_STORE_KEY_PREFIX)) return null;
23112
+ return key.slice(TRAP_STORE_KEY_PREFIX.length);
23113
+ }
23114
+
22280
23115
  // src/sentinel/sentinel.ts
22281
23116
  var Sentinel = class {
22282
23117
  /**
@@ -37434,7 +38269,7 @@ function hashOf(input) {
37434
38269
  init_encryption();
37435
38270
  init_encoding();
37436
38271
  var OPERATOR_CHAT_NAMESPACE = "_chat";
37437
- var HKDF_INFO3 = "operator-chat-store-v1";
38272
+ var HKDF_INFO4 = "operator-chat-store-v1";
37438
38273
  function chatStorageKey(surface, threadKey) {
37439
38274
  return `${surface}.${threadKey}`;
37440
38275
  }
@@ -37443,7 +38278,7 @@ var OperatorChatStore = class {
37443
38278
  encryptionKey;
37444
38279
  constructor(storage, masterKey) {
37445
38280
  this.storage = storage;
37446
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
38281
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
37447
38282
  }
37448
38283
  /**
37449
38284
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -37527,7 +38362,7 @@ init_encryption();
37527
38362
  init_encoding();
37528
38363
  var CONCIERGE_MEMORY_NAMESPACE = "_chat";
37529
38364
  var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
37530
- var HKDF_INFO4 = "concierge-memory-store-v1";
38365
+ var HKDF_INFO5 = "concierge-memory-store-v1";
37531
38366
  var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
37532
38367
  var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
37533
38368
  var ConciergeMemoryStore = class {
@@ -37538,7 +38373,7 @@ var ConciergeMemoryStore = class {
37538
38373
  locks;
37539
38374
  constructor(opts) {
37540
38375
  this.storage = opts.storage;
37541
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
38376
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO5);
37542
38377
  this.fortressId = opts.fortressId;
37543
38378
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
37544
38379
  this.locks = /* @__PURE__ */ new Map();
@@ -37664,7 +38499,7 @@ var ConciergeMemoryStore = class {
37664
38499
  );
37665
38500
  const summaries = [];
37666
38501
  for (const meta of entries) {
37667
- const threadId = stripKeyPrefix3(meta.key);
38502
+ const threadId = stripKeyPrefix4(meta.key);
37668
38503
  if (threadId === null) continue;
37669
38504
  const bundle = await this.loadBundle(threadId);
37670
38505
  if (!bundle || bundle.turns.length === 0) continue;
@@ -37717,7 +38552,7 @@ var ConciergeMemoryStore = class {
37717
38552
  );
37718
38553
  let pruned = 0;
37719
38554
  for (const meta of entries) {
37720
- const threadId = stripKeyPrefix3(meta.key);
38555
+ const threadId = stripKeyPrefix4(meta.key);
37721
38556
  if (threadId === null) continue;
37722
38557
  pruned += await this.withLock(threadId, async () => {
37723
38558
  const bundle = await this.loadBundle(threadId);
@@ -37801,7 +38636,7 @@ var ConciergeMemoryStore = class {
37801
38636
  function bundleKey(threadId) {
37802
38637
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
37803
38638
  }
37804
- function stripKeyPrefix3(key) {
38639
+ function stripKeyPrefix4(key) {
37805
38640
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
37806
38641
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
37807
38642
  }
@@ -38174,13 +39009,13 @@ init_encryption();
38174
39009
  init_encoding();
38175
39010
  var INTELLIGENCE_NAMESPACE = "_intelligence";
38176
39011
  var SUBSTRATE_CONFIG_KEY = "substrate-config";
38177
- var HKDF_INFO5 = "intelligence-substrate-config";
39012
+ var HKDF_INFO6 = "intelligence-substrate-config";
38178
39013
  var IntelligenceConfigStore = class {
38179
39014
  storage;
38180
39015
  encryptionKey;
38181
39016
  constructor(storage, masterKey) {
38182
39017
  this.storage = storage;
38183
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
39018
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO6);
38184
39019
  }
38185
39020
  /**
38186
39021
  * Load the operator's substrate config from disk. Returns the config
@@ -40228,6 +41063,55 @@ async function defaultFetcher(url, init) {
40228
41063
  json: () => response.json()
40229
41064
  };
40230
41065
  }
41066
+ var FORTRESS_DID_WEB_REGISTRY_PATH = "recognition/did-web.json";
41067
+ async function loadFortressDidWebRecord(storagePath) {
41068
+ const persistPath = path.join(storagePath, FORTRESS_DID_WEB_REGISTRY_PATH);
41069
+ let raw;
41070
+ try {
41071
+ raw = await promises.readFile(persistPath, "utf-8");
41072
+ } catch (err) {
41073
+ const code = err.code;
41074
+ if (code === "ENOENT") return null;
41075
+ throw err;
41076
+ }
41077
+ let parsed;
41078
+ try {
41079
+ parsed = JSON.parse(raw);
41080
+ } catch (e) {
41081
+ const message = e instanceof Error ? e.message : String(e);
41082
+ throw new Error(
41083
+ `did-web: fortress-config record at ${persistPath} is not valid JSON: ${message}`
41084
+ );
41085
+ }
41086
+ if (!isFortressDidWebRecord(parsed)) {
41087
+ throw new Error(
41088
+ `did-web: fortress-config record at ${persistPath} is malformed (expected version: 1 with identifier.did + identifier.authority_host)`
41089
+ );
41090
+ }
41091
+ return parsed;
41092
+ }
41093
+ function isFortressDidWebRecord(value) {
41094
+ if (!value || typeof value !== "object") return false;
41095
+ const v = value;
41096
+ if (v["version"] !== 1) return false;
41097
+ const id = v["identifier"];
41098
+ if (!id || typeof id !== "object") return false;
41099
+ if (typeof id["did"] !== "string" || !id["did"].startsWith("did:web:")) {
41100
+ return false;
41101
+ }
41102
+ if (typeof id["authority_host"] !== "string") return false;
41103
+ if (typeof id["fortress_id"] !== "string") return false;
41104
+ if (typeof id["created_at"] !== "string") return false;
41105
+ if (!id["did_document"] || typeof id["did_document"] !== "object") {
41106
+ return false;
41107
+ }
41108
+ const artifact = v["artifact"];
41109
+ if (!artifact || typeof artifact !== "object") return false;
41110
+ if (typeof artifact["url"] !== "string") return false;
41111
+ if (typeof artifact["publish_path"] !== "string") return false;
41112
+ if (typeof artifact["sha256"] !== "string") return false;
41113
+ return true;
41114
+ }
40231
41115
 
40232
41116
  // src/exit/bundle.ts
40233
41117
  init_hashing();
@@ -41676,9 +42560,31 @@ Options:
41676
42560
  --accept-unverifiable-attestations
41677
42561
  On import: accept reputation attestations whose
41678
42562
  signer DID is not in the bundle (Tier 1 confirmation)
42563
+ --did-web <identifier> Embed a specific did:web identifier in the export
42564
+ manifest. Requires --did-web-authority-host.
42565
+ Overrides fortress-config auto-inclusion.
42566
+ --did-web-authority-host <host> Authority host for --did-web (required with it).
42567
+ --did-web-published-at <iso8601> Operator's claimed publication time for the DID
42568
+ Document (optional; ISO 8601).
42569
+ --no-did-web Explicit opt-out: skip did:web inclusion even if
42570
+ a fortress-config record exists. (Alias for
42571
+ --include-did-web=false.)
42572
+ --did-web-allowed-host <host> On import: host allowed for outbound did:web
42573
+ resolution; repeatable. Empty means refuse to
42574
+ resolve (no-outbound-by-default).
42575
+ --skip-did-web-verify On import: skip did:web resolution entirely.
41679
42576
  --json
41680
42577
  --yes, -y Explicit non-interactive Tier 1 approval
41681
42578
  --help, -h
42579
+
42580
+ did:web auto-inclusion (build 3):
42581
+ Running "sanctuary did-web issue --authority-host <host>" registers the
42582
+ operator's did:web identifier at <storage>/recognition/did-web.json.
42583
+ Subsequent "sanctuary exit export" runs auto-include this identifier in
42584
+ the manifest's identity_binding without requiring any --did-web flag.
42585
+ Per-fortress isolation is structural: the record lives under the
42586
+ fortress's storage_path, so different fortresses carry different
42587
+ registered identifiers.
41682
42588
  `);
41683
42589
  }
41684
42590
  async function runExitCommand(args) {
@@ -41777,12 +42683,15 @@ ${policyErr.message}
41777
42683
  throw policyErr;
41778
42684
  }
41779
42685
  const includeDidWebFlag = flagValue(argv, "--include-did-web");
41780
- const includeDidWebDisabled = includeDidWebFlag === "false";
42686
+ const explicitOptOut = hasFlag(argv, "--no-did-web") || includeDidWebFlag === "false";
41781
42687
  const didWebIdentifier = flagValue(argv, "--did-web");
41782
42688
  const didWebAuthorityHost = flagValue(argv, "--did-web-authority-host");
41783
42689
  const didWebPublishedAt = flagValue(argv, "--did-web-published-at");
41784
42690
  let exportDidWeb;
41785
- if (!includeDidWebDisabled && didWebIdentifier !== void 0) {
42691
+ let didWebSource;
42692
+ if (explicitOptOut) {
42693
+ didWebSource = "opted-out";
42694
+ } else if (didWebIdentifier !== void 0) {
41786
42695
  if (didWebAuthorityHost === void 0) {
41787
42696
  write(
41788
42697
  err,
@@ -41795,6 +42704,19 @@ ${policyErr.message}
41795
42704
  authority_host: didWebAuthorityHost,
41796
42705
  ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
41797
42706
  };
42707
+ didWebSource = "cli-override";
42708
+ } else {
42709
+ const record = await loadFortressDidWebRecord(ctx.storagePath);
42710
+ if (record !== null) {
42711
+ exportDidWeb = {
42712
+ identifier: record.identifier.did,
42713
+ authority_host: record.identifier.authority_host,
42714
+ ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
42715
+ };
42716
+ didWebSource = "fortress-config";
42717
+ } else {
42718
+ didWebSource = "no-record";
42719
+ }
41798
42720
  }
41799
42721
  const result = await exportExitBundle({
41800
42722
  bundleDir: outDir,
@@ -41810,12 +42732,42 @@ ${policyErr.message}
41810
42732
  keySource: ctx.keySource,
41811
42733
  ...exportDidWeb !== void 0 ? { didWeb: exportDidWeb } : {}
41812
42734
  });
41813
- if (json) write(out, JSON.stringify(result, null, 2) + "\n");
41814
- else {
42735
+ if (json) {
42736
+ write(
42737
+ out,
42738
+ JSON.stringify(
42739
+ { ...result, did_web_source: didWebSource },
42740
+ null,
42741
+ 2
42742
+ ) + "\n"
42743
+ );
42744
+ } else {
41815
42745
  write(out, `exported: ${result.bundle_dir}
41816
42746
  `);
41817
42747
  write(out, `manifest_hash: ${result.manifest_hash}
41818
42748
  `);
42749
+ if (didWebSource === "fortress-config" && exportDidWeb) {
42750
+ write(
42751
+ out,
42752
+ `did:web: auto-included from fortress config (${exportDidWeb.identifier})
42753
+ `
42754
+ );
42755
+ } else if (didWebSource === "cli-override" && exportDidWeb) {
42756
+ write(
42757
+ out,
42758
+ `did:web: included via CLI override (${exportDidWeb.identifier})
42759
+ `
42760
+ );
42761
+ } else if (didWebSource === "opted-out") {
42762
+ write(out, `did:web: skipped (operator opt-out via --no-did-web)
42763
+ `);
42764
+ } else if (didWebSource === "no-record") {
42765
+ write(
42766
+ out,
42767
+ `did:web: not included (no fortress config; run "sanctuary did-web issue" to register)
42768
+ `
42769
+ );
42770
+ }
41819
42771
  for (const item of result.unsupported_artifacts) {
41820
42772
  write(out, `unsupported: ${item}
41821
42773
  `);
@@ -42472,6 +43424,7 @@ ${err.message}
42472
43424
  await baseline.load();
42473
43425
  let approvalChannel;
42474
43426
  let dashboard;
43427
+ let intelligenceSelector;
42475
43428
  if (config.dashboard.enabled) {
42476
43429
  let authToken = config.dashboard.auth_token;
42477
43430
  if (authToken === "auto") {
@@ -42498,7 +43451,6 @@ ${err.message}
42498
43451
  profileStore
42499
43452
  });
42500
43453
  const embeddedHubIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
42501
- let intelligenceSelector;
42502
43454
  try {
42503
43455
  intelligenceSelector = new SubstrateSelector({
42504
43456
  storage,
@@ -42655,6 +43607,44 @@ ${err.message}
42655
43607
  workflowStateTracker
42656
43608
  });
42657
43609
  }
43610
+ const honeypotRegistry = new TrapRegistry();
43611
+ const honeypotStore = new TrapStore({
43612
+ storage,
43613
+ masterKey,
43614
+ fortressId: fortressIdForAggregator
43615
+ });
43616
+ try {
43617
+ const persistedSpecs = await honeypotStore.loadAll();
43618
+ for (const spec of persistedSpecs) {
43619
+ honeypotRegistry.deploy(spec);
43620
+ }
43621
+ if (persistedSpecs.length > 0) {
43622
+ auditLog.append(
43623
+ "l2",
43624
+ HONEYPOT_AUDIT_OPS.LOADED,
43625
+ aggregatorIdentityId,
43626
+ {
43627
+ fortress_id: fortressIdForAggregator,
43628
+ trap_count: persistedSpecs.length
43629
+ }
43630
+ );
43631
+ }
43632
+ } catch (err) {
43633
+ console.error(
43634
+ ` Note: honeypot trap store unavailable (${err.message}). Deployed traps from prior runs will not be restored; re-deploy via the management API.`
43635
+ );
43636
+ }
43637
+ if (dashboard) {
43638
+ dashboard.setHoneypotRegistry({
43639
+ registry: honeypotRegistry,
43640
+ findingStore: sentinelFindingStore,
43641
+ auditLog,
43642
+ operatorId: aggregatorIdentityId,
43643
+ fortressId: fortressIdForAggregator,
43644
+ ...intelligenceSelector ? { selector: intelligenceSelector } : {},
43645
+ store: honeypotStore
43646
+ });
43647
+ }
42658
43648
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
42659
43649
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
42660
43650
  config,