@sanctuary-framework/mcp-server 1.2.14 → 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/cli.cjs CHANGED
@@ -18388,6 +18388,552 @@ var init_handoff_routes = __esm({
18388
18388
  };
18389
18389
  }
18390
18390
  });
18391
+
18392
+ // src/honeypot/types.ts
18393
+ function honeypotSentinelId(trapId) {
18394
+ return `${HONEYPOT_SENTINEL_ID_PREFIX}${trapId}`;
18395
+ }
18396
+ var FILESYSTEM_OPS, HONEYPOT_AUDIT_OPS, HONEYPOT_SENTINEL_ID_PREFIX;
18397
+ var init_types3 = __esm({
18398
+ "src/honeypot/types.ts"() {
18399
+ FILESYSTEM_OPS = [
18400
+ "read",
18401
+ "write",
18402
+ "delete",
18403
+ "list"
18404
+ ];
18405
+ HONEYPOT_AUDIT_OPS = {
18406
+ DRAFTED: "honeypot_drafted",
18407
+ COMPILED: "honeypot_compiled",
18408
+ DEPLOYED: "honeypot_deployed",
18409
+ TRIGGERED: "honeypot_triggered",
18410
+ UNDEPLOYED: "honeypot_undeployed",
18411
+ LOADED: "honeypot_loaded"
18412
+ };
18413
+ HONEYPOT_SENTINEL_ID_PREFIX = "honeypot:";
18414
+ }
18415
+ });
18416
+ async function compileHoneypot(draft, opts) {
18417
+ const now = opts?.now ?? (() => /* @__PURE__ */ new Date());
18418
+ const trapIdFactory = opts?.trapIdFactory ?? (() => crypto.randomUUID());
18419
+ const warnings = [];
18420
+ let trigger = null;
18421
+ let trapClass = "http_endpoint";
18422
+ let severity = DEFAULT_SEVERITY;
18423
+ let explanation = "";
18424
+ let source = "heuristic";
18425
+ if (opts?.selector) {
18426
+ try {
18427
+ const handle = await opts.selector.getSubstrate(COMPILE_SURFACE);
18428
+ if (handle.capability.summarize) {
18429
+ const response = await opts.selector.invokeSummarize(
18430
+ COMPILE_SURFACE,
18431
+ {
18432
+ kind: "summarize",
18433
+ context: COMPILE_PROMPT,
18434
+ query: draft.english_text,
18435
+ maxTokens: COMPILE_MAX_TOKENS
18436
+ }
18437
+ );
18438
+ if (response.body.kind === "summarize" && !response.failureClass) {
18439
+ const parsed = tryParseLlmResponse(response.body.text);
18440
+ if (parsed.ok) {
18441
+ trigger = parsed.trigger;
18442
+ trapClass = parsed.trapClass;
18443
+ severity = parsed.severity;
18444
+ explanation = parsed.explanation;
18445
+ source = "llm";
18446
+ } else {
18447
+ warnings.push(
18448
+ `LLM response failed validation (${parsed.failure}); falling back to heuristic compile`
18449
+ );
18450
+ }
18451
+ } else {
18452
+ warnings.push(
18453
+ `LLM compile failed (${response.failureClass ?? "non_summarize_body"}); falling back to heuristic compile`
18454
+ );
18455
+ }
18456
+ } else {
18457
+ warnings.push(
18458
+ "Substrate at template-suggestion surface does not support summarize; falling back to heuristic compile"
18459
+ );
18460
+ }
18461
+ } catch (err) {
18462
+ const message = err instanceof Error ? err.message : String(err);
18463
+ warnings.push(
18464
+ `LLM compile threw (${message}); falling back to heuristic compile`
18465
+ );
18466
+ }
18467
+ }
18468
+ if (trigger === null) {
18469
+ const heuristic = heuristicCompile(draft.english_text);
18470
+ trigger = heuristic.trigger;
18471
+ trapClass = heuristic.trapClass;
18472
+ if (heuristic.severity) severity = heuristic.severity;
18473
+ explanation = heuristic.explanation;
18474
+ if (heuristic.warning) warnings.push(heuristic.warning);
18475
+ }
18476
+ const spec = {
18477
+ trap_id: trapIdFactory(),
18478
+ trap_class: trapClass,
18479
+ trigger,
18480
+ finding_severity: severity,
18481
+ english_text: draft.english_text,
18482
+ explanation_paragraph: explanation,
18483
+ compiled_at: now().toISOString()
18484
+ };
18485
+ return { spec, source, warnings };
18486
+ }
18487
+ function tryParseLlmResponse(text) {
18488
+ let body;
18489
+ try {
18490
+ const stripped = text.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
18491
+ body = JSON.parse(stripped);
18492
+ } catch {
18493
+ return { ok: false, failure: "invalid_json" };
18494
+ }
18495
+ if (!body || typeof body !== "object") {
18496
+ return { ok: false, failure: "invalid_json" };
18497
+ }
18498
+ const obj = body;
18499
+ const pathPattern = obj["path_pattern"];
18500
+ if (typeof pathPattern !== "string" || pathPattern.length === 0) {
18501
+ return { ok: false, failure: "missing_path_pattern" };
18502
+ }
18503
+ const callerTypes = Array.isArray(obj["expected_caller_types"]) ? obj["expected_caller_types"].filter(
18504
+ (v) => typeof v === "string" && v.length > 0
18505
+ ) : ["wrapped_agent"];
18506
+ if (callerTypes.length === 0) {
18507
+ return { ok: false, failure: "invalid_caller_types" };
18508
+ }
18509
+ const severityRaw = obj["finding_severity"];
18510
+ const severity = severityRaw === "warn" ? "warn" : severityRaw === "alert" ? "alert" : DEFAULT_SEVERITY;
18511
+ const explanationRaw = obj["explanation_paragraph"];
18512
+ const explanation = typeof explanationRaw === "string" && explanationRaw.length > 0 ? explanationRaw : "Honeypot compiled from operator draft via LLM-assisted compile path.";
18513
+ const trapClassRaw = obj["trap_class"];
18514
+ const trapClass = trapClassRaw === "filesystem" ? "filesystem" : "http_endpoint";
18515
+ if (trapClass === "filesystem") {
18516
+ const opsParsed = parseFilesystemOps(obj["ops"]);
18517
+ if (opsParsed === null) {
18518
+ return { ok: false, failure: "invalid_filesystem_ops" };
18519
+ }
18520
+ const trigger2 = {
18521
+ kind: "filesystem",
18522
+ path_pattern: pathPattern,
18523
+ ops: opsParsed,
18524
+ expected_caller_types: callerTypes
18525
+ };
18526
+ return { ok: true, trapClass, trigger: trigger2, severity, explanation };
18527
+ }
18528
+ const method = typeof obj["method"] === "string" ? obj["method"] : "ANY";
18529
+ const trigger = {
18530
+ kind: "http_endpoint",
18531
+ path_pattern: pathPattern,
18532
+ ...method !== "ANY" ? { method: method.toUpperCase() } : {},
18533
+ expected_caller_types: callerTypes
18534
+ };
18535
+ return { ok: true, trapClass, trigger, severity, explanation };
18536
+ }
18537
+ function parseFilesystemOps(raw) {
18538
+ if (raw === void 0 || raw === null) {
18539
+ return [...FILESYSTEM_OPS];
18540
+ }
18541
+ if (!Array.isArray(raw)) return null;
18542
+ if (raw.length === 0) return [...FILESYSTEM_OPS];
18543
+ const out = [];
18544
+ for (const entry of raw) {
18545
+ if (typeof entry !== "string") return null;
18546
+ if (!FILESYSTEM_OPS.includes(entry)) return null;
18547
+ if (!out.includes(entry)) out.push(entry);
18548
+ }
18549
+ return out;
18550
+ }
18551
+ function heuristicCompile(english) {
18552
+ let pathPattern = null;
18553
+ for (const re of HEURISTIC_PATH_PATTERNS) {
18554
+ const match = english.match(re);
18555
+ if (match && match[1]) {
18556
+ pathPattern = match[1];
18557
+ break;
18558
+ }
18559
+ }
18560
+ const fallbackUsed = pathPattern === null;
18561
+ if (pathPattern === null) {
18562
+ pathPattern = "/honeypot-stub";
18563
+ }
18564
+ let severity;
18565
+ for (const hint of SEVERITY_HINTS) {
18566
+ if (hint.phrase.test(english)) {
18567
+ severity = hint.severity;
18568
+ break;
18569
+ }
18570
+ }
18571
+ const isFilesystem = FILESYSTEM_CLASS_HINTS.some((re) => re.test(english));
18572
+ if (isFilesystem) {
18573
+ const ops = [];
18574
+ for (const hint of FILESYSTEM_OP_HINTS) {
18575
+ if (hint.phrase.test(english) && !ops.includes(hint.op)) {
18576
+ ops.push(hint.op);
18577
+ }
18578
+ }
18579
+ const resolvedOps = ops.length > 0 ? ops : [...FILESYSTEM_OPS];
18580
+ const trigger2 = {
18581
+ kind: "filesystem",
18582
+ path_pattern: pathPattern,
18583
+ ops: resolvedOps,
18584
+ expected_caller_types: ["wrapped_agent"]
18585
+ };
18586
+ const opsRendered = resolvedOps.join(",");
18587
+ 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}.`;
18588
+ return {
18589
+ trapClass: "filesystem",
18590
+ trigger: trigger2,
18591
+ ...severity !== void 0 ? { severity } : {},
18592
+ explanation: explanation2,
18593
+ ...fallbackUsed ? {
18594
+ 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"
18595
+ } : {}
18596
+ };
18597
+ }
18598
+ const trigger = {
18599
+ kind: "http_endpoint",
18600
+ path_pattern: pathPattern,
18601
+ expected_caller_types: ["wrapped_agent"]
18602
+ };
18603
+ 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}.`;
18604
+ return {
18605
+ trapClass: "http_endpoint",
18606
+ trigger,
18607
+ ...severity !== void 0 ? { severity } : {},
18608
+ explanation,
18609
+ ...fallbackUsed ? {
18610
+ 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"
18611
+ } : {}
18612
+ };
18613
+ }
18614
+ function hashOfEnglishDraft(text) {
18615
+ return crypto.createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32);
18616
+ }
18617
+ var COMPILE_SURFACE, COMPILE_MAX_TOKENS, DEFAULT_SEVERITY, COMPILE_PROMPT, HEURISTIC_PATH_PATTERNS, SEVERITY_HINTS, FILESYSTEM_CLASS_HINTS, FILESYSTEM_OP_HINTS;
18618
+ var init_honeypot_compiler = __esm({
18619
+ "src/honeypot/honeypot-compiler.ts"() {
18620
+ init_types3();
18621
+ COMPILE_SURFACE = "template-suggestion";
18622
+ COMPILE_MAX_TOKENS = 800;
18623
+ DEFAULT_SEVERITY = "alert";
18624
+ COMPILE_PROMPT = `You are compiling a Sanctuary honeypot from an operator's plain-English description.
18625
+ Return STRICT JSON with the following shape (no markdown, no commentary):
18626
+ {
18627
+ "trap_class": "http_endpoint" | "filesystem",
18628
+ "path_pattern": "string (glob with * or **)",
18629
+ "method": "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY",
18630
+ "ops": ["read", "write", "delete", "list"],
18631
+ "expected_caller_types": ["wrapped_agent" | "operator" | "external"],
18632
+ "finding_severity": "warn" | "alert",
18633
+ "explanation_paragraph": "one-sentence operator-friendly explanation"
18634
+ }
18635
+ 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.`;
18636
+ HEURISTIC_PATH_PATTERNS = [
18637
+ /honeypot\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
18638
+ /trap\s+(?:at|on|for)\s+([\/][\w\/\-:*\.]+)/i,
18639
+ /deploy\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
18640
+ /catch\s+(?:requests?\s+to|callers?\s+at)\s+([\/][\w\/\-:*\.]+)/i,
18641
+ /watch\s+(?:for\s+)?(?:requests?\s+(?:to|on))\s+([\/][\w\/\-:*\.]+)/i,
18642
+ /([\/][\w\/\-:*\.]+)\s+(?:endpoint|path|route)/i
18643
+ ];
18644
+ SEVERITY_HINTS = [
18645
+ { phrase: /\b(?:warn|warning|low\s+severity)\b/i, severity: "warn" },
18646
+ { phrase: /\b(?:alert|critical|high\s+severity)\b/i, severity: "alert" }
18647
+ ];
18648
+ FILESYSTEM_CLASS_HINTS = [
18649
+ /\bfilesystem\b/i,
18650
+ /\bfile[-\s]?system\b/i,
18651
+ /\bfile\s+(?:read|write|delete|list|access|trap|honeypot)/i,
18652
+ /\b(?:read|write|delete|list)\s+file/i,
18653
+ /\bdirectory\b/i,
18654
+ /\bon[-\s]?disk\b/i,
18655
+ /\bpath\s+on\s+disk\b/i
18656
+ ];
18657
+ FILESYSTEM_OP_HINTS = [
18658
+ { phrase: /\b(?:read|reads|reading|access(?:es|ed)?)\b/i, op: "read" },
18659
+ { phrase: /\b(?:write|writes|writing|modif(?:y|ies|ied)|edit)/i, op: "write" },
18660
+ { phrase: /\b(?:delete|deletes|deletion|remove|removal|unlink)/i, op: "delete" },
18661
+ { phrase: /\b(?:list|listing|enumerate|enumeration|directory\s+listing)/i, op: "list" }
18662
+ ];
18663
+ }
18664
+ });
18665
+ async function handleHoneypotTriggerIfMatch(deps, req, res) {
18666
+ const url = req.url ?? "/";
18667
+ const path = url.split("?")[0] ?? "/";
18668
+ const method = (req.method ?? "GET").toUpperCase();
18669
+ if (path.startsWith(HONEYPOT_API_PREFIX)) return false;
18670
+ if (path.startsWith("/api/sentinels")) return false;
18671
+ if (path.startsWith("/api/coordination")) return false;
18672
+ const match = deps.registry.findMatching({ path, method });
18673
+ if (!match) return false;
18674
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
18675
+ const callerIdentity = extractCallerIdentity(req);
18676
+ const payloadHash = await safeReadAndHashBody(req);
18677
+ const findingId = crypto.randomUUID();
18678
+ const finding = {
18679
+ finding_id: findingId,
18680
+ sentinel_id: honeypotSentinelId(match.trap_id),
18681
+ severity: match.finding_severity,
18682
+ summary: buildSummary(match, callerIdentity, path, method),
18683
+ details: {
18684
+ trap_id: match.trap_id,
18685
+ trap_class: match.trap_class,
18686
+ path_matched: path,
18687
+ method,
18688
+ caller_identity: callerIdentity,
18689
+ payload_hash: payloadHash
18690
+ },
18691
+ observed_at: now.toISOString(),
18692
+ evidence_audit_ids: [],
18693
+ fortress_id: deps.fortressId
18694
+ };
18695
+ await deps.findingStore.saveFinding(finding).catch(() => void 0);
18696
+ deps.auditLog.append(
18697
+ "l2",
18698
+ HONEYPOT_AUDIT_OPS.TRIGGERED,
18699
+ deps.operatorId,
18700
+ {
18701
+ trap_id: match.trap_id,
18702
+ trap_class: match.trap_class,
18703
+ path_matched: path,
18704
+ method,
18705
+ caller_identity: callerIdentity,
18706
+ payload_hash: payloadHash,
18707
+ finding_id: findingId,
18708
+ severity: match.finding_severity
18709
+ }
18710
+ );
18711
+ res.writeHead(404, { "Content-Type": "application/json" });
18712
+ res.end(JSON.stringify({ error: "not_found", path }));
18713
+ return true;
18714
+ }
18715
+ function buildSummary(spec, callerIdentity, path, method) {
18716
+ return `honeypot ${spec.trap_id} triggered: ${method} ${path} from ${callerIdentity} (severity ${spec.finding_severity}, pattern ${spec.trigger.path_pattern})`;
18717
+ }
18718
+ function extractCallerIdentity(req) {
18719
+ const headers = req.headers;
18720
+ const agent = headers["x-sanctuary-agent"];
18721
+ if (typeof agent === "string" && agent.length > 0) return `agent:${agent}`;
18722
+ const xff = headers["x-forwarded-for"];
18723
+ if (typeof xff === "string" && xff.length > 0) {
18724
+ const first = xff.split(",")[0]?.trim();
18725
+ if (first) return `ip:${first}`;
18726
+ }
18727
+ const ip = req.socket.remoteAddress;
18728
+ return ip ? `ip:${ip}` : "ip:unknown";
18729
+ }
18730
+ async function safeReadAndHashBody(req) {
18731
+ const MAX_BYTES = 64 * 1024;
18732
+ try {
18733
+ const chunks = [];
18734
+ let total = 0;
18735
+ for await (const chunk of req) {
18736
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
18737
+ total += buf.length;
18738
+ if (total > MAX_BYTES) {
18739
+ return "unhashed";
18740
+ }
18741
+ chunks.push(buf);
18742
+ }
18743
+ if (chunks.length === 0) return "empty";
18744
+ const body = Buffer.concat(chunks);
18745
+ return crypto.createHash("sha256").update(body).digest("hex").slice(0, 32);
18746
+ } catch {
18747
+ return "unhashed";
18748
+ }
18749
+ }
18750
+ function writeJSON7(res, status, payload) {
18751
+ res.writeHead(status, {
18752
+ "Content-Type": "application/json",
18753
+ "Cache-Control": "no-store"
18754
+ });
18755
+ res.end(JSON.stringify(payload));
18756
+ }
18757
+ async function readJSONBody4(req) {
18758
+ const chunks = [];
18759
+ for await (const chunk of req) {
18760
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
18761
+ }
18762
+ if (chunks.length === 0) return void 0;
18763
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
18764
+ }
18765
+ function matchTrapIdRoute(path) {
18766
+ const prefix = `${HONEYPOT_API_PREFIX}/traps/`;
18767
+ if (!path.startsWith(prefix)) return null;
18768
+ const rest = path.slice(prefix.length);
18769
+ if (rest.length === 0 || rest.includes("/")) return null;
18770
+ return { trapId: decodeURIComponent(rest) };
18771
+ }
18772
+ async function handleHoneypotRoute(deps, req, res) {
18773
+ const host = req.headers.host || "localhost";
18774
+ const url = new URL(req.url ?? "/", `http://${host}`);
18775
+ const method = (req.method ?? "GET").toUpperCase();
18776
+ const path = url.pathname;
18777
+ if (path !== HONEYPOT_API_PREFIX && !path.startsWith(`${HONEYPOT_API_PREFIX}/`)) {
18778
+ return false;
18779
+ }
18780
+ const checkAuth = authMiddleware(deps.authConfig);
18781
+ if (!checkAuth(req, res, url)) return true;
18782
+ try {
18783
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/compile`) {
18784
+ const body = await readJSONBody4(req);
18785
+ const englishText = body && typeof body === "object" && typeof body["english_text"] === "string" ? body["english_text"] : "";
18786
+ if (englishText.length === 0) {
18787
+ writeJSON7(res, 400, {
18788
+ ok: false,
18789
+ error: "english_text required"
18790
+ });
18791
+ return true;
18792
+ }
18793
+ const draft = {
18794
+ english_text: englishText,
18795
+ operator_id: deps.operatorId,
18796
+ observed_at: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
18797
+ };
18798
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DRAFTED, deps.operatorId, {
18799
+ fortress_id: deps.fortressId,
18800
+ english_hash: hashOfEnglishDraft(englishText)
18801
+ });
18802
+ const result = await compileHoneypot(draft, {
18803
+ ...deps.selector !== void 0 ? { selector: deps.selector } : {},
18804
+ ...deps.now !== void 0 ? { now: deps.now } : {}
18805
+ });
18806
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.COMPILED, deps.operatorId, {
18807
+ fortress_id: deps.fortressId,
18808
+ trap_id: result.spec.trap_id,
18809
+ source: result.source,
18810
+ warning_count: result.warnings.length
18811
+ });
18812
+ writeJSON7(res, 200, {
18813
+ ok: true,
18814
+ data: { spec: result.spec, source: result.source, warnings: result.warnings }
18815
+ });
18816
+ return true;
18817
+ }
18818
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/deploy`) {
18819
+ const body = await readJSONBody4(req);
18820
+ const spec = body && typeof body === "object" && body["spec"] ? body["spec"] : null;
18821
+ if (!spec || typeof spec.trap_id !== "string" || spec.trap_id.length === 0) {
18822
+ writeJSON7(res, 400, { ok: false, error: "spec.trap_id required" });
18823
+ return true;
18824
+ }
18825
+ const isNew = deps.registry.deploy(spec);
18826
+ let persistError = null;
18827
+ if (deps.store) {
18828
+ try {
18829
+ await deps.store.save(spec);
18830
+ } catch (err) {
18831
+ persistError = err instanceof Error ? err.message : String(err);
18832
+ }
18833
+ }
18834
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DEPLOYED, deps.operatorId, {
18835
+ fortress_id: deps.fortressId,
18836
+ trap_id: spec.trap_id,
18837
+ trap_class: spec.trap_class,
18838
+ path_pattern: spec.trigger.path_pattern,
18839
+ was_new: isNew,
18840
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
18841
+ });
18842
+ writeJSON7(res, 200, {
18843
+ ok: true,
18844
+ data: {
18845
+ trap_id: spec.trap_id,
18846
+ was_new: isNew,
18847
+ ...deps.store ? { persisted: persistError === null } : {},
18848
+ ...persistError !== null ? { persist_error: persistError } : {}
18849
+ }
18850
+ });
18851
+ return true;
18852
+ }
18853
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/traps`) {
18854
+ const traps = deps.registry.list();
18855
+ writeJSON7(res, 200, { ok: true, data: { traps } });
18856
+ return true;
18857
+ }
18858
+ const trapMatch = matchTrapIdRoute(path);
18859
+ if (method === "DELETE" && trapMatch) {
18860
+ const removed = deps.registry.undeploy(trapMatch.trapId);
18861
+ let persistError = null;
18862
+ if (deps.store) {
18863
+ try {
18864
+ await deps.store.delete(trapMatch.trapId);
18865
+ } catch (err) {
18866
+ persistError = err instanceof Error ? err.message : String(err);
18867
+ }
18868
+ }
18869
+ if (removed) {
18870
+ deps.auditLog.append(
18871
+ "l2",
18872
+ HONEYPOT_AUDIT_OPS.UNDEPLOYED,
18873
+ deps.operatorId,
18874
+ {
18875
+ fortress_id: deps.fortressId,
18876
+ trap_id: trapMatch.trapId,
18877
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
18878
+ }
18879
+ );
18880
+ }
18881
+ writeJSON7(res, removed ? 200 : 404, {
18882
+ ok: removed,
18883
+ data: {
18884
+ trap_id: trapMatch.trapId,
18885
+ removed,
18886
+ ...deps.store ? { persisted: persistError === null } : {},
18887
+ ...persistError !== null ? { persist_error: persistError } : {}
18888
+ }
18889
+ });
18890
+ return true;
18891
+ }
18892
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/findings`) {
18893
+ const since = url.searchParams.get("since") ?? void 0;
18894
+ const severityRaw = url.searchParams.get("severity") ?? void 0;
18895
+ const severity = isValidSeverity(severityRaw) ? severityRaw : void 0;
18896
+ const limit = parseLimit5(url.searchParams.get("limit"), 50, 500);
18897
+ const all = await deps.findingStore.listFindings({
18898
+ ...since !== void 0 ? { since } : {},
18899
+ ...severity !== void 0 ? { severity } : {},
18900
+ limit: 500
18901
+ });
18902
+ const honeypotFindings = all.filter(
18903
+ (f) => f.sentinel_id.startsWith(HONEYPOT_SENTINEL_ID_PREFIX)
18904
+ );
18905
+ writeJSON7(res, 200, {
18906
+ ok: true,
18907
+ data: { findings: honeypotFindings.slice(0, limit) }
18908
+ });
18909
+ return true;
18910
+ }
18911
+ writeJSON7(res, 404, { ok: false, error: "not_found", path });
18912
+ return true;
18913
+ } catch (err) {
18914
+ const msg = err instanceof Error ? err.message : String(err);
18915
+ writeJSON7(res, 500, { ok: false, error: "internal", detail: msg });
18916
+ return true;
18917
+ }
18918
+ }
18919
+ function isValidSeverity(value) {
18920
+ return value === "info" || value === "warn" || value === "alert";
18921
+ }
18922
+ function parseLimit5(raw, defaultValue, max) {
18923
+ if (raw === null || raw === "") return defaultValue;
18924
+ const parsed = Number.parseInt(raw, 10);
18925
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
18926
+ return Math.min(parsed, max);
18927
+ }
18928
+ var HONEYPOT_API_PREFIX;
18929
+ var init_runtime_trap_handler = __esm({
18930
+ "src/honeypot/runtime-trap-handler.ts"() {
18931
+ init_auth_middleware();
18932
+ init_types3();
18933
+ init_honeypot_compiler();
18934
+ HONEYPOT_API_PREFIX = "/api/honeypot";
18935
+ }
18936
+ });
18391
18937
  function isDashboardViewRoute(method, path) {
18392
18938
  if (method !== "GET") return false;
18393
18939
  return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
@@ -18404,6 +18950,7 @@ var init_dashboard = __esm({
18404
18950
  init_approval_aggregator_routes();
18405
18951
  init_sentinel_routes();
18406
18952
  init_handoff_routes();
18953
+ init_runtime_trap_handler();
18407
18954
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
18408
18955
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
18409
18956
  MAX_SESSIONS = 1e3;
@@ -18491,6 +19038,22 @@ var init_dashboard = __esm({
18491
19038
  workflowStateTracker = null;
18492
19039
  handoffAuditLog = null;
18493
19040
  handoffOperatorId = null;
19041
+ // v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: per-fortress trap registry
19042
+ // + finding store + audit log + operator id. Front-of-dispatch hook
19043
+ // consults the registry on every request; management routes at
19044
+ // /api/honeypot/* go through the dispatch path.
19045
+ honeypotRegistry = null;
19046
+ honeypotFindingStore = null;
19047
+ honeypotAuditLog = null;
19048
+ honeypotOperatorId = null;
19049
+ honeypotFortressId = null;
19050
+ honeypotSelector = null;
19051
+ // Pi-2: encrypted at-rest persistence for deployed honeypot traps.
19052
+ // When present, the management API's deploy + undeploy handlers
19053
+ // write through to the store; on fortress boot the host code calls
19054
+ // `store.loadAll()` and re-deploys the persisted specs into the
19055
+ // in-memory registry before this dashboard begins serving.
19056
+ honeypotStore = null;
18494
19057
  constructor(config) {
18495
19058
  this.config = config;
18496
19059
  this.authToken = config.auth_token;
@@ -18572,6 +19135,30 @@ var init_dashboard = __esm({
18572
19135
  this.handoffContextTransfer = opts.contextTransfer ?? null;
18573
19136
  this.workflowStateTracker = opts.workflowStateTracker ?? null;
18574
19137
  }
19138
+ /**
19139
+ * v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: bind the per-fortress
19140
+ * trap registry + finding store + audit log + operator id. Once
19141
+ * set, two surfaces activate:
19142
+ * 1. Front-of-dispatch trap-trigger hook: every request runs
19143
+ * through `handleHoneypotTriggerIfMatch` BEFORE legacy/v1.1/
19144
+ * sentinel/coordination routing. Matching traps return 404
19145
+ * and the request never reaches the regular dispatcher.
19146
+ * 2. Management API at /api/honeypot/* routes through
19147
+ * `handleHoneypotRoute`.
19148
+ *
19149
+ * The optional `selector` opt wires the LLM compile path; absent
19150
+ * selector forces the heuristic compile path (which still produces
19151
+ * a usable TrapSpec with warnings).
19152
+ */
19153
+ setHoneypotRegistry(opts) {
19154
+ this.honeypotRegistry = opts.registry;
19155
+ this.honeypotFindingStore = opts.findingStore ?? null;
19156
+ this.honeypotAuditLog = opts.auditLog ?? null;
19157
+ this.honeypotOperatorId = opts.operatorId ?? null;
19158
+ this.honeypotFortressId = opts.fortressId ?? null;
19159
+ this.honeypotSelector = opts.selector ?? null;
19160
+ this.honeypotStore = opts.store ?? null;
19161
+ }
18575
19162
  /**
18576
19163
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
18577
19164
  * before the legacy approval route table. Returns true when served.
@@ -18636,6 +19223,57 @@ var init_dashboard = __esm({
18636
19223
  res
18637
19224
  );
18638
19225
  }
19226
+ /**
19227
+ * v1.3 WP-V1.3-5 Pi-1 dispatch entry point. Routes
19228
+ * `/api/honeypot/*` requests through the honeypot management
19229
+ * router when a registry has been bound. Returns true when served.
19230
+ */
19231
+ async dispatchHoneypot(req, res) {
19232
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
19233
+ return false;
19234
+ }
19235
+ return handleHoneypotRoute(
19236
+ {
19237
+ authConfig: {
19238
+ loopbackAutoAuth: this._autoAuthLocalhost,
19239
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
19240
+ },
19241
+ registry: this.honeypotRegistry,
19242
+ findingStore: this.honeypotFindingStore,
19243
+ auditLog: this.honeypotAuditLog,
19244
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
19245
+ fortressId: this.honeypotFortressId ?? "fortress_default",
19246
+ ...this.honeypotSelector !== null ? { selector: this.honeypotSelector } : {},
19247
+ ...this.honeypotStore !== null ? { store: this.honeypotStore } : {}
19248
+ },
19249
+ req,
19250
+ res
19251
+ );
19252
+ }
19253
+ /**
19254
+ * v1.3 WP-V1.3-5 Pi-1 front-of-dispatch trap-trigger hook. Examines
19255
+ * every request BEFORE legacy/v1.1/sentinel/coordination routing.
19256
+ * Returns true when a deployed trap matched the request and the
19257
+ * handler emitted the audit event + sentinel finding + plausible
19258
+ * 404 response. Returns false when no trap matched; caller
19259
+ * continues with normal routing.
19260
+ */
19261
+ async dispatchHoneypotTrap(req, res) {
19262
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
19263
+ return false;
19264
+ }
19265
+ return handleHoneypotTriggerIfMatch(
19266
+ {
19267
+ registry: this.honeypotRegistry,
19268
+ findingStore: this.honeypotFindingStore,
19269
+ auditLog: this.honeypotAuditLog,
19270
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
19271
+ fortressId: this.honeypotFortressId ?? "fortress_default"
19272
+ },
19273
+ req,
19274
+ res
19275
+ );
19276
+ }
18639
19277
  /**
18640
19278
  * v1.1 dispatch entry point. Called from `handleRequest` before the
18641
19279
  * legacy route table. Returns true when the request was served by v1.1
@@ -19011,6 +19649,40 @@ var init_dashboard = __esm({
19011
19649
  res.end();
19012
19650
  return;
19013
19651
  }
19652
+ if (this.honeypotRegistry) {
19653
+ this.dispatchHoneypotTrap(req, res).then((handled) => {
19654
+ if (handled) return;
19655
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
19656
+ }).catch(() => {
19657
+ if (!res.headersSent) {
19658
+ res.writeHead(500, { "Content-Type": "application/json" });
19659
+ res.end(JSON.stringify({ error: "Internal server error" }));
19660
+ }
19661
+ });
19662
+ return;
19663
+ }
19664
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
19665
+ }
19666
+ /**
19667
+ * v1.3 WP-V1.3-5 Pi-1: post-honeypot-trap request continuation. The
19668
+ * front-of-dispatch trap-trigger hook may short-circuit a request;
19669
+ * when it does not, this method runs the original dispatch ladder.
19670
+ * Pulled out as a helper so the trap-hook + non-trap paths share
19671
+ * one code path through every downstream dispatcher.
19672
+ */
19673
+ continueHandleRequest(req, res, url, method, _origin, _selfOrigin) {
19674
+ if (this.honeypotRegistry && url.pathname.startsWith(HONEYPOT_API_PREFIX)) {
19675
+ this.dispatchHoneypot(req, res).then((handled) => {
19676
+ if (handled) return;
19677
+ this.handleLegacyRequest(req, res, url, method);
19678
+ }).catch(() => {
19679
+ if (!res.headersSent) {
19680
+ res.writeHead(500, { "Content-Type": "application/json" });
19681
+ res.end(JSON.stringify({ error: "Internal server error" }));
19682
+ }
19683
+ });
19684
+ return;
19685
+ }
19014
19686
  if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
19015
19687
  this.dispatchApprovalInbox(req, res).then((handled) => {
19016
19688
  if (handled) return;
@@ -22443,7 +23115,7 @@ function proxyServerFromAuditEntry(entry) {
22443
23115
  return server;
22444
23116
  }
22445
23117
  var SENTINEL_SUMMARY_MAX_CHARS, SENTINEL_AUDIT_OPS, SENTINEL_OBSERVED_AUDIT_OPS;
22446
- var init_types3 = __esm({
23118
+ var init_types4 = __esm({
22447
23119
  "src/sentinel/types.ts"() {
22448
23120
  SENTINEL_SUMMARY_MAX_CHARS = 240;
22449
23121
  SENTINEL_AUDIT_OPS = {
@@ -22477,7 +23149,7 @@ var init_sentinel_finding_store = __esm({
22477
23149
  init_encryption();
22478
23150
  init_key_derivation();
22479
23151
  init_encoding();
22480
- init_types3();
23152
+ init_types4();
22481
23153
  SENTINEL_FINDING_NAMESPACE = "_sentinel_findings";
22482
23154
  SENTINEL_FINDING_KEY_PREFIX = "finding.";
22483
23155
  HKDF_INFO2 = "l2-sentinel-finding-v1";
@@ -22720,7 +23392,7 @@ var init_sentinel_registry = __esm({
22720
23392
  var DEFAULT_TICK_INTERVAL_MS, SentinelDispatcher;
22721
23393
  var init_sentinel_dispatcher = __esm({
22722
23394
  "src/sentinel/sentinel-dispatcher.ts"() {
22723
- init_types3();
23395
+ init_types4();
22724
23396
  DEFAULT_TICK_INTERVAL_MS = 6e4;
22725
23397
  SentinelDispatcher = class {
22726
23398
  registry;
@@ -23086,7 +23758,7 @@ function formatAnomalySummary(detector, classifier, vector, prediction, severity
23086
23758
  return `${detector.detectorId}/${classifier.classifierId} ${severity}: agent ${vector.agent_id} drifted ${prediction.anomaly_score.toFixed(2)} sigma from baseline. Top contributors: ${top || "(none)"}.`;
23087
23759
  }
23088
23760
  var AnomalyDetector, ANOMALY_SENTINEL_ID_PREFIX;
23089
- var init_types4 = __esm({
23761
+ var init_types5 = __esm({
23090
23762
  "src/anomaly-detection/types.ts"() {
23091
23763
  AnomalyDetector = class {
23092
23764
  /**
@@ -23214,7 +23886,7 @@ var init_anomaly_pipeline = __esm({
23214
23886
  "src/anomaly-detection/anomaly-pipeline.ts"() {
23215
23887
  init_cusum();
23216
23888
  init_psi();
23217
- init_types4();
23889
+ init_types5();
23218
23890
  ANOMALY_AUDIT_OPS = {
23219
23891
  DETECTOR_REGISTERED: "anomaly_detector_registered",
23220
23892
  DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
@@ -23577,6 +24249,198 @@ var init_workflow_state_tracker = __esm({
23577
24249
  }
23578
24250
  });
23579
24251
 
24252
+ // src/honeypot/trap-registry.ts
24253
+ function matchesTrap(spec, input) {
24254
+ if (spec.trigger.kind !== "http_endpoint") return false;
24255
+ const trigger = spec.trigger;
24256
+ if (trigger.method && trigger.method.toUpperCase() !== input.method.toUpperCase()) {
24257
+ return false;
24258
+ }
24259
+ const re = compileGlob(trigger.path_pattern);
24260
+ return re.test(input.path);
24261
+ }
24262
+ function compileGlob(pattern) {
24263
+ let out = "";
24264
+ let i = 0;
24265
+ while (i < pattern.length) {
24266
+ const ch = pattern[i];
24267
+ if (ch === "*" && pattern[i + 1] === "*") {
24268
+ out += ".*";
24269
+ i += 2;
24270
+ continue;
24271
+ }
24272
+ if (ch === "*") {
24273
+ out += "[^/]*";
24274
+ i += 1;
24275
+ continue;
24276
+ }
24277
+ if ("\\^$.|?+()[]{}".includes(ch)) {
24278
+ out += `\\${ch}`;
24279
+ } else {
24280
+ out += ch;
24281
+ }
24282
+ i += 1;
24283
+ }
24284
+ return new RegExp(`^${out}$`);
24285
+ }
24286
+ var TrapRegistry;
24287
+ var init_trap_registry = __esm({
24288
+ "src/honeypot/trap-registry.ts"() {
24289
+ TrapRegistry = class {
24290
+ traps = /* @__PURE__ */ new Map();
24291
+ /**
24292
+ * Deploy a trap. Idempotent on `trap_id`: re-deploying replaces the
24293
+ * previous spec for that id. Returns true on first deploy, false on
24294
+ * re-deploy (so callers can branch audit emission).
24295
+ */
24296
+ deploy(spec) {
24297
+ const isNew = !this.traps.has(spec.trap_id);
24298
+ this.traps.set(spec.trap_id, spec);
24299
+ return isNew;
24300
+ }
24301
+ /**
24302
+ * Undeploy by trap_id. Returns true when a trap was removed, false
24303
+ * when no trap had that id (idempotent).
24304
+ */
24305
+ undeploy(trapId) {
24306
+ return this.traps.delete(trapId);
24307
+ }
24308
+ /** List deployed traps. Returns a fresh array; mutation is safe. */
24309
+ list() {
24310
+ return [...this.traps.values()];
24311
+ }
24312
+ /** Look up a single trap by id. */
24313
+ get(trapId) {
24314
+ return this.traps.get(trapId);
24315
+ }
24316
+ /**
24317
+ * Find the first trap matching the request. Iteration order is
24318
+ * insertion order; operators who deploy multiple overlapping traps
24319
+ * see the earliest-deployed one fire. Tests cover this contract.
24320
+ */
24321
+ findMatching(input) {
24322
+ for (const spec of this.traps.values()) {
24323
+ if (matchesTrap(spec, input)) return spec;
24324
+ }
24325
+ return void 0;
24326
+ }
24327
+ /** Drop every trap. Tests use this between runs; not surfaced via API. */
24328
+ clear() {
24329
+ this.traps.clear();
24330
+ }
24331
+ };
24332
+ }
24333
+ });
24334
+
24335
+ // src/honeypot/trap-store.ts
24336
+ function trapKey(trapId) {
24337
+ return `${TRAP_STORE_KEY_PREFIX}${trapId}`;
24338
+ }
24339
+ function stripKeyPrefix3(key) {
24340
+ if (!key.startsWith(TRAP_STORE_KEY_PREFIX)) return null;
24341
+ return key.slice(TRAP_STORE_KEY_PREFIX.length);
24342
+ }
24343
+ var TRAP_STORE_NAMESPACE, TRAP_STORE_KEY_PREFIX, HKDF_INFO4, MAX_TRAP_BYTES, TrapStore;
24344
+ var init_trap_store = __esm({
24345
+ "src/honeypot/trap-store.ts"() {
24346
+ init_encryption();
24347
+ init_key_derivation();
24348
+ init_encoding();
24349
+ TRAP_STORE_NAMESPACE = "_honeypot_traps";
24350
+ TRAP_STORE_KEY_PREFIX = "trap.";
24351
+ HKDF_INFO4 = "l2-honeypot-trap-v1";
24352
+ MAX_TRAP_BYTES = 64 * 1024;
24353
+ TrapStore = class {
24354
+ storage;
24355
+ encryptionKey;
24356
+ fortressId;
24357
+ constructor(opts) {
24358
+ this.storage = opts.storage;
24359
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
24360
+ this.fortressId = opts.fortressId;
24361
+ }
24362
+ /** Persist (or overwrite) one trap. Returns the trap_id on success. */
24363
+ async save(spec) {
24364
+ const persisted = { version: 1, spec };
24365
+ const aad = stringToBytes(spec.trap_id);
24366
+ const plaintext = stringToBytes(JSON.stringify(persisted));
24367
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
24368
+ await this.storage.write(
24369
+ TRAP_STORE_NAMESPACE,
24370
+ trapKey(spec.trap_id),
24371
+ stringToBytes(JSON.stringify(envelope))
24372
+ );
24373
+ return spec.trap_id;
24374
+ }
24375
+ /**
24376
+ * Remove one trap by id. Returns true when a record was removed,
24377
+ * false when no record existed (idempotent).
24378
+ */
24379
+ async delete(trapId) {
24380
+ try {
24381
+ const raw = await this.storage.read(
24382
+ TRAP_STORE_NAMESPACE,
24383
+ trapKey(trapId)
24384
+ );
24385
+ if (!raw) return false;
24386
+ await this.storage.delete(TRAP_STORE_NAMESPACE, trapKey(trapId));
24387
+ return true;
24388
+ } catch {
24389
+ return false;
24390
+ }
24391
+ }
24392
+ /**
24393
+ * Load every persisted trap. Used at boot to repopulate the
24394
+ * in-memory TrapRegistry. Corrupted records are silently skipped
24395
+ * so one malformed entry never blocks the rest of the fortress's
24396
+ * traps from rehydrating.
24397
+ *
24398
+ * Returns the specs sorted by `compiled_at` ascending so the
24399
+ * in-memory registry's insertion order matches the original
24400
+ * deploy order (relevant for Pi-1's "first-deployed wins on
24401
+ * overlapping match" contract).
24402
+ */
24403
+ async loadAll() {
24404
+ const metas = await this.storage.list(
24405
+ TRAP_STORE_NAMESPACE,
24406
+ TRAP_STORE_KEY_PREFIX
24407
+ );
24408
+ const out = [];
24409
+ for (const meta of metas) {
24410
+ const trapId = stripKeyPrefix3(meta.key);
24411
+ if (trapId === null) continue;
24412
+ const raw = await this.storage.read(TRAP_STORE_NAMESPACE, meta.key);
24413
+ if (!raw) continue;
24414
+ if (raw.length > MAX_TRAP_BYTES) continue;
24415
+ const spec = this.decode(trapId, raw);
24416
+ if (spec !== null) out.push(spec);
24417
+ }
24418
+ out.sort(
24419
+ (a, b) => a.compiled_at < b.compiled_at ? -1 : a.compiled_at > b.compiled_at ? 1 : 0
24420
+ );
24421
+ return out;
24422
+ }
24423
+ /** Read-only fortress-id getter. */
24424
+ getFortressId() {
24425
+ return this.fortressId;
24426
+ }
24427
+ decode(trapId, raw) {
24428
+ try {
24429
+ const aad = stringToBytes(trapId);
24430
+ const envelope = JSON.parse(bytesToString(raw));
24431
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
24432
+ const persisted = JSON.parse(bytesToString(plaintext));
24433
+ if (persisted.version !== 1) return null;
24434
+ if (persisted.spec.trap_id !== trapId) return null;
24435
+ return persisted.spec;
24436
+ } catch {
24437
+ return null;
24438
+ }
24439
+ }
24440
+ };
24441
+ }
24442
+ });
24443
+
23580
24444
  // src/sentinel/sentinel.ts
23581
24445
  var Sentinel;
23582
24446
  var init_sentinel = __esm({
@@ -23617,7 +24481,7 @@ var EGRESS_VOLUME_SENTINEL_ID, WARN_SIGMA, ALERT_SIGMA, BASELINE_WINDOWS, QUERY_
23617
24481
  var init_egress_volume_watcher = __esm({
23618
24482
  "src/sentinel/sentinels/egress-volume-watcher.ts"() {
23619
24483
  init_sentinel();
23620
- init_types3();
24484
+ init_types4();
23621
24485
  EGRESS_VOLUME_SENTINEL_ID = "egress-volume";
23622
24486
  WARN_SIGMA = 3;
23623
24487
  ALERT_SIGMA = 6;
@@ -36521,7 +37385,7 @@ var init_recovery_key_disclosure = __esm({
36521
37385
  });
36522
37386
 
36523
37387
  // src/hub/types.ts
36524
- var init_types5 = __esm({
37388
+ var init_types6 = __esm({
36525
37389
  "src/hub/types.ts"() {
36526
37390
  }
36527
37391
  });
@@ -37560,7 +38424,7 @@ var init_hub = __esm({
37560
38424
  "src/hub/index.ts"() {
37561
38425
  init_constants3();
37562
38426
  init_errors4();
37563
- init_types5();
38427
+ init_types6();
37564
38428
  init_agent_registry();
37565
38429
  init_inbox_store();
37566
38430
  init_inbox_aggregator();
@@ -39288,7 +40152,7 @@ ${runningLines.join("\n")}`;
39288
40152
  function chatStorageKey(surface, threadKey) {
39289
40153
  return `${surface}.${threadKey}`;
39290
40154
  }
39291
- var OPERATOR_CHAT_NAMESPACE, HKDF_INFO4, OperatorChatStore;
40155
+ var OPERATOR_CHAT_NAMESPACE, HKDF_INFO5, OperatorChatStore;
39292
40156
  var init_operator_chat_store = __esm({
39293
40157
  "src/chat/operator-chat-store.ts"() {
39294
40158
  init_encryption();
@@ -39296,13 +40160,13 @@ var init_operator_chat_store = __esm({
39296
40160
  init_encoding();
39297
40161
  init_operator_chat_types();
39298
40162
  OPERATOR_CHAT_NAMESPACE = "_chat";
39299
- HKDF_INFO4 = "operator-chat-store-v1";
40163
+ HKDF_INFO5 = "operator-chat-store-v1";
39300
40164
  OperatorChatStore = class {
39301
40165
  storage;
39302
40166
  encryptionKey;
39303
40167
  constructor(storage, masterKey) {
39304
40168
  this.storage = storage;
39305
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
40169
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
39306
40170
  }
39307
40171
  /**
39308
40172
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -39387,7 +40251,7 @@ var init_operator_chat_store = __esm({
39387
40251
  function bundleKey(threadId) {
39388
40252
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
39389
40253
  }
39390
- function stripKeyPrefix3(key) {
40254
+ function stripKeyPrefix4(key) {
39391
40255
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
39392
40256
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
39393
40257
  }
@@ -39398,7 +40262,7 @@ function lastTurnId(bundle) {
39398
40262
  }
39399
40263
  return max;
39400
40264
  }
39401
- var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO5, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
40265
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO6, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
39402
40266
  var init_concierge_memory_store = __esm({
39403
40267
  "src/chat/concierge-memory-store.ts"() {
39404
40268
  init_encryption();
@@ -39406,7 +40270,7 @@ var init_concierge_memory_store = __esm({
39406
40270
  init_encoding();
39407
40271
  CONCIERGE_MEMORY_NAMESPACE = "_chat";
39408
40272
  CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
39409
- HKDF_INFO5 = "concierge-memory-store-v1";
40273
+ HKDF_INFO6 = "concierge-memory-store-v1";
39410
40274
  DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
39411
40275
  MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
39412
40276
  ConciergeMemoryStore = class {
@@ -39417,7 +40281,7 @@ var init_concierge_memory_store = __esm({
39417
40281
  locks;
39418
40282
  constructor(opts) {
39419
40283
  this.storage = opts.storage;
39420
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO5);
40284
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO6);
39421
40285
  this.fortressId = opts.fortressId;
39422
40286
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
39423
40287
  this.locks = /* @__PURE__ */ new Map();
@@ -39543,7 +40407,7 @@ var init_concierge_memory_store = __esm({
39543
40407
  );
39544
40408
  const summaries = [];
39545
40409
  for (const meta of entries) {
39546
- const threadId = stripKeyPrefix3(meta.key);
40410
+ const threadId = stripKeyPrefix4(meta.key);
39547
40411
  if (threadId === null) continue;
39548
40412
  const bundle = await this.loadBundle(threadId);
39549
40413
  if (!bundle || bundle.turns.length === 0) continue;
@@ -39596,7 +40460,7 @@ var init_concierge_memory_store = __esm({
39596
40460
  );
39597
40461
  let pruned = 0;
39598
40462
  for (const meta of entries) {
39599
- const threadId = stripKeyPrefix3(meta.key);
40463
+ const threadId = stripKeyPrefix4(meta.key);
39600
40464
  if (threadId === null) continue;
39601
40465
  pruned += await this.withLock(threadId, async () => {
39602
40466
  const bundle = await this.loadBundle(threadId);
@@ -40066,7 +40930,7 @@ var init_defaults = __esm({
40066
40930
  });
40067
40931
 
40068
40932
  // src/intelligence/policy-store.ts
40069
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO6, IntelligenceConfigStore;
40933
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO7, IntelligenceConfigStore;
40070
40934
  var init_policy_store = __esm({
40071
40935
  "src/intelligence/policy-store.ts"() {
40072
40936
  init_encryption();
@@ -40075,13 +40939,13 @@ var init_policy_store = __esm({
40075
40939
  init_defaults();
40076
40940
  INTELLIGENCE_NAMESPACE = "_intelligence";
40077
40941
  SUBSTRATE_CONFIG_KEY = "substrate-config";
40078
- HKDF_INFO6 = "intelligence-substrate-config";
40942
+ HKDF_INFO7 = "intelligence-substrate-config";
40079
40943
  IntelligenceConfigStore = class {
40080
40944
  storage;
40081
40945
  encryptionKey;
40082
40946
  constructor(storage, masterKey) {
40083
40947
  this.storage = storage;
40084
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO6);
40948
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO7);
40085
40949
  }
40086
40950
  /**
40087
40951
  * Load the operator's substrate config from disk. Returns the config
@@ -41874,8 +42738,60 @@ var init_model_provenance = __esm({
41874
42738
  });
41875
42739
 
41876
42740
  // src/storage/memory.ts
42741
+ var MemoryStorage;
41877
42742
  var init_memory = __esm({
41878
42743
  "src/storage/memory.ts"() {
42744
+ MemoryStorage = class {
42745
+ store = /* @__PURE__ */ new Map();
42746
+ storageKey(namespace, key) {
42747
+ return `${namespace}/${key}`;
42748
+ }
42749
+ async write(namespace, key, data) {
42750
+ this.store.set(this.storageKey(namespace, key), {
42751
+ data: new Uint8Array(data),
42752
+ // Copy to prevent external mutation
42753
+ modified_at: (/* @__PURE__ */ new Date()).toISOString()
42754
+ });
42755
+ }
42756
+ async read(namespace, key) {
42757
+ const entry = this.store.get(this.storageKey(namespace, key));
42758
+ if (!entry) return null;
42759
+ return new Uint8Array(entry.data);
42760
+ }
42761
+ async delete(namespace, key, _secureOverwrite) {
42762
+ return this.store.delete(this.storageKey(namespace, key));
42763
+ }
42764
+ async list(namespace, prefix) {
42765
+ const entries = [];
42766
+ const nsPrefix = `${namespace}/`;
42767
+ for (const [storeKey, entry] of this.store) {
42768
+ if (!storeKey.startsWith(nsPrefix)) continue;
42769
+ const key = storeKey.slice(nsPrefix.length);
42770
+ if (prefix && !key.startsWith(prefix)) continue;
42771
+ entries.push({
42772
+ key,
42773
+ namespace,
42774
+ size_bytes: entry.data.length,
42775
+ modified_at: entry.modified_at
42776
+ });
42777
+ }
42778
+ return entries.sort((a, b) => a.key.localeCompare(b.key));
42779
+ }
42780
+ async exists(namespace, key) {
42781
+ return this.store.has(this.storageKey(namespace, key));
42782
+ }
42783
+ async totalSize() {
42784
+ let total = 0;
42785
+ for (const entry of this.store.values()) {
42786
+ total += entry.data.length;
42787
+ }
42788
+ return total;
42789
+ }
42790
+ /** Clear all stored data (useful in tests) */
42791
+ clear() {
42792
+ this.store.clear();
42793
+ }
42794
+ };
41879
42795
  }
41880
42796
  });
41881
42797
 
@@ -42122,7 +43038,55 @@ async function defaultFetcher(url, init) {
42122
43038
  json: () => response.json()
42123
43039
  };
42124
43040
  }
42125
- var DID_CONTEXT, DEFAULT_TIMEOUT_MS4, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE;
43041
+ async function loadFortressDidWebRecord(storagePath) {
43042
+ const persistPath = path.join(storagePath, FORTRESS_DID_WEB_REGISTRY_PATH);
43043
+ let raw;
43044
+ try {
43045
+ raw = await promises.readFile(persistPath, "utf-8");
43046
+ } catch (err) {
43047
+ const code = err.code;
43048
+ if (code === "ENOENT") return null;
43049
+ throw err;
43050
+ }
43051
+ let parsed;
43052
+ try {
43053
+ parsed = JSON.parse(raw);
43054
+ } catch (e) {
43055
+ const message = e instanceof Error ? e.message : String(e);
43056
+ throw new Error(
43057
+ `did-web: fortress-config record at ${persistPath} is not valid JSON: ${message}`
43058
+ );
43059
+ }
43060
+ if (!isFortressDidWebRecord(parsed)) {
43061
+ throw new Error(
43062
+ `did-web: fortress-config record at ${persistPath} is malformed (expected version: 1 with identifier.did + identifier.authority_host)`
43063
+ );
43064
+ }
43065
+ return parsed;
43066
+ }
43067
+ function isFortressDidWebRecord(value) {
43068
+ if (!value || typeof value !== "object") return false;
43069
+ const v = value;
43070
+ if (v["version"] !== 1) return false;
43071
+ const id = v["identifier"];
43072
+ if (!id || typeof id !== "object") return false;
43073
+ if (typeof id["did"] !== "string" || !id["did"].startsWith("did:web:")) {
43074
+ return false;
43075
+ }
43076
+ if (typeof id["authority_host"] !== "string") return false;
43077
+ if (typeof id["fortress_id"] !== "string") return false;
43078
+ if (typeof id["created_at"] !== "string") return false;
43079
+ if (!id["did_document"] || typeof id["did_document"] !== "object") {
43080
+ return false;
43081
+ }
43082
+ const artifact = v["artifact"];
43083
+ if (!artifact || typeof artifact !== "object") return false;
43084
+ if (typeof artifact["url"] !== "string") return false;
43085
+ if (typeof artifact["publish_path"] !== "string") return false;
43086
+ if (typeof artifact["sha256"] !== "string") return false;
43087
+ return true;
43088
+ }
43089
+ var DID_CONTEXT, DEFAULT_TIMEOUT_MS4, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE, FORTRESS_DID_WEB_REGISTRY_PATH;
42126
43090
  var init_did_web = __esm({
42127
43091
  "src/recognition/did-web.ts"() {
42128
43092
  init_encoding();
@@ -42135,6 +43099,7 @@ var init_did_web = __esm({
42135
43099
  HOST_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
42136
43100
  FORTRESS_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
42137
43101
  AGENT_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
43102
+ FORTRESS_DID_WEB_REGISTRY_PATH = "recognition/did-web.json";
42138
43103
  }
42139
43104
  });
42140
43105
 
@@ -43605,9 +44570,31 @@ Options:
43605
44570
  --accept-unverifiable-attestations
43606
44571
  On import: accept reputation attestations whose
43607
44572
  signer DID is not in the bundle (Tier 1 confirmation)
44573
+ --did-web <identifier> Embed a specific did:web identifier in the export
44574
+ manifest. Requires --did-web-authority-host.
44575
+ Overrides fortress-config auto-inclusion.
44576
+ --did-web-authority-host <host> Authority host for --did-web (required with it).
44577
+ --did-web-published-at <iso8601> Operator's claimed publication time for the DID
44578
+ Document (optional; ISO 8601).
44579
+ --no-did-web Explicit opt-out: skip did:web inclusion even if
44580
+ a fortress-config record exists. (Alias for
44581
+ --include-did-web=false.)
44582
+ --did-web-allowed-host <host> On import: host allowed for outbound did:web
44583
+ resolution; repeatable. Empty means refuse to
44584
+ resolve (no-outbound-by-default).
44585
+ --skip-did-web-verify On import: skip did:web resolution entirely.
43608
44586
  --json
43609
44587
  --yes, -y Explicit non-interactive Tier 1 approval
43610
44588
  --help, -h
44589
+
44590
+ did:web auto-inclusion (build 3):
44591
+ Running "sanctuary did-web issue --authority-host <host>" registers the
44592
+ operator's did:web identifier at <storage>/recognition/did-web.json.
44593
+ Subsequent "sanctuary exit export" runs auto-include this identifier in
44594
+ the manifest's identity_binding without requiring any --did-web flag.
44595
+ Per-fortress isolation is structural: the record lives under the
44596
+ fortress's storage_path, so different fortresses carry different
44597
+ registered identifiers.
43611
44598
  `);
43612
44599
  }
43613
44600
  async function runExitCommand(args) {
@@ -43706,12 +44693,15 @@ ${policyErr.message}
43706
44693
  throw policyErr;
43707
44694
  }
43708
44695
  const includeDidWebFlag = flagValue(argv, "--include-did-web");
43709
- const includeDidWebDisabled = includeDidWebFlag === "false";
44696
+ const explicitOptOut = hasFlag(argv, "--no-did-web") || includeDidWebFlag === "false";
43710
44697
  const didWebIdentifier = flagValue(argv, "--did-web");
43711
44698
  const didWebAuthorityHost = flagValue(argv, "--did-web-authority-host");
43712
44699
  const didWebPublishedAt = flagValue(argv, "--did-web-published-at");
43713
44700
  let exportDidWeb;
43714
- if (!includeDidWebDisabled && didWebIdentifier !== void 0) {
44701
+ let didWebSource;
44702
+ if (explicitOptOut) {
44703
+ didWebSource = "opted-out";
44704
+ } else if (didWebIdentifier !== void 0) {
43715
44705
  if (didWebAuthorityHost === void 0) {
43716
44706
  write(
43717
44707
  err,
@@ -43724,6 +44714,19 @@ ${policyErr.message}
43724
44714
  authority_host: didWebAuthorityHost,
43725
44715
  ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
43726
44716
  };
44717
+ didWebSource = "cli-override";
44718
+ } else {
44719
+ const record = await loadFortressDidWebRecord(ctx.storagePath);
44720
+ if (record !== null) {
44721
+ exportDidWeb = {
44722
+ identifier: record.identifier.did,
44723
+ authority_host: record.identifier.authority_host,
44724
+ ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
44725
+ };
44726
+ didWebSource = "fortress-config";
44727
+ } else {
44728
+ didWebSource = "no-record";
44729
+ }
43727
44730
  }
43728
44731
  const result = await exportExitBundle({
43729
44732
  bundleDir: outDir,
@@ -43739,12 +44742,42 @@ ${policyErr.message}
43739
44742
  keySource: ctx.keySource,
43740
44743
  ...exportDidWeb !== void 0 ? { didWeb: exportDidWeb } : {}
43741
44744
  });
43742
- if (json) write(out, JSON.stringify(result, null, 2) + "\n");
43743
- else {
44745
+ if (json) {
44746
+ write(
44747
+ out,
44748
+ JSON.stringify(
44749
+ { ...result, did_web_source: didWebSource },
44750
+ null,
44751
+ 2
44752
+ ) + "\n"
44753
+ );
44754
+ } else {
43744
44755
  write(out, `exported: ${result.bundle_dir}
43745
44756
  `);
43746
44757
  write(out, `manifest_hash: ${result.manifest_hash}
43747
44758
  `);
44759
+ if (didWebSource === "fortress-config" && exportDidWeb) {
44760
+ write(
44761
+ out,
44762
+ `did:web: auto-included from fortress config (${exportDidWeb.identifier})
44763
+ `
44764
+ );
44765
+ } else if (didWebSource === "cli-override" && exportDidWeb) {
44766
+ write(
44767
+ out,
44768
+ `did:web: included via CLI override (${exportDidWeb.identifier})
44769
+ `
44770
+ );
44771
+ } else if (didWebSource === "opted-out") {
44772
+ write(out, `did:web: skipped (operator opt-out via --no-did-web)
44773
+ `);
44774
+ } else if (didWebSource === "no-record") {
44775
+ write(
44776
+ out,
44777
+ `did:web: not included (no fortress config; run "sanctuary did-web issue" to register)
44778
+ `
44779
+ );
44780
+ }
43748
44781
  for (const item of result.unsupported_artifacts) {
43749
44782
  write(out, `unsupported: ${item}
43750
44783
  `);
@@ -43895,6 +44928,7 @@ var init_cli = __esm({
43895
44928
  init_encoding();
43896
44929
  init_bundle();
43897
44930
  init_verifier2();
44931
+ init_did_web();
43898
44932
  }
43899
44933
  });
43900
44934
 
@@ -44449,6 +45483,7 @@ ${err.message}
44449
45483
  await baseline.load();
44450
45484
  let approvalChannel;
44451
45485
  let dashboard;
45486
+ let intelligenceSelector;
44452
45487
  if (config.dashboard.enabled) {
44453
45488
  let authToken = config.dashboard.auth_token;
44454
45489
  if (authToken === "auto") {
@@ -44475,7 +45510,6 @@ ${err.message}
44475
45510
  profileStore
44476
45511
  });
44477
45512
  const embeddedHubIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
44478
- let intelligenceSelector;
44479
45513
  try {
44480
45514
  intelligenceSelector = new SubstrateSelector({
44481
45515
  storage,
@@ -44632,6 +45666,44 @@ ${err.message}
44632
45666
  workflowStateTracker
44633
45667
  });
44634
45668
  }
45669
+ const honeypotRegistry = new TrapRegistry();
45670
+ const honeypotStore = new TrapStore({
45671
+ storage,
45672
+ masterKey,
45673
+ fortressId: fortressIdForAggregator
45674
+ });
45675
+ try {
45676
+ const persistedSpecs = await honeypotStore.loadAll();
45677
+ for (const spec of persistedSpecs) {
45678
+ honeypotRegistry.deploy(spec);
45679
+ }
45680
+ if (persistedSpecs.length > 0) {
45681
+ auditLog.append(
45682
+ "l2",
45683
+ HONEYPOT_AUDIT_OPS.LOADED,
45684
+ aggregatorIdentityId,
45685
+ {
45686
+ fortress_id: fortressIdForAggregator,
45687
+ trap_count: persistedSpecs.length
45688
+ }
45689
+ );
45690
+ }
45691
+ } catch (err) {
45692
+ console.error(
45693
+ ` Note: honeypot trap store unavailable (${err.message}). Deployed traps from prior runs will not be restored; re-deploy via the management API.`
45694
+ );
45695
+ }
45696
+ if (dashboard) {
45697
+ dashboard.setHoneypotRegistry({
45698
+ registry: honeypotRegistry,
45699
+ findingStore: sentinelFindingStore,
45700
+ auditLog,
45701
+ operatorId: aggregatorIdentityId,
45702
+ fortressId: fortressIdForAggregator,
45703
+ ...intelligenceSelector ? { selector: intelligenceSelector } : {},
45704
+ store: honeypotStore
45705
+ });
45706
+ }
44635
45707
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
44636
45708
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
44637
45709
  config,
@@ -44832,6 +45904,9 @@ var init_src = __esm({
44832
45904
  init_handoff_log();
44833
45905
  init_handoff_routes();
44834
45906
  init_workflow_state_tracker();
45907
+ init_trap_registry();
45908
+ init_trap_store();
45909
+ init_types3();
44835
45910
  init_sentinels();
44836
45911
  init_subscription_store();
44837
45912
  init_tools4();
@@ -50271,6 +51346,14 @@ Options:
50271
51346
  --json Output as JSON.
50272
51347
  --help, -h Show this help.
50273
51348
 
51349
+ Fortress-config auto-inclusion (build 3): "issue" persists the record
51350
+ at <storage>/recognition/did-web.json. This file IS the fortress's
51351
+ registered did:web identifier. Subsequent "sanctuary exit export"
51352
+ runs auto-include it in the manifest without any --did-web flag.
51353
+ Per-fortress isolation is structural (different storage paths carry
51354
+ different records). Use "sanctuary exit export --no-did-web" to
51355
+ opt out for a specific export without removing the registration.
51356
+
50274
51357
  Castle-walking note: did:web resolution is outbound HTTPS by design.
50275
51358
  This CLI never opens an outbound socket. The opt-in surface is your
50276
51359
  choice to run "did-web issue" with --authority-host; the resulting
@@ -50405,7 +51488,7 @@ async function cmdIssue(argv, out, err, env) {
50405
51488
  write3(out, JSON.stringify(record, null, 2) + "\n");
50406
51489
  return 0;
50407
51490
  }
50408
- write3(out, `did:web identifier issued.
51491
+ write3(out, `did:web identifier issued and registered on this fortress.
50409
51492
  `);
50410
51493
  write3(out, ` DID: ${identifier.did}
50411
51494
  `);
@@ -50427,6 +51510,13 @@ Next step: publish the DID Document to your HTTPS host.
50427
51510
  const artifactPath = path.join(persistDir, "did.json");
50428
51511
  await promises.writeFile(artifactPath, artifact.artifact, { mode: 420 });
50429
51512
  write3(out, `
51513
+ Auto-inclusion: subsequent "sanctuary exit export" runs will
51514
+ `);
51515
+ write3(out, `auto-include this identifier in the bundle manifest. Pass
51516
+ `);
51517
+ write3(out, `"--no-did-web" to opt out for a specific export.
51518
+ `);
51519
+ write3(out, `
50430
51520
  Castle-walking note: this CLI never opens an outbound socket.
50431
51521
  `);
50432
51522
  write3(out, `Publishing the DID Document is your operation; serve the artifact
@@ -50461,7 +51551,7 @@ Run "sanctuary did-web issue --authority-host <host>" to issue one.
50461
51551
  return 0;
50462
51552
  }
50463
51553
  const parsed = JSON.parse(bytes.toString("utf-8"));
50464
- write3(out, `did:web identifier on this fortress:
51554
+ write3(out, `did:web identifier registered on this fortress:
50465
51555
  `);
50466
51556
  write3(out, ` DID: ${parsed.identifier.did}
50467
51557
  `);
@@ -50472,6 +51562,13 @@ Run "sanctuary did-web issue --authority-host <host>" to issue one.
50472
51562
  write3(out, ` Publish URL: ${parsed.artifact.url}
50473
51563
  `);
50474
51564
  write3(out, ` SHA-256: ${parsed.artifact.sha256}
51565
+ `);
51566
+ write3(out, `
51567
+ Auto-inclusion: subsequent "sanctuary exit export" runs auto-include
51568
+ `);
51569
+ write3(out, `this identifier in the bundle manifest without --did-web. Pass
51570
+ `);
51571
+ write3(out, `"--no-did-web" to opt out for a specific export.
50475
51572
  `);
50476
51573
  return 0;
50477
51574
  }
@@ -50685,7 +51782,7 @@ var init_per_agent_activity = __esm({
50685
51782
  var PER_AGENT_ACTIVITY_DETECTOR_ID, PerAgentActivityDetector, PendingClassifier;
50686
51783
  var init_per_agent_activity_detector = __esm({
50687
51784
  "src/anomaly-detection/detectors/per-agent-activity-detector.ts"() {
50688
- init_types4();
51785
+ init_types5();
50689
51786
  init_rolling_baseline();
50690
51787
  init_classifier_state_store();
50691
51788
  init_per_agent_activity();
@@ -51165,7 +52262,1064 @@ var init_anomaly = __esm({
51165
52262
  init_anomaly_catalog();
51166
52263
  init_anomaly_subscription_store();
51167
52264
  init_classifier_state_store();
51168
- init_types4();
52265
+ init_types5();
52266
+ }
52267
+ });
52268
+ function computeDraftId(draft) {
52269
+ return crypto.createHash("sha256").update(`${draft.english_text}|${draft.observed_at}|${draft.operator_id}`).digest("hex");
52270
+ }
52271
+ function compileDeterministic(text) {
52272
+ const normalized = text.trim().toLowerCase();
52273
+ const requireMatch = normalized.match(
52274
+ /^(always\s+)?require approval (?:for|on) ([a-z][a-z0-9_]*)\.?$/
52275
+ );
52276
+ if (requireMatch) {
52277
+ const op = requireMatch[2];
52278
+ return {
52279
+ rule: { kind: "tier1_add_operation", operation: op },
52280
+ explanation: `Adds "${op}" to the Tier 1 always-approve list. Every call to ${op} will require explicit human approval before it executes.`,
52281
+ confidence: "high",
52282
+ warnings: []
52283
+ };
52284
+ }
52285
+ const noAgentMatch = normalized.match(
52286
+ /^no agent should ([a-z][a-z0-9_]*)\.?$/
52287
+ );
52288
+ if (noAgentMatch) {
52289
+ const op = noAgentMatch[1];
52290
+ return {
52291
+ rule: { kind: "tier1_add_operation", operation: op },
52292
+ explanation: `Adds "${op}" to the Tier 1 always-approve list. The operator must approve every ${op} call before it executes.`,
52293
+ confidence: "high",
52294
+ warnings: []
52295
+ };
52296
+ }
52297
+ const allowMatch = normalized.match(
52298
+ /^(?:allow ([a-z][a-z0-9_]*) without approval|auto-allow ([a-z][a-z0-9_]*))\.?$/
52299
+ );
52300
+ if (allowMatch) {
52301
+ const op = allowMatch[1] ?? allowMatch[2];
52302
+ return {
52303
+ rule: { kind: "tier3_add_operation", operation: op },
52304
+ explanation: `Adds "${op}" to the Tier 3 always-allow list. Calls to ${op} will be audit-logged but never block on approval.`,
52305
+ confidence: "high",
52306
+ warnings: []
52307
+ };
52308
+ }
52309
+ const removeMatch = normalized.match(
52310
+ /^(?:remove ([a-z][a-z0-9_]*) from approval list|stop requiring approval for ([a-z][a-z0-9_]*))\.?$/
52311
+ );
52312
+ if (removeMatch) {
52313
+ const op = removeMatch[1] ?? removeMatch[2];
52314
+ return {
52315
+ rule: { kind: "tier1_remove_operation", operation: op },
52316
+ explanation: `Removes "${op}" from the Tier 1 always-approve list. Future calls will fall through to Tier 2 anomaly evaluation or Tier 3 if explicitly listed.`,
52317
+ confidence: "high",
52318
+ warnings: []
52319
+ };
52320
+ }
52321
+ const tier2Multiplier = normalized.match(
52322
+ /^set anomaly frequency multiplier to (\d+(?:\.\d+)?)\.?$/
52323
+ );
52324
+ if (tier2Multiplier) {
52325
+ const value = Number.parseFloat(tier2Multiplier[1]);
52326
+ return {
52327
+ rule: {
52328
+ kind: "tier2_set_field",
52329
+ tier2_update: { field: "frequency_spike_multiplier", value }
52330
+ },
52331
+ explanation: `Sets the Tier 2 anomaly frequency multiplier to ${value}. Tool calls that exceed the rolling baseline by more than this multiple will fire an anomaly evaluation.`,
52332
+ confidence: "high",
52333
+ warnings: []
52334
+ };
52335
+ }
52336
+ const tier2MaxSigns = normalized.match(
52337
+ /^set max signs per minute to (\d+)\.?$/
52338
+ );
52339
+ if (tier2MaxSigns) {
52340
+ const value = Number.parseInt(tier2MaxSigns[1], 10);
52341
+ return {
52342
+ rule: {
52343
+ kind: "tier2_set_field",
52344
+ tier2_update: { field: "max_signs_per_minute", value }
52345
+ },
52346
+ explanation: `Caps Tier 2 signing operations at ${value} per minute. Crossing the cap fires an anomaly evaluation.`,
52347
+ confidence: "high",
52348
+ warnings: []
52349
+ };
52350
+ }
52351
+ return null;
52352
+ }
52353
+ function parseLlmOutput(text) {
52354
+ let parsed;
52355
+ try {
52356
+ const stripped = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
52357
+ parsed = JSON.parse(stripped);
52358
+ } catch (err) {
52359
+ return {
52360
+ ok: false,
52361
+ reason: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`
52362
+ };
52363
+ }
52364
+ if (!parsed || typeof parsed !== "object") {
52365
+ return { ok: false, reason: "top-level is not an object" };
52366
+ }
52367
+ const obj = parsed;
52368
+ const ruleObj = obj["rule"];
52369
+ if (!ruleObj || typeof ruleObj !== "object") {
52370
+ return { ok: false, reason: "missing or non-object rule" };
52371
+ }
52372
+ const rule = ruleObj;
52373
+ const kindRaw = rule["kind"];
52374
+ if (typeof kindRaw !== "string" || !isValidKind(kindRaw)) {
52375
+ return { ok: false, reason: `invalid rule.kind: ${String(kindRaw)}` };
52376
+ }
52377
+ const compiledRule = { kind: kindRaw };
52378
+ if (typeof rule["operation"] === "string") {
52379
+ compiledRule.operation = rule["operation"];
52380
+ }
52381
+ const tier2 = rule["tier2_update"];
52382
+ if (tier2 && typeof tier2 === "object") {
52383
+ const fld = tier2["field"];
52384
+ const val = tier2["value"];
52385
+ if (typeof fld === "string" && (typeof val === "number" || typeof val === "string")) {
52386
+ compiledRule.tier2_update = {
52387
+ field: fld,
52388
+ value: val
52389
+ };
52390
+ }
52391
+ }
52392
+ if (kindRaw === "tier1_add_operation" || kindRaw === "tier1_remove_operation" || kindRaw === "tier3_add_operation" || kindRaw === "tier3_remove_operation") {
52393
+ if (typeof compiledRule.operation !== "string" || compiledRule.operation.length === 0) {
52394
+ return { ok: false, reason: `${kindRaw} requires operation` };
52395
+ }
52396
+ }
52397
+ if (kindRaw === "tier2_set_field" && compiledRule.tier2_update === void 0) {
52398
+ return { ok: false, reason: "tier2_set_field requires tier2_update" };
52399
+ }
52400
+ const explanation = typeof obj["explanation"] === "string" ? obj["explanation"] : "";
52401
+ if (!explanation) return { ok: false, reason: "missing explanation" };
52402
+ let confidence = "medium";
52403
+ const confRaw = obj["confidence"];
52404
+ if (confRaw === "high" || confRaw === "medium" || confRaw === "low") {
52405
+ confidence = confRaw;
52406
+ }
52407
+ const warnings = [];
52408
+ const warnRaw = obj["warnings"];
52409
+ if (Array.isArray(warnRaw)) {
52410
+ for (const w of warnRaw) {
52411
+ if (typeof w === "string") warnings.push(w);
52412
+ }
52413
+ }
52414
+ return { ok: true, rule: compiledRule, explanation, confidence, warnings };
52415
+ }
52416
+ function isValidKind(s) {
52417
+ return s === "tier1_add_operation" || s === "tier1_remove_operation" || s === "tier3_add_operation" || s === "tier3_remove_operation" || s === "tier2_set_field";
52418
+ }
52419
+ function buildLlmPromptContext() {
52420
+ return [
52421
+ "You compile a Sanctuary operator's plain-English policy statement into a structured rule.",
52422
+ "Output strictly a JSON object with these fields:",
52423
+ ' "rule": { "kind": "<one of tier1_add_operation | tier1_remove_operation | tier3_add_operation | tier3_remove_operation | tier2_set_field>", "operation": "<snake_case>"? , "tier2_update": { "field": "<keyof Tier2Config>", "value": <number|string> }? }',
52424
+ ' "explanation": "<operator-facing prose paragraph>"',
52425
+ ' "confidence": "high" | "medium" | "low"',
52426
+ ' "warnings": [ "<short warning string>" ]',
52427
+ "Do NOT include any text outside the JSON. Do NOT activate the rule; this is a draft for operator review."
52428
+ ].join("\n");
52429
+ }
52430
+ function buildLlmPromptQuery(englishText) {
52431
+ return `Compile this operator policy statement: ${englishText}`;
52432
+ }
52433
+ var ENGLISH_POLICY_AUDIT_OPS, ENGLISH_POLICY_MAX_TEXT_CHARS, EnglishPolicyCompiler;
52434
+ var init_english_policy_compiler = __esm({
52435
+ "src/policy-engine/english-policy-compiler.ts"() {
52436
+ ENGLISH_POLICY_AUDIT_OPS = {
52437
+ DRAFTED: "english_policy_drafted",
52438
+ COMPILED: "english_policy_compiled",
52439
+ COMPILE_FAILED: "english_policy_compile_failed"
52440
+ };
52441
+ ENGLISH_POLICY_MAX_TEXT_CHARS = 2e3;
52442
+ EnglishPolicyCompiler = class {
52443
+ auditLog;
52444
+ fortressId;
52445
+ selector;
52446
+ now;
52447
+ constructor(deps) {
52448
+ this.auditLog = deps.auditLog;
52449
+ this.fortressId = deps.fortressId;
52450
+ this.selector = deps.selector ?? null;
52451
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
52452
+ }
52453
+ async compile(draft) {
52454
+ const draftId = computeDraftId(draft);
52455
+ this.auditLog.append(
52456
+ "l2",
52457
+ ENGLISH_POLICY_AUDIT_OPS.DRAFTED,
52458
+ draft.operator_id,
52459
+ {
52460
+ draft_id: draftId,
52461
+ text_length: draft.english_text.length,
52462
+ fortress_id: this.fortressId
52463
+ }
52464
+ );
52465
+ if (draft.english_text.length === 0) {
52466
+ return this.buildLowConfidence(
52467
+ draft,
52468
+ draftId,
52469
+ "Operator provided empty text.",
52470
+ "deterministic",
52471
+ ["empty input"]
52472
+ );
52473
+ }
52474
+ if (draft.english_text.length > ENGLISH_POLICY_MAX_TEXT_CHARS) {
52475
+ return this.buildLowConfidence(
52476
+ draft,
52477
+ draftId,
52478
+ `Operator text exceeded ${ENGLISH_POLICY_MAX_TEXT_CHARS} chars; refusing to compile to avoid prompt blowback.`,
52479
+ "deterministic",
52480
+ [`text exceeds ${ENGLISH_POLICY_MAX_TEXT_CHARS} chars`]
52481
+ );
52482
+ }
52483
+ const deterministic = compileDeterministic(draft.english_text);
52484
+ if (deterministic !== null) {
52485
+ const compiled = this.buildCompiled(
52486
+ draft,
52487
+ draftId,
52488
+ deterministic.rule,
52489
+ deterministic.explanation,
52490
+ deterministic.confidence,
52491
+ deterministic.warnings,
52492
+ "deterministic"
52493
+ );
52494
+ this.auditCompiled(compiled);
52495
+ return compiled;
52496
+ }
52497
+ if (this.selector === null) {
52498
+ const low = this.buildLowConfidence(
52499
+ draft,
52500
+ draftId,
52501
+ "No deterministic match; LLM-assist disabled.",
52502
+ "deterministic",
52503
+ ["no deterministic match", "LLM-assist disabled"]
52504
+ );
52505
+ this.auditCompileFailed(low, "no_llm_substrate");
52506
+ return low;
52507
+ }
52508
+ try {
52509
+ const resp = await this.selector.invokeSummarize("gate-explanation", {
52510
+ kind: "summarize",
52511
+ context: buildLlmPromptContext(),
52512
+ query: buildLlmPromptQuery(draft.english_text),
52513
+ maxTokens: 600
52514
+ });
52515
+ if (resp.failureClass) {
52516
+ const low = this.buildLowConfidence(
52517
+ draft,
52518
+ draftId,
52519
+ `LLM substrate failure: ${resp.failureClass}`,
52520
+ resp.servedBy,
52521
+ [`substrate failure: ${resp.failureClass}`]
52522
+ );
52523
+ this.auditCompileFailed(low, "substrate_failure");
52524
+ return low;
52525
+ }
52526
+ if (resp.body.kind !== "summarize") {
52527
+ const low = this.buildLowConfidence(
52528
+ draft,
52529
+ draftId,
52530
+ "LLM substrate returned a non-summarize response.",
52531
+ resp.servedBy,
52532
+ [`unexpected body kind: ${resp.body.kind}`]
52533
+ );
52534
+ this.auditCompileFailed(low, "unexpected_body");
52535
+ return low;
52536
+ }
52537
+ const parsed = parseLlmOutput(resp.body.text);
52538
+ if (!parsed.ok) {
52539
+ const low = this.buildLowConfidence(
52540
+ draft,
52541
+ draftId,
52542
+ `LLM output failed schema validation: ${parsed.reason}.`,
52543
+ resp.servedBy,
52544
+ [`schema validation failed: ${parsed.reason}`]
52545
+ );
52546
+ this.auditCompileFailed(low, "schema_validation_failed");
52547
+ return low;
52548
+ }
52549
+ const compiled = this.buildCompiled(
52550
+ draft,
52551
+ draftId,
52552
+ parsed.rule,
52553
+ parsed.explanation,
52554
+ parsed.confidence,
52555
+ parsed.warnings,
52556
+ resp.servedBy
52557
+ );
52558
+ this.auditCompiled(compiled);
52559
+ return compiled;
52560
+ } catch (err) {
52561
+ const msg = err instanceof Error ? err.message : String(err);
52562
+ const low = this.buildLowConfidence(
52563
+ draft,
52564
+ draftId,
52565
+ `LLM-assist threw: ${msg}`,
52566
+ "unknown",
52567
+ [msg]
52568
+ );
52569
+ this.auditCompileFailed(low, "llm_threw");
52570
+ return low;
52571
+ }
52572
+ }
52573
+ // ── helpers ────────────────────────────────────────────────────────
52574
+ buildCompiled(draft, draftId, rule, explanation, confidence, warnings, substrateUsed) {
52575
+ return {
52576
+ draft_id: draftId,
52577
+ english_text: draft.english_text,
52578
+ compiled_rule: rule,
52579
+ explanation_paragraph: explanation,
52580
+ compile_confidence: confidence,
52581
+ compile_warnings: warnings,
52582
+ substrate_used: substrateUsed,
52583
+ compiled_at: this.now().toISOString(),
52584
+ operator_id: draft.operator_id,
52585
+ fortress_id: this.fortressId
52586
+ };
52587
+ }
52588
+ buildLowConfidence(draft, draftId, explanation, substrateUsed, warnings) {
52589
+ return this.buildCompiled(
52590
+ draft,
52591
+ draftId,
52592
+ // Placeholder rule; operator inspects the draft + decides whether
52593
+ // to discard or re-author. Xi-2's activation flow refuses to
52594
+ // activate compile_confidence "low" without explicit operator
52595
+ // override.
52596
+ { kind: "tier1_add_operation", operation: "__low_confidence_placeholder__" },
52597
+ explanation,
52598
+ "low",
52599
+ warnings,
52600
+ substrateUsed
52601
+ );
52602
+ }
52603
+ auditCompiled(compiled) {
52604
+ this.auditLog.append(
52605
+ "l2",
52606
+ ENGLISH_POLICY_AUDIT_OPS.COMPILED,
52607
+ compiled.operator_id,
52608
+ {
52609
+ draft_id: compiled.draft_id,
52610
+ compile_confidence: compiled.compile_confidence,
52611
+ rule_kind: compiled.compiled_rule.kind,
52612
+ warnings_count: compiled.compile_warnings.length,
52613
+ substrate_used: compiled.substrate_used,
52614
+ fortress_id: compiled.fortress_id
52615
+ }
52616
+ );
52617
+ }
52618
+ auditCompileFailed(compiled, reason) {
52619
+ this.auditLog.append(
52620
+ "l2",
52621
+ ENGLISH_POLICY_AUDIT_OPS.COMPILE_FAILED,
52622
+ compiled.operator_id,
52623
+ {
52624
+ draft_id: compiled.draft_id,
52625
+ reason,
52626
+ warnings: compiled.compile_warnings,
52627
+ fortress_id: compiled.fortress_id
52628
+ },
52629
+ "failure"
52630
+ );
52631
+ }
52632
+ };
52633
+ }
52634
+ });
52635
+
52636
+ // src/cli/policy.ts
52637
+ var policy_exports = {};
52638
+ __export(policy_exports, {
52639
+ formatCompiledHumanReadable: () => formatCompiledHumanReadable,
52640
+ runPolicyCommand: () => runPolicyCommand
52641
+ });
52642
+ async function runPolicyCommand(args) {
52643
+ const out = args.out ?? process.stdout;
52644
+ const err = args.err ?? process.stderr;
52645
+ const [sub, ...rest] = args.argv;
52646
+ if (!sub || sub === "--help" || sub === "-h") {
52647
+ printUsage9(out);
52648
+ return 0;
52649
+ }
52650
+ try {
52651
+ switch (sub) {
52652
+ case "compile":
52653
+ return await cmdCompile(rest, { out, err });
52654
+ case "drafts":
52655
+ return await cmdDrafts(rest, { out, err });
52656
+ default:
52657
+ err.write(`Unknown subcommand: ${sub}
52658
+ `);
52659
+ printUsage9(err);
52660
+ return 2;
52661
+ }
52662
+ } catch (e) {
52663
+ const msg = e instanceof Error ? e.message : String(e);
52664
+ err.write(`sanctuary policy: ${msg}
52665
+ `);
52666
+ return 1;
52667
+ }
52668
+ }
52669
+ function printUsage9(s) {
52670
+ s.write(`Usage: sanctuary policy <command> [args]
52671
+
52672
+ compile "<English text>" Compile an operator policy statement
52673
+ to a structured rule + explanation.
52674
+ CLI mode runs deterministic matcher
52675
+ only; LLM-assist is server-only.
52676
+ drafts list Placeholder for Xi-2 persistence.
52677
+ drafts show <draft_id> Placeholder for Xi-2 persistence.
52678
+
52679
+ Xi-1 ships review-only; activation (Xi-2) is a separate flow.
52680
+
52681
+ `);
52682
+ }
52683
+ async function cmdCompile(argv, ctx) {
52684
+ const englishText = argv[0];
52685
+ if (!englishText) {
52686
+ ctx.err.write("compile requires the English text as a single argument:\n");
52687
+ ctx.err.write(' sanctuary policy compile "always require approval for state_export"\n');
52688
+ return 2;
52689
+ }
52690
+ const storage = new MemoryStorage();
52691
+ const masterKey = generateRandomKey();
52692
+ const auditLog = new AuditLog(storage, masterKey);
52693
+ const compiler = new EnglishPolicyCompiler({
52694
+ auditLog,
52695
+ fortressId: "cli-local",
52696
+ selector: null
52697
+ });
52698
+ const compiled = await compiler.compile({
52699
+ english_text: englishText,
52700
+ observed_at: (/* @__PURE__ */ new Date()).toISOString(),
52701
+ operator_id: "cli-operator"
52702
+ });
52703
+ ctx.out.write(formatCompiledHumanReadable(compiled) + "\n");
52704
+ return 0;
52705
+ }
52706
+ async function cmdDrafts(argv, ctx) {
52707
+ const [sub, ...rest] = argv;
52708
+ if (sub === "list" || sub === void 0) {
52709
+ ctx.out.write(
52710
+ "(no local drafts persisted in CLI mode; drafts live in the running server's in-memory store)\n"
52711
+ );
52712
+ ctx.out.write(
52713
+ 'Use "sanctuary policy compile \\"<text>\\"" to preview a compile result locally,\n'
52714
+ );
52715
+ ctx.out.write(
52716
+ "or POST to /api/policy/compile on the running fortress for the full surface.\n"
52717
+ );
52718
+ return 0;
52719
+ }
52720
+ if (sub === "show") {
52721
+ const draftId = rest[0];
52722
+ if (!draftId) {
52723
+ ctx.err.write("drafts show requires a draft_id\n");
52724
+ return 2;
52725
+ }
52726
+ ctx.out.write(
52727
+ `(CLI does not yet persist drafts; show ${draftId} via GET /api/policy/drafts/${draftId} on the running fortress)
52728
+ `
52729
+ );
52730
+ return 0;
52731
+ }
52732
+ ctx.err.write(`Unknown drafts subcommand: ${sub}
52733
+ `);
52734
+ return 2;
52735
+ }
52736
+ function formatCompiledHumanReadable(c) {
52737
+ const lines = [];
52738
+ lines.push(`draft_id: ${c.draft_id}`);
52739
+ lines.push(`compile_confidence: ${c.compile_confidence}`);
52740
+ lines.push(`substrate_used: ${c.substrate_used}`);
52741
+ lines.push(`compiled_at: ${c.compiled_at}`);
52742
+ lines.push(``);
52743
+ lines.push(`english_text:`);
52744
+ lines.push(` ${c.english_text}`);
52745
+ lines.push(``);
52746
+ lines.push(`compiled_rule:`);
52747
+ lines.push(` kind: ${c.compiled_rule.kind}`);
52748
+ if (c.compiled_rule.operation !== void 0) {
52749
+ lines.push(` operation: ${c.compiled_rule.operation}`);
52750
+ }
52751
+ if (c.compiled_rule.tier2_update !== void 0) {
52752
+ lines.push(` tier2_update.field: ${c.compiled_rule.tier2_update.field}`);
52753
+ lines.push(` tier2_update.value: ${String(c.compiled_rule.tier2_update.value)}`);
52754
+ }
52755
+ lines.push(``);
52756
+ lines.push(`explanation:`);
52757
+ lines.push(` ${c.explanation_paragraph}`);
52758
+ if (c.compile_warnings.length > 0) {
52759
+ lines.push(``);
52760
+ lines.push(`warnings:`);
52761
+ for (const w of c.compile_warnings) lines.push(` - ${w}`);
52762
+ }
52763
+ return lines.join("\n");
52764
+ }
52765
+ var init_policy2 = __esm({
52766
+ "src/cli/policy.ts"() {
52767
+ init_audit_log();
52768
+ init_memory();
52769
+ init_random();
52770
+ init_english_policy_compiler();
52771
+ }
52772
+ });
52773
+
52774
+ // src/auto-trigger/types.ts
52775
+ function defaultRuleConfig(ruleId, ruleType, fortressId, now = () => /* @__PURE__ */ new Date()) {
52776
+ return {
52777
+ rule_id: ruleId,
52778
+ rule_type: ruleType,
52779
+ fortress_id: fortressId,
52780
+ current_rung: 1,
52781
+ threshold_overrides: {},
52782
+ cancel_window_seconds: DEFAULT_CANCEL_WINDOW_SECONDS,
52783
+ history: [],
52784
+ updated_at: now().toISOString()
52785
+ };
52786
+ }
52787
+ function appendHistory(history, entry) {
52788
+ const next = [...history, entry];
52789
+ if (next.length <= MAX_HISTORY_PER_RULE) return next;
52790
+ return next.slice(next.length - MAX_HISTORY_PER_RULE);
52791
+ }
52792
+ var DEFAULT_CANCEL_WINDOW_SECONDS, MAX_HISTORY_PER_RULE, AutoTriggerError;
52793
+ var init_types7 = __esm({
52794
+ "src/auto-trigger/types.ts"() {
52795
+ DEFAULT_CANCEL_WINDOW_SECONDS = 60;
52796
+ MAX_HISTORY_PER_RULE = 200;
52797
+ AutoTriggerError = class extends Error {
52798
+ constructor(message, code) {
52799
+ super(message);
52800
+ this.code = code;
52801
+ this.name = "AutoTriggerError";
52802
+ }
52803
+ code;
52804
+ };
52805
+ }
52806
+ });
52807
+
52808
+ // src/auto-trigger/threshold-config-store.ts
52809
+ function ruleStorageKey(ruleId) {
52810
+ return `${AUTO_TRIGGER_RULE_KEY_PREFIX}${ruleId}`;
52811
+ }
52812
+ function aadFor2(ruleId, fortressId) {
52813
+ return `${ruleId}|${fortressId}`;
52814
+ }
52815
+ var AUTO_TRIGGER_RULES_NAMESPACE, AUTO_TRIGGER_RULE_KEY_PREFIX, HKDF_INFO8, MAX_RULE_BYTES, ThresholdConfigStore;
52816
+ var init_threshold_config_store = __esm({
52817
+ "src/auto-trigger/threshold-config-store.ts"() {
52818
+ init_encryption();
52819
+ init_key_derivation();
52820
+ init_encoding();
52821
+ init_types7();
52822
+ AUTO_TRIGGER_RULES_NAMESPACE = "_auto_trigger_rules";
52823
+ AUTO_TRIGGER_RULE_KEY_PREFIX = "rule.";
52824
+ HKDF_INFO8 = "l2-auto-trigger-rules-v1";
52825
+ MAX_RULE_BYTES = 256 * 1024;
52826
+ ThresholdConfigStore = class {
52827
+ storage;
52828
+ encryptionKey;
52829
+ fortressId;
52830
+ now;
52831
+ constructor(opts) {
52832
+ this.storage = opts.storage;
52833
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO8);
52834
+ this.fortressId = opts.fortressId;
52835
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
52836
+ }
52837
+ /** Read a rule config. Returns null when absent. */
52838
+ async get(ruleId) {
52839
+ const key = ruleStorageKey(ruleId);
52840
+ let raw;
52841
+ try {
52842
+ raw = await this.storage.read(AUTO_TRIGGER_RULES_NAMESPACE, key);
52843
+ } catch {
52844
+ return null;
52845
+ }
52846
+ if (!raw) return null;
52847
+ if (raw.length > MAX_RULE_BYTES) return null;
52848
+ try {
52849
+ const aad = stringToBytes(aadFor2(ruleId, this.fortressId));
52850
+ const envelope = JSON.parse(bytesToString(raw));
52851
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
52852
+ const persisted = JSON.parse(
52853
+ bytesToString(plaintext)
52854
+ );
52855
+ if (persisted.version !== 1) return null;
52856
+ if (persisted.rule_id !== ruleId) return null;
52857
+ if (persisted.fortress_id !== this.fortressId) return null;
52858
+ return persisted.config;
52859
+ } catch {
52860
+ return null;
52861
+ }
52862
+ }
52863
+ /**
52864
+ * Read a rule config, creating a fresh Rung-1 default if absent. The
52865
+ * default is persisted on first access so the dispatcher sees a
52866
+ * consistent shape on subsequent reads.
52867
+ */
52868
+ async getOrInit(ruleId, ruleType) {
52869
+ const existing = await this.get(ruleId);
52870
+ if (existing) return existing;
52871
+ const fresh = defaultRuleConfig(
52872
+ ruleId,
52873
+ ruleType,
52874
+ this.fortressId,
52875
+ this.now
52876
+ );
52877
+ await this.set(fresh);
52878
+ return fresh;
52879
+ }
52880
+ /** Persist a rule config. AAD-binds to (rule_id, fortress_id). */
52881
+ async set(config) {
52882
+ if (config.fortress_id !== this.fortressId) {
52883
+ throw new Error(
52884
+ `ThresholdConfigStore: fortress_id mismatch (got ${config.fortress_id}, store bound to ${this.fortressId})`
52885
+ );
52886
+ }
52887
+ const persisted = {
52888
+ version: 1,
52889
+ rule_id: config.rule_id,
52890
+ fortress_id: this.fortressId,
52891
+ saved_at: this.now().toISOString(),
52892
+ config: {
52893
+ ...config,
52894
+ updated_at: this.now().toISOString()
52895
+ }
52896
+ };
52897
+ const aad = stringToBytes(aadFor2(config.rule_id, this.fortressId));
52898
+ const plaintext = stringToBytes(JSON.stringify(persisted));
52899
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
52900
+ await this.storage.write(
52901
+ AUTO_TRIGGER_RULES_NAMESPACE,
52902
+ ruleStorageKey(config.rule_id),
52903
+ stringToBytes(JSON.stringify(envelope))
52904
+ );
52905
+ }
52906
+ /** Promote a rule one rung up. Throws on ceiling (rung 3). */
52907
+ async promote(ruleId, ruleType) {
52908
+ const config = await this.getOrInit(ruleId, ruleType);
52909
+ if (config.current_rung >= 3) {
52910
+ throw new AutoTriggerError(
52911
+ `rule ${ruleId} already at rung 3 (ceiling)`,
52912
+ "rung_ceiling"
52913
+ );
52914
+ }
52915
+ const next = {
52916
+ ...config,
52917
+ current_rung: config.current_rung + 1,
52918
+ last_promoted_at: this.now().toISOString()
52919
+ };
52920
+ await this.set(next);
52921
+ return next;
52922
+ }
52923
+ /** Demote a rule one rung down. Throws on floor (rung 1). */
52924
+ async demote(ruleId, ruleType) {
52925
+ const config = await this.getOrInit(ruleId, ruleType);
52926
+ if (config.current_rung <= 1) {
52927
+ throw new AutoTriggerError(
52928
+ `rule ${ruleId} already at rung 1 (floor)`,
52929
+ "rung_floor"
52930
+ );
52931
+ }
52932
+ const next = {
52933
+ ...config,
52934
+ current_rung: config.current_rung - 1,
52935
+ last_demoted_at: this.now().toISOString()
52936
+ };
52937
+ await this.set(next);
52938
+ return next;
52939
+ }
52940
+ /** Patch the threshold overrides + optional cancel-window. */
52941
+ async updateConfig(ruleId, ruleType, patch) {
52942
+ const config = await this.getOrInit(ruleId, ruleType);
52943
+ const next = {
52944
+ ...config,
52945
+ threshold_overrides: patch.threshold_overrides ?? config.threshold_overrides,
52946
+ cancel_window_seconds: patch.cancel_window_seconds ?? config.cancel_window_seconds
52947
+ };
52948
+ await this.set(next);
52949
+ return next;
52950
+ }
52951
+ /**
52952
+ * Append an action history entry. Bounded ring buffer (max 200 per
52953
+ * rule); oldest entries drop.
52954
+ */
52955
+ async recordAction(ruleId, ruleType, entry) {
52956
+ const config = await this.getOrInit(ruleId, ruleType);
52957
+ const next = {
52958
+ ...config,
52959
+ history: appendHistory(config.history, entry)
52960
+ };
52961
+ await this.set(next);
52962
+ return next;
52963
+ }
52964
+ /**
52965
+ * Update the outcome of a specific pending history entry (e.g. when a
52966
+ * cancel-window expires or the operator cancels). Identified by
52967
+ * finding_id. No-op when the entry is absent. Returns the updated
52968
+ * config; the matched entry's outcome is replaced.
52969
+ */
52970
+ async updateActionOutcome(ruleId, ruleType, findingId, outcome) {
52971
+ const config = await this.getOrInit(ruleId, ruleType);
52972
+ const idx = config.history.findIndex((h) => h.finding_id === findingId);
52973
+ if (idx < 0) return config;
52974
+ const updatedHistory = [...config.history];
52975
+ updatedHistory[idx] = { ...updatedHistory[idx], outcome };
52976
+ const next = { ...config, history: updatedHistory };
52977
+ await this.set(next);
52978
+ return next;
52979
+ }
52980
+ /** Delete a rule config (and its history) by id. */
52981
+ async delete(ruleId) {
52982
+ const key = ruleStorageKey(ruleId);
52983
+ const exists = await this.storage.exists(
52984
+ AUTO_TRIGGER_RULES_NAMESPACE,
52985
+ key
52986
+ );
52987
+ if (!exists) return false;
52988
+ try {
52989
+ await this.storage.delete(AUTO_TRIGGER_RULES_NAMESPACE, key);
52990
+ } catch {
52991
+ return false;
52992
+ }
52993
+ return true;
52994
+ }
52995
+ /** Enumerate every rule id persisted for this fortress. */
52996
+ async listRuleIds() {
52997
+ const metas = await this.storage.list(
52998
+ AUTO_TRIGGER_RULES_NAMESPACE,
52999
+ AUTO_TRIGGER_RULE_KEY_PREFIX
53000
+ );
53001
+ const out = [];
53002
+ for (const meta of metas) {
53003
+ if (!meta.key.startsWith(AUTO_TRIGGER_RULE_KEY_PREFIX)) continue;
53004
+ out.push(meta.key.slice(AUTO_TRIGGER_RULE_KEY_PREFIX.length));
53005
+ }
53006
+ return out;
53007
+ }
53008
+ };
53009
+ }
53010
+ });
53011
+
53012
+ // src/cli/auto-trigger.ts
53013
+ var auto_trigger_exports = {};
53014
+ __export(auto_trigger_exports, {
53015
+ runAutoTriggerCommand: () => runAutoTriggerCommand
53016
+ });
53017
+ async function runAutoTriggerCommand(args) {
53018
+ const out = args.out ?? process.stdout;
53019
+ const err = args.err ?? process.stderr;
53020
+ const [sub, ...rest] = args.argv;
53021
+ if (!sub || sub === "--help" || sub === "-h") {
53022
+ printUsage10(out);
53023
+ return 0;
53024
+ }
53025
+ try {
53026
+ switch (sub) {
53027
+ case "rules":
53028
+ return await cmdRules(rest, { out, err, args });
53029
+ case "cancel":
53030
+ return await cmdCancel(rest, { out, err });
53031
+ default:
53032
+ err.write(`Unknown subcommand: ${sub}
53033
+ `);
53034
+ printUsage10(err);
53035
+ return 2;
53036
+ }
53037
+ } catch (cause) {
53038
+ const msg = cause instanceof Error ? cause.message : String(cause);
53039
+ err.write(`sanctuary auto-trigger: ${msg}
53040
+ `);
53041
+ return 1;
53042
+ }
53043
+ }
53044
+ function printUsage10(s) {
53045
+ s.write(`Usage: sanctuary auto-trigger <command> [args]
53046
+
53047
+ rules list List rules + current rungs.
53048
+ rules show <rule_id> Full detail + history.
53049
+ rules promote <rule_id> --rule-type <t> Rung N -> N+1.
53050
+ rules demote <rule_id> --rule-type <t> Rung N -> N-1.
53051
+ rules set-threshold <rule_id> --rule-type <t>
53052
+ [--warn-sigma <n>] [--alert-sigma <n>]
53053
+ [--cancel-window <s>] Update overrides + window.
53054
+ cancel <finding_id> Cancel a pending rung-2
53055
+ action (HTTP-delegated to
53056
+ the local dashboard).
53057
+
53058
+ Rule types: sentinel | anomaly | honeypot
53059
+
53060
+ Env (for cancel): SANCTUARY_DASHBOARD_URL, SANCTUARY_DASHBOARD_AUTH_TOKEN
53061
+ `);
53062
+ }
53063
+ async function cmdRules(argv, ctx) {
53064
+ const sub = argv[0];
53065
+ if (!sub) {
53066
+ ctx.err.write("rules subcommand required\n");
53067
+ return 2;
53068
+ }
53069
+ switch (sub) {
53070
+ case "list":
53071
+ return await cmdRulesList(ctx);
53072
+ case "show":
53073
+ return await cmdRulesShow(argv.slice(1), ctx);
53074
+ case "promote":
53075
+ return await cmdRulesPromote(argv.slice(1), ctx);
53076
+ case "demote":
53077
+ return await cmdRulesDemote(argv.slice(1), ctx);
53078
+ case "set-threshold":
53079
+ return await cmdRulesSetThreshold(argv.slice(1), ctx);
53080
+ default:
53081
+ ctx.err.write(`Unknown rules subcommand: ${sub}
53082
+ `);
53083
+ return 2;
53084
+ }
53085
+ }
53086
+ async function cmdRulesList(ctx) {
53087
+ const { store } = await openStore(ctx.args);
53088
+ const ids = await store.listRuleIds();
53089
+ if (ids.length === 0) {
53090
+ ctx.out.write("(no rules configured; defaults are Rung 1 / no overrides)\n");
53091
+ return 0;
53092
+ }
53093
+ for (const id of ids) {
53094
+ const config = await store.get(id);
53095
+ if (!config) continue;
53096
+ const overrides = Object.keys(config.threshold_overrides).length === 0 ? "defaults" : JSON.stringify(config.threshold_overrides);
53097
+ ctx.out.write(
53098
+ `${id} [type: ${config.rule_type}] rung=${config.current_rung} cancel-window=${config.cancel_window_seconds}s overrides=${overrides}
53099
+ `
53100
+ );
53101
+ }
53102
+ return 0;
53103
+ }
53104
+ async function cmdRulesShow(argv, ctx) {
53105
+ const ruleId = argv[0];
53106
+ if (!ruleId) {
53107
+ ctx.err.write("show requires a rule_id\n");
53108
+ return 2;
53109
+ }
53110
+ const { store } = await openStore(ctx.args);
53111
+ const config = await store.get(ruleId);
53112
+ if (!config) {
53113
+ ctx.err.write(`rule not found: ${ruleId}
53114
+ `);
53115
+ return 1;
53116
+ }
53117
+ ctx.out.write(JSON.stringify(config, null, 2) + "\n");
53118
+ return 0;
53119
+ }
53120
+ async function cmdRulesPromote(argv, ctx) {
53121
+ const ruleId = argv[0];
53122
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53123
+ if (!ruleId) {
53124
+ ctx.err.write("promote requires a rule_id\n");
53125
+ return 2;
53126
+ }
53127
+ if (!ruleType) {
53128
+ ctx.err.write(
53129
+ "promote requires --rule-type (sentinel|anomaly|honeypot)\n"
53130
+ );
53131
+ return 2;
53132
+ }
53133
+ const { store } = await openStore(ctx.args);
53134
+ try {
53135
+ const after = await store.promote(ruleId, ruleType);
53136
+ ctx.out.write(`Promoted ${ruleId} -> rung ${after.current_rung}
53137
+ `);
53138
+ return 0;
53139
+ } catch (err) {
53140
+ if (err instanceof AutoTriggerError) {
53141
+ ctx.err.write(`${err.code}: ${err.message}
53142
+ `);
53143
+ return 1;
53144
+ }
53145
+ throw err;
53146
+ }
53147
+ }
53148
+ async function cmdRulesDemote(argv, ctx) {
53149
+ const ruleId = argv[0];
53150
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53151
+ if (!ruleId) {
53152
+ ctx.err.write("demote requires a rule_id\n");
53153
+ return 2;
53154
+ }
53155
+ if (!ruleType) {
53156
+ ctx.err.write(
53157
+ "demote requires --rule-type (sentinel|anomaly|honeypot)\n"
53158
+ );
53159
+ return 2;
53160
+ }
53161
+ const { store } = await openStore(ctx.args);
53162
+ try {
53163
+ const after = await store.demote(ruleId, ruleType);
53164
+ ctx.out.write(`Demoted ${ruleId} -> rung ${after.current_rung}
53165
+ `);
53166
+ return 0;
53167
+ } catch (err) {
53168
+ if (err instanceof AutoTriggerError) {
53169
+ ctx.err.write(`${err.code}: ${err.message}
53170
+ `);
53171
+ return 1;
53172
+ }
53173
+ throw err;
53174
+ }
53175
+ }
53176
+ async function cmdRulesSetThreshold(argv, ctx) {
53177
+ const ruleId = argv[0];
53178
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53179
+ if (!ruleId) {
53180
+ ctx.err.write("set-threshold requires a rule_id\n");
53181
+ return 2;
53182
+ }
53183
+ if (!ruleType) {
53184
+ ctx.err.write(
53185
+ "set-threshold requires --rule-type (sentinel|anomaly|honeypot)\n"
53186
+ );
53187
+ return 2;
53188
+ }
53189
+ const warnSigma = parseNumberFlag(argv, "--warn-sigma");
53190
+ const alertSigma = parseNumberFlag(argv, "--alert-sigma");
53191
+ const cancelWindow = parseNumberFlag(argv, "--cancel-window");
53192
+ const overrides = {};
53193
+ if (warnSigma !== void 0) overrides.warn_sigma = warnSigma;
53194
+ if (alertSigma !== void 0) overrides.alert_sigma = alertSigma;
53195
+ if (Object.keys(overrides).length === 0 && cancelWindow === void 0) {
53196
+ ctx.err.write(
53197
+ "set-threshold requires at least one of --warn-sigma, --alert-sigma, --cancel-window\n"
53198
+ );
53199
+ return 2;
53200
+ }
53201
+ const { store } = await openStore(ctx.args);
53202
+ const patch = {};
53203
+ if (Object.keys(overrides).length > 0) patch.threshold_overrides = overrides;
53204
+ if (cancelWindow !== void 0) patch.cancel_window_seconds = cancelWindow;
53205
+ const after = await store.updateConfig(ruleId, ruleType, patch);
53206
+ ctx.out.write(
53207
+ `Updated ${ruleId}: overrides=${JSON.stringify(after.threshold_overrides)} cancel-window=${after.cancel_window_seconds}s
53208
+ `
53209
+ );
53210
+ return 0;
53211
+ }
53212
+ async function cmdCancel(argv, ctx) {
53213
+ const findingId = argv[0];
53214
+ if (!findingId) {
53215
+ ctx.err.write("cancel requires a finding_id\n");
53216
+ return 2;
53217
+ }
53218
+ const baseUrl = process.env["SANCTUARY_DASHBOARD_URL"] ?? "http://127.0.0.1:3501";
53219
+ const token = process.env["SANCTUARY_DASHBOARD_AUTH_TOKEN"];
53220
+ const headers = {
53221
+ "Content-Type": "application/json"
53222
+ };
53223
+ if (token) headers["Authorization"] = `Bearer ${token}`;
53224
+ let res;
53225
+ try {
53226
+ res = await fetch(`${baseUrl}/api/auto-trigger/cancel/${encodeURIComponent(findingId)}`, {
53227
+ method: "POST",
53228
+ headers
53229
+ });
53230
+ } catch (err) {
53231
+ const msg = err instanceof Error ? err.message : String(err);
53232
+ ctx.err.write(`cancel: cannot reach dashboard at ${baseUrl}: ${msg}
53233
+ `);
53234
+ return 1;
53235
+ }
53236
+ if (res.status === 200) {
53237
+ ctx.out.write(`Canceled pending action: ${findingId}
53238
+ `);
53239
+ return 0;
53240
+ }
53241
+ if (res.status === 404) {
53242
+ ctx.err.write(
53243
+ `No pending action for ${findingId} (window may have expired)
53244
+ `
53245
+ );
53246
+ return 1;
53247
+ }
53248
+ if (res.status === 401) {
53249
+ ctx.err.write(
53250
+ `cancel: unauthorized (set SANCTUARY_DASHBOARD_AUTH_TOKEN)
53251
+ `
53252
+ );
53253
+ return 1;
53254
+ }
53255
+ ctx.err.write(`cancel: HTTP ${res.status}
53256
+ `);
53257
+ return 1;
53258
+ }
53259
+ function flagValue5(argv, name) {
53260
+ const i = argv.indexOf(name);
53261
+ if (i === -1) return void 0;
53262
+ return argv[i + 1];
53263
+ }
53264
+ function parseNumberFlag(argv, name) {
53265
+ const raw = flagValue5(argv, name);
53266
+ if (raw === void 0) return void 0;
53267
+ const n = Number.parseFloat(raw);
53268
+ if (Number.isNaN(n)) return void 0;
53269
+ return n;
53270
+ }
53271
+ function parseRuleType(raw) {
53272
+ if (raw === "sentinel" || raw === "anomaly" || raw === "honeypot") return raw;
53273
+ return null;
53274
+ }
53275
+ async function resolveStoragePath4(args) {
53276
+ if (args.storagePath) return args.storagePath;
53277
+ const config = await loadConfig();
53278
+ return config.storage_path;
53279
+ }
53280
+ async function openStore(args) {
53281
+ const storagePath = await resolveStoragePath4(args);
53282
+ const storage = new FilesystemStorage(`${storagePath}/state`);
53283
+ let passphrase = args.passphrase ?? process.env["SANCTUARY_PASSPHRASE"];
53284
+ if (!passphrase) {
53285
+ const resolved = await getOrCreatePassphrase();
53286
+ passphrase = resolved.value;
53287
+ }
53288
+ let existingParams;
53289
+ try {
53290
+ const raw = await storage.read("_meta", "key-params");
53291
+ if (raw) existingParams = JSON.parse(bytesToString(raw));
53292
+ } catch {
53293
+ }
53294
+ const { key: masterKey, params } = await deriveMasterKey(
53295
+ passphrase,
53296
+ existingParams
53297
+ );
53298
+ if (!existingParams) {
53299
+ await storage.write(
53300
+ "_meta",
53301
+ "key-params",
53302
+ stringToBytes(JSON.stringify(params))
53303
+ );
53304
+ }
53305
+ const fortressId = fortressIdFromStoragePath(storagePath);
53306
+ const store = new ThresholdConfigStore({
53307
+ storage,
53308
+ masterKey,
53309
+ fortressId
53310
+ });
53311
+ return { store };
53312
+ }
53313
+ var init_auto_trigger = __esm({
53314
+ "src/cli/auto-trigger.ts"() {
53315
+ init_config();
53316
+ init_filesystem();
53317
+ init_key_derivation();
53318
+ init_encoding();
53319
+ init_passphrase();
53320
+ init_wiring();
53321
+ init_threshold_config_store();
53322
+ init_types7();
51169
53323
  }
51170
53324
  });
51171
53325
 
@@ -52043,6 +54197,16 @@ async function main() {
52043
54197
  const code = await runAnomalyCommand2({ argv: args.slice(1) });
52044
54198
  process.exit(code);
52045
54199
  }
54200
+ if (args[0] === "policy") {
54201
+ const { runPolicyCommand: runPolicyCommand2 } = await Promise.resolve().then(() => (init_policy2(), policy_exports));
54202
+ const code = await runPolicyCommand2({ argv: args.slice(1) });
54203
+ process.exit(code);
54204
+ }
54205
+ if (args[0] === "auto-trigger") {
54206
+ const { runAutoTriggerCommand: runAutoTriggerCommand2 } = await Promise.resolve().then(() => (init_auto_trigger(), auto_trigger_exports));
54207
+ const code = await runAutoTriggerCommand2({ argv: args.slice(1) });
54208
+ process.exit(code);
54209
+ }
52046
54210
  if (args[0] === "broker-server") {
52047
54211
  const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
52048
54212
  const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));