@sanctuary-framework/mcp-server 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -18381,6 +18381,552 @@ var init_handoff_routes = __esm({
18381
18381
  };
18382
18382
  }
18383
18383
  });
18384
+
18385
+ // src/honeypot/types.ts
18386
+ function honeypotSentinelId(trapId) {
18387
+ return `${HONEYPOT_SENTINEL_ID_PREFIX}${trapId}`;
18388
+ }
18389
+ var FILESYSTEM_OPS, HONEYPOT_AUDIT_OPS, HONEYPOT_SENTINEL_ID_PREFIX;
18390
+ var init_types3 = __esm({
18391
+ "src/honeypot/types.ts"() {
18392
+ FILESYSTEM_OPS = [
18393
+ "read",
18394
+ "write",
18395
+ "delete",
18396
+ "list"
18397
+ ];
18398
+ HONEYPOT_AUDIT_OPS = {
18399
+ DRAFTED: "honeypot_drafted",
18400
+ COMPILED: "honeypot_compiled",
18401
+ DEPLOYED: "honeypot_deployed",
18402
+ TRIGGERED: "honeypot_triggered",
18403
+ UNDEPLOYED: "honeypot_undeployed",
18404
+ LOADED: "honeypot_loaded"
18405
+ };
18406
+ HONEYPOT_SENTINEL_ID_PREFIX = "honeypot:";
18407
+ }
18408
+ });
18409
+ async function compileHoneypot(draft, opts) {
18410
+ const now = opts?.now ?? (() => /* @__PURE__ */ new Date());
18411
+ const trapIdFactory = opts?.trapIdFactory ?? (() => randomUUID());
18412
+ const warnings = [];
18413
+ let trigger = null;
18414
+ let trapClass = "http_endpoint";
18415
+ let severity = DEFAULT_SEVERITY;
18416
+ let explanation = "";
18417
+ let source = "heuristic";
18418
+ if (opts?.selector) {
18419
+ try {
18420
+ const handle = await opts.selector.getSubstrate(COMPILE_SURFACE);
18421
+ if (handle.capability.summarize) {
18422
+ const response = await opts.selector.invokeSummarize(
18423
+ COMPILE_SURFACE,
18424
+ {
18425
+ kind: "summarize",
18426
+ context: COMPILE_PROMPT,
18427
+ query: draft.english_text,
18428
+ maxTokens: COMPILE_MAX_TOKENS
18429
+ }
18430
+ );
18431
+ if (response.body.kind === "summarize" && !response.failureClass) {
18432
+ const parsed = tryParseLlmResponse(response.body.text);
18433
+ if (parsed.ok) {
18434
+ trigger = parsed.trigger;
18435
+ trapClass = parsed.trapClass;
18436
+ severity = parsed.severity;
18437
+ explanation = parsed.explanation;
18438
+ source = "llm";
18439
+ } else {
18440
+ warnings.push(
18441
+ `LLM response failed validation (${parsed.failure}); falling back to heuristic compile`
18442
+ );
18443
+ }
18444
+ } else {
18445
+ warnings.push(
18446
+ `LLM compile failed (${response.failureClass ?? "non_summarize_body"}); falling back to heuristic compile`
18447
+ );
18448
+ }
18449
+ } else {
18450
+ warnings.push(
18451
+ "Substrate at template-suggestion surface does not support summarize; falling back to heuristic compile"
18452
+ );
18453
+ }
18454
+ } catch (err) {
18455
+ const message = err instanceof Error ? err.message : String(err);
18456
+ warnings.push(
18457
+ `LLM compile threw (${message}); falling back to heuristic compile`
18458
+ );
18459
+ }
18460
+ }
18461
+ if (trigger === null) {
18462
+ const heuristic = heuristicCompile(draft.english_text);
18463
+ trigger = heuristic.trigger;
18464
+ trapClass = heuristic.trapClass;
18465
+ if (heuristic.severity) severity = heuristic.severity;
18466
+ explanation = heuristic.explanation;
18467
+ if (heuristic.warning) warnings.push(heuristic.warning);
18468
+ }
18469
+ const spec = {
18470
+ trap_id: trapIdFactory(),
18471
+ trap_class: trapClass,
18472
+ trigger,
18473
+ finding_severity: severity,
18474
+ english_text: draft.english_text,
18475
+ explanation_paragraph: explanation,
18476
+ compiled_at: now().toISOString()
18477
+ };
18478
+ return { spec, source, warnings };
18479
+ }
18480
+ function tryParseLlmResponse(text) {
18481
+ let body;
18482
+ try {
18483
+ const stripped = text.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
18484
+ body = JSON.parse(stripped);
18485
+ } catch {
18486
+ return { ok: false, failure: "invalid_json" };
18487
+ }
18488
+ if (!body || typeof body !== "object") {
18489
+ return { ok: false, failure: "invalid_json" };
18490
+ }
18491
+ const obj = body;
18492
+ const pathPattern = obj["path_pattern"];
18493
+ if (typeof pathPattern !== "string" || pathPattern.length === 0) {
18494
+ return { ok: false, failure: "missing_path_pattern" };
18495
+ }
18496
+ const callerTypes = Array.isArray(obj["expected_caller_types"]) ? obj["expected_caller_types"].filter(
18497
+ (v) => typeof v === "string" && v.length > 0
18498
+ ) : ["wrapped_agent"];
18499
+ if (callerTypes.length === 0) {
18500
+ return { ok: false, failure: "invalid_caller_types" };
18501
+ }
18502
+ const severityRaw = obj["finding_severity"];
18503
+ const severity = severityRaw === "warn" ? "warn" : severityRaw === "alert" ? "alert" : DEFAULT_SEVERITY;
18504
+ const explanationRaw = obj["explanation_paragraph"];
18505
+ const explanation = typeof explanationRaw === "string" && explanationRaw.length > 0 ? explanationRaw : "Honeypot compiled from operator draft via LLM-assisted compile path.";
18506
+ const trapClassRaw = obj["trap_class"];
18507
+ const trapClass = trapClassRaw === "filesystem" ? "filesystem" : "http_endpoint";
18508
+ if (trapClass === "filesystem") {
18509
+ const opsParsed = parseFilesystemOps(obj["ops"]);
18510
+ if (opsParsed === null) {
18511
+ return { ok: false, failure: "invalid_filesystem_ops" };
18512
+ }
18513
+ const trigger2 = {
18514
+ kind: "filesystem",
18515
+ path_pattern: pathPattern,
18516
+ ops: opsParsed,
18517
+ expected_caller_types: callerTypes
18518
+ };
18519
+ return { ok: true, trapClass, trigger: trigger2, severity, explanation };
18520
+ }
18521
+ const method = typeof obj["method"] === "string" ? obj["method"] : "ANY";
18522
+ const trigger = {
18523
+ kind: "http_endpoint",
18524
+ path_pattern: pathPattern,
18525
+ ...method !== "ANY" ? { method: method.toUpperCase() } : {},
18526
+ expected_caller_types: callerTypes
18527
+ };
18528
+ return { ok: true, trapClass, trigger, severity, explanation };
18529
+ }
18530
+ function parseFilesystemOps(raw) {
18531
+ if (raw === void 0 || raw === null) {
18532
+ return [...FILESYSTEM_OPS];
18533
+ }
18534
+ if (!Array.isArray(raw)) return null;
18535
+ if (raw.length === 0) return [...FILESYSTEM_OPS];
18536
+ const out = [];
18537
+ for (const entry of raw) {
18538
+ if (typeof entry !== "string") return null;
18539
+ if (!FILESYSTEM_OPS.includes(entry)) return null;
18540
+ if (!out.includes(entry)) out.push(entry);
18541
+ }
18542
+ return out;
18543
+ }
18544
+ function heuristicCompile(english) {
18545
+ let pathPattern = null;
18546
+ for (const re of HEURISTIC_PATH_PATTERNS) {
18547
+ const match = english.match(re);
18548
+ if (match && match[1]) {
18549
+ pathPattern = match[1];
18550
+ break;
18551
+ }
18552
+ }
18553
+ const fallbackUsed = pathPattern === null;
18554
+ if (pathPattern === null) {
18555
+ pathPattern = "/honeypot-stub";
18556
+ }
18557
+ let severity;
18558
+ for (const hint of SEVERITY_HINTS) {
18559
+ if (hint.phrase.test(english)) {
18560
+ severity = hint.severity;
18561
+ break;
18562
+ }
18563
+ }
18564
+ const isFilesystem = FILESYSTEM_CLASS_HINTS.some((re) => re.test(english));
18565
+ if (isFilesystem) {
18566
+ const ops = [];
18567
+ for (const hint of FILESYSTEM_OP_HINTS) {
18568
+ if (hint.phrase.test(english) && !ops.includes(hint.op)) {
18569
+ ops.push(hint.op);
18570
+ }
18571
+ }
18572
+ const resolvedOps = ops.length > 0 ? ops : [...FILESYSTEM_OPS];
18573
+ const trigger2 = {
18574
+ kind: "filesystem",
18575
+ path_pattern: pathPattern,
18576
+ ops: resolvedOps,
18577
+ expected_caller_types: ["wrapped_agent"]
18578
+ };
18579
+ const opsRendered = resolvedOps.join(",");
18580
+ 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}.`;
18581
+ return {
18582
+ trapClass: "filesystem",
18583
+ trigger: trigger2,
18584
+ ...severity !== void 0 ? { severity } : {},
18585
+ explanation: explanation2,
18586
+ ...fallbackUsed ? {
18587
+ 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"
18588
+ } : {}
18589
+ };
18590
+ }
18591
+ const trigger = {
18592
+ kind: "http_endpoint",
18593
+ path_pattern: pathPattern,
18594
+ expected_caller_types: ["wrapped_agent"]
18595
+ };
18596
+ 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}.`;
18597
+ return {
18598
+ trapClass: "http_endpoint",
18599
+ trigger,
18600
+ ...severity !== void 0 ? { severity } : {},
18601
+ explanation,
18602
+ ...fallbackUsed ? {
18603
+ 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"
18604
+ } : {}
18605
+ };
18606
+ }
18607
+ function hashOfEnglishDraft(text) {
18608
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32);
18609
+ }
18610
+ var COMPILE_SURFACE, COMPILE_MAX_TOKENS, DEFAULT_SEVERITY, COMPILE_PROMPT, HEURISTIC_PATH_PATTERNS, SEVERITY_HINTS, FILESYSTEM_CLASS_HINTS, FILESYSTEM_OP_HINTS;
18611
+ var init_honeypot_compiler = __esm({
18612
+ "src/honeypot/honeypot-compiler.ts"() {
18613
+ init_types3();
18614
+ COMPILE_SURFACE = "template-suggestion";
18615
+ COMPILE_MAX_TOKENS = 800;
18616
+ DEFAULT_SEVERITY = "alert";
18617
+ COMPILE_PROMPT = `You are compiling a Sanctuary honeypot from an operator's plain-English description.
18618
+ Return STRICT JSON with the following shape (no markdown, no commentary):
18619
+ {
18620
+ "trap_class": "http_endpoint" | "filesystem",
18621
+ "path_pattern": "string (glob with * or **)",
18622
+ "method": "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY",
18623
+ "ops": ["read", "write", "delete", "list"],
18624
+ "expected_caller_types": ["wrapped_agent" | "operator" | "external"],
18625
+ "finding_severity": "warn" | "alert",
18626
+ "explanation_paragraph": "one-sentence operator-friendly explanation"
18627
+ }
18628
+ 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.`;
18629
+ HEURISTIC_PATH_PATTERNS = [
18630
+ /honeypot\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
18631
+ /trap\s+(?:at|on|for)\s+([\/][\w\/\-:*\.]+)/i,
18632
+ /deploy\s+(?:at|on)\s+([\/][\w\/\-:*\.]+)/i,
18633
+ /catch\s+(?:requests?\s+to|callers?\s+at)\s+([\/][\w\/\-:*\.]+)/i,
18634
+ /watch\s+(?:for\s+)?(?:requests?\s+(?:to|on))\s+([\/][\w\/\-:*\.]+)/i,
18635
+ /([\/][\w\/\-:*\.]+)\s+(?:endpoint|path|route)/i
18636
+ ];
18637
+ SEVERITY_HINTS = [
18638
+ { phrase: /\b(?:warn|warning|low\s+severity)\b/i, severity: "warn" },
18639
+ { phrase: /\b(?:alert|critical|high\s+severity)\b/i, severity: "alert" }
18640
+ ];
18641
+ FILESYSTEM_CLASS_HINTS = [
18642
+ /\bfilesystem\b/i,
18643
+ /\bfile[-\s]?system\b/i,
18644
+ /\bfile\s+(?:read|write|delete|list|access|trap|honeypot)/i,
18645
+ /\b(?:read|write|delete|list)\s+file/i,
18646
+ /\bdirectory\b/i,
18647
+ /\bon[-\s]?disk\b/i,
18648
+ /\bpath\s+on\s+disk\b/i
18649
+ ];
18650
+ FILESYSTEM_OP_HINTS = [
18651
+ { phrase: /\b(?:read|reads|reading|access(?:es|ed)?)\b/i, op: "read" },
18652
+ { phrase: /\b(?:write|writes|writing|modif(?:y|ies|ied)|edit)/i, op: "write" },
18653
+ { phrase: /\b(?:delete|deletes|deletion|remove|removal|unlink)/i, op: "delete" },
18654
+ { phrase: /\b(?:list|listing|enumerate|enumeration|directory\s+listing)/i, op: "list" }
18655
+ ];
18656
+ }
18657
+ });
18658
+ async function handleHoneypotTriggerIfMatch(deps, req, res) {
18659
+ const url = req.url ?? "/";
18660
+ const path = url.split("?")[0] ?? "/";
18661
+ const method = (req.method ?? "GET").toUpperCase();
18662
+ if (path.startsWith(HONEYPOT_API_PREFIX)) return false;
18663
+ if (path.startsWith("/api/sentinels")) return false;
18664
+ if (path.startsWith("/api/coordination")) return false;
18665
+ const match = deps.registry.findMatching({ path, method });
18666
+ if (!match) return false;
18667
+ const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
18668
+ const callerIdentity = extractCallerIdentity(req);
18669
+ const payloadHash = await safeReadAndHashBody(req);
18670
+ const findingId = randomUUID();
18671
+ const finding = {
18672
+ finding_id: findingId,
18673
+ sentinel_id: honeypotSentinelId(match.trap_id),
18674
+ severity: match.finding_severity,
18675
+ summary: buildSummary(match, callerIdentity, path, method),
18676
+ details: {
18677
+ trap_id: match.trap_id,
18678
+ trap_class: match.trap_class,
18679
+ path_matched: path,
18680
+ method,
18681
+ caller_identity: callerIdentity,
18682
+ payload_hash: payloadHash
18683
+ },
18684
+ observed_at: now.toISOString(),
18685
+ evidence_audit_ids: [],
18686
+ fortress_id: deps.fortressId
18687
+ };
18688
+ await deps.findingStore.saveFinding(finding).catch(() => void 0);
18689
+ deps.auditLog.append(
18690
+ "l2",
18691
+ HONEYPOT_AUDIT_OPS.TRIGGERED,
18692
+ deps.operatorId,
18693
+ {
18694
+ trap_id: match.trap_id,
18695
+ trap_class: match.trap_class,
18696
+ path_matched: path,
18697
+ method,
18698
+ caller_identity: callerIdentity,
18699
+ payload_hash: payloadHash,
18700
+ finding_id: findingId,
18701
+ severity: match.finding_severity
18702
+ }
18703
+ );
18704
+ res.writeHead(404, { "Content-Type": "application/json" });
18705
+ res.end(JSON.stringify({ error: "not_found", path }));
18706
+ return true;
18707
+ }
18708
+ function buildSummary(spec, callerIdentity, path, method) {
18709
+ return `honeypot ${spec.trap_id} triggered: ${method} ${path} from ${callerIdentity} (severity ${spec.finding_severity}, pattern ${spec.trigger.path_pattern})`;
18710
+ }
18711
+ function extractCallerIdentity(req) {
18712
+ const headers = req.headers;
18713
+ const agent = headers["x-sanctuary-agent"];
18714
+ if (typeof agent === "string" && agent.length > 0) return `agent:${agent}`;
18715
+ const xff = headers["x-forwarded-for"];
18716
+ if (typeof xff === "string" && xff.length > 0) {
18717
+ const first = xff.split(",")[0]?.trim();
18718
+ if (first) return `ip:${first}`;
18719
+ }
18720
+ const ip = req.socket.remoteAddress;
18721
+ return ip ? `ip:${ip}` : "ip:unknown";
18722
+ }
18723
+ async function safeReadAndHashBody(req) {
18724
+ const MAX_BYTES = 64 * 1024;
18725
+ try {
18726
+ const chunks = [];
18727
+ let total = 0;
18728
+ for await (const chunk of req) {
18729
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
18730
+ total += buf.length;
18731
+ if (total > MAX_BYTES) {
18732
+ return "unhashed";
18733
+ }
18734
+ chunks.push(buf);
18735
+ }
18736
+ if (chunks.length === 0) return "empty";
18737
+ const body = Buffer.concat(chunks);
18738
+ return createHash("sha256").update(body).digest("hex").slice(0, 32);
18739
+ } catch {
18740
+ return "unhashed";
18741
+ }
18742
+ }
18743
+ function writeJSON7(res, status, payload) {
18744
+ res.writeHead(status, {
18745
+ "Content-Type": "application/json",
18746
+ "Cache-Control": "no-store"
18747
+ });
18748
+ res.end(JSON.stringify(payload));
18749
+ }
18750
+ async function readJSONBody4(req) {
18751
+ const chunks = [];
18752
+ for await (const chunk of req) {
18753
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
18754
+ }
18755
+ if (chunks.length === 0) return void 0;
18756
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
18757
+ }
18758
+ function matchTrapIdRoute(path) {
18759
+ const prefix = `${HONEYPOT_API_PREFIX}/traps/`;
18760
+ if (!path.startsWith(prefix)) return null;
18761
+ const rest = path.slice(prefix.length);
18762
+ if (rest.length === 0 || rest.includes("/")) return null;
18763
+ return { trapId: decodeURIComponent(rest) };
18764
+ }
18765
+ async function handleHoneypotRoute(deps, req, res) {
18766
+ const host = req.headers.host || "localhost";
18767
+ const url = new URL(req.url ?? "/", `http://${host}`);
18768
+ const method = (req.method ?? "GET").toUpperCase();
18769
+ const path = url.pathname;
18770
+ if (path !== HONEYPOT_API_PREFIX && !path.startsWith(`${HONEYPOT_API_PREFIX}/`)) {
18771
+ return false;
18772
+ }
18773
+ const checkAuth = authMiddleware(deps.authConfig);
18774
+ if (!checkAuth(req, res, url)) return true;
18775
+ try {
18776
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/compile`) {
18777
+ const body = await readJSONBody4(req);
18778
+ const englishText = body && typeof body === "object" && typeof body["english_text"] === "string" ? body["english_text"] : "";
18779
+ if (englishText.length === 0) {
18780
+ writeJSON7(res, 400, {
18781
+ ok: false,
18782
+ error: "english_text required"
18783
+ });
18784
+ return true;
18785
+ }
18786
+ const draft = {
18787
+ english_text: englishText,
18788
+ operator_id: deps.operatorId,
18789
+ observed_at: (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
18790
+ };
18791
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DRAFTED, deps.operatorId, {
18792
+ fortress_id: deps.fortressId,
18793
+ english_hash: hashOfEnglishDraft(englishText)
18794
+ });
18795
+ const result = await compileHoneypot(draft, {
18796
+ ...deps.selector !== void 0 ? { selector: deps.selector } : {},
18797
+ ...deps.now !== void 0 ? { now: deps.now } : {}
18798
+ });
18799
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.COMPILED, deps.operatorId, {
18800
+ fortress_id: deps.fortressId,
18801
+ trap_id: result.spec.trap_id,
18802
+ source: result.source,
18803
+ warning_count: result.warnings.length
18804
+ });
18805
+ writeJSON7(res, 200, {
18806
+ ok: true,
18807
+ data: { spec: result.spec, source: result.source, warnings: result.warnings }
18808
+ });
18809
+ return true;
18810
+ }
18811
+ if (method === "POST" && path === `${HONEYPOT_API_PREFIX}/deploy`) {
18812
+ const body = await readJSONBody4(req);
18813
+ const spec = body && typeof body === "object" && body["spec"] ? body["spec"] : null;
18814
+ if (!spec || typeof spec.trap_id !== "string" || spec.trap_id.length === 0) {
18815
+ writeJSON7(res, 400, { ok: false, error: "spec.trap_id required" });
18816
+ return true;
18817
+ }
18818
+ const isNew = deps.registry.deploy(spec);
18819
+ let persistError = null;
18820
+ if (deps.store) {
18821
+ try {
18822
+ await deps.store.save(spec);
18823
+ } catch (err) {
18824
+ persistError = err instanceof Error ? err.message : String(err);
18825
+ }
18826
+ }
18827
+ deps.auditLog.append("l2", HONEYPOT_AUDIT_OPS.DEPLOYED, deps.operatorId, {
18828
+ fortress_id: deps.fortressId,
18829
+ trap_id: spec.trap_id,
18830
+ trap_class: spec.trap_class,
18831
+ path_pattern: spec.trigger.path_pattern,
18832
+ was_new: isNew,
18833
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
18834
+ });
18835
+ writeJSON7(res, 200, {
18836
+ ok: true,
18837
+ data: {
18838
+ trap_id: spec.trap_id,
18839
+ was_new: isNew,
18840
+ ...deps.store ? { persisted: persistError === null } : {},
18841
+ ...persistError !== null ? { persist_error: persistError } : {}
18842
+ }
18843
+ });
18844
+ return true;
18845
+ }
18846
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/traps`) {
18847
+ const traps = deps.registry.list();
18848
+ writeJSON7(res, 200, { ok: true, data: { traps } });
18849
+ return true;
18850
+ }
18851
+ const trapMatch = matchTrapIdRoute(path);
18852
+ if (method === "DELETE" && trapMatch) {
18853
+ const removed = deps.registry.undeploy(trapMatch.trapId);
18854
+ let persistError = null;
18855
+ if (deps.store) {
18856
+ try {
18857
+ await deps.store.delete(trapMatch.trapId);
18858
+ } catch (err) {
18859
+ persistError = err instanceof Error ? err.message : String(err);
18860
+ }
18861
+ }
18862
+ if (removed) {
18863
+ deps.auditLog.append(
18864
+ "l2",
18865
+ HONEYPOT_AUDIT_OPS.UNDEPLOYED,
18866
+ deps.operatorId,
18867
+ {
18868
+ fortress_id: deps.fortressId,
18869
+ trap_id: trapMatch.trapId,
18870
+ ...persistError !== null ? { persist_error: persistError, persisted: false } : deps.store ? { persisted: true } : {}
18871
+ }
18872
+ );
18873
+ }
18874
+ writeJSON7(res, removed ? 200 : 404, {
18875
+ ok: removed,
18876
+ data: {
18877
+ trap_id: trapMatch.trapId,
18878
+ removed,
18879
+ ...deps.store ? { persisted: persistError === null } : {},
18880
+ ...persistError !== null ? { persist_error: persistError } : {}
18881
+ }
18882
+ });
18883
+ return true;
18884
+ }
18885
+ if (method === "GET" && path === `${HONEYPOT_API_PREFIX}/findings`) {
18886
+ const since = url.searchParams.get("since") ?? void 0;
18887
+ const severityRaw = url.searchParams.get("severity") ?? void 0;
18888
+ const severity = isValidSeverity(severityRaw) ? severityRaw : void 0;
18889
+ const limit = parseLimit5(url.searchParams.get("limit"), 50, 500);
18890
+ const all = await deps.findingStore.listFindings({
18891
+ ...since !== void 0 ? { since } : {},
18892
+ ...severity !== void 0 ? { severity } : {},
18893
+ limit: 500
18894
+ });
18895
+ const honeypotFindings = all.filter(
18896
+ (f) => f.sentinel_id.startsWith(HONEYPOT_SENTINEL_ID_PREFIX)
18897
+ );
18898
+ writeJSON7(res, 200, {
18899
+ ok: true,
18900
+ data: { findings: honeypotFindings.slice(0, limit) }
18901
+ });
18902
+ return true;
18903
+ }
18904
+ writeJSON7(res, 404, { ok: false, error: "not_found", path });
18905
+ return true;
18906
+ } catch (err) {
18907
+ const msg = err instanceof Error ? err.message : String(err);
18908
+ writeJSON7(res, 500, { ok: false, error: "internal", detail: msg });
18909
+ return true;
18910
+ }
18911
+ }
18912
+ function isValidSeverity(value) {
18913
+ return value === "info" || value === "warn" || value === "alert";
18914
+ }
18915
+ function parseLimit5(raw, defaultValue, max) {
18916
+ if (raw === null || raw === "") return defaultValue;
18917
+ const parsed = Number.parseInt(raw, 10);
18918
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
18919
+ return Math.min(parsed, max);
18920
+ }
18921
+ var HONEYPOT_API_PREFIX;
18922
+ var init_runtime_trap_handler = __esm({
18923
+ "src/honeypot/runtime-trap-handler.ts"() {
18924
+ init_auth_middleware();
18925
+ init_types3();
18926
+ init_honeypot_compiler();
18927
+ HONEYPOT_API_PREFIX = "/api/honeypot";
18928
+ }
18929
+ });
18384
18930
  function isDashboardViewRoute(method, path) {
18385
18931
  if (method !== "GET") return false;
18386
18932
  return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
@@ -18397,6 +18943,7 @@ var init_dashboard = __esm({
18397
18943
  init_approval_aggregator_routes();
18398
18944
  init_sentinel_routes();
18399
18945
  init_handoff_routes();
18946
+ init_runtime_trap_handler();
18400
18947
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
18401
18948
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
18402
18949
  MAX_SESSIONS = 1e3;
@@ -18484,6 +19031,22 @@ var init_dashboard = __esm({
18484
19031
  workflowStateTracker = null;
18485
19032
  handoffAuditLog = null;
18486
19033
  handoffOperatorId = null;
19034
+ // v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: per-fortress trap registry
19035
+ // + finding store + audit log + operator id. Front-of-dispatch hook
19036
+ // consults the registry on every request; management routes at
19037
+ // /api/honeypot/* go through the dispatch path.
19038
+ honeypotRegistry = null;
19039
+ honeypotFindingStore = null;
19040
+ honeypotAuditLog = null;
19041
+ honeypotOperatorId = null;
19042
+ honeypotFortressId = null;
19043
+ honeypotSelector = null;
19044
+ // Pi-2: encrypted at-rest persistence for deployed honeypot traps.
19045
+ // When present, the management API's deploy + undeploy handlers
19046
+ // write through to the store; on fortress boot the host code calls
19047
+ // `store.loadAll()` and re-deploys the persisted specs into the
19048
+ // in-memory registry before this dashboard begins serving.
19049
+ honeypotStore = null;
18487
19050
  constructor(config) {
18488
19051
  this.config = config;
18489
19052
  this.authToken = config.auth_token;
@@ -18565,6 +19128,30 @@ var init_dashboard = __esm({
18565
19128
  this.handoffContextTransfer = opts.contextTransfer ?? null;
18566
19129
  this.workflowStateTracker = opts.workflowStateTracker ?? null;
18567
19130
  }
19131
+ /**
19132
+ * v1.3 WP-V1.3-5 Pi-1 Honeypot Authoring: bind the per-fortress
19133
+ * trap registry + finding store + audit log + operator id. Once
19134
+ * set, two surfaces activate:
19135
+ * 1. Front-of-dispatch trap-trigger hook: every request runs
19136
+ * through `handleHoneypotTriggerIfMatch` BEFORE legacy/v1.1/
19137
+ * sentinel/coordination routing. Matching traps return 404
19138
+ * and the request never reaches the regular dispatcher.
19139
+ * 2. Management API at /api/honeypot/* routes through
19140
+ * `handleHoneypotRoute`.
19141
+ *
19142
+ * The optional `selector` opt wires the LLM compile path; absent
19143
+ * selector forces the heuristic compile path (which still produces
19144
+ * a usable TrapSpec with warnings).
19145
+ */
19146
+ setHoneypotRegistry(opts) {
19147
+ this.honeypotRegistry = opts.registry;
19148
+ this.honeypotFindingStore = opts.findingStore ?? null;
19149
+ this.honeypotAuditLog = opts.auditLog ?? null;
19150
+ this.honeypotOperatorId = opts.operatorId ?? null;
19151
+ this.honeypotFortressId = opts.fortressId ?? null;
19152
+ this.honeypotSelector = opts.selector ?? null;
19153
+ this.honeypotStore = opts.store ?? null;
19154
+ }
18568
19155
  /**
18569
19156
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
18570
19157
  * before the legacy approval route table. Returns true when served.
@@ -18629,6 +19216,57 @@ var init_dashboard = __esm({
18629
19216
  res
18630
19217
  );
18631
19218
  }
19219
+ /**
19220
+ * v1.3 WP-V1.3-5 Pi-1 dispatch entry point. Routes
19221
+ * `/api/honeypot/*` requests through the honeypot management
19222
+ * router when a registry has been bound. Returns true when served.
19223
+ */
19224
+ async dispatchHoneypot(req, res) {
19225
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
19226
+ return false;
19227
+ }
19228
+ return handleHoneypotRoute(
19229
+ {
19230
+ authConfig: {
19231
+ loopbackAutoAuth: this._autoAuthLocalhost,
19232
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
19233
+ },
19234
+ registry: this.honeypotRegistry,
19235
+ findingStore: this.honeypotFindingStore,
19236
+ auditLog: this.honeypotAuditLog,
19237
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
19238
+ fortressId: this.honeypotFortressId ?? "fortress_default",
19239
+ ...this.honeypotSelector !== null ? { selector: this.honeypotSelector } : {},
19240
+ ...this.honeypotStore !== null ? { store: this.honeypotStore } : {}
19241
+ },
19242
+ req,
19243
+ res
19244
+ );
19245
+ }
19246
+ /**
19247
+ * v1.3 WP-V1.3-5 Pi-1 front-of-dispatch trap-trigger hook. Examines
19248
+ * every request BEFORE legacy/v1.1/sentinel/coordination routing.
19249
+ * Returns true when a deployed trap matched the request and the
19250
+ * handler emitted the audit event + sentinel finding + plausible
19251
+ * 404 response. Returns false when no trap matched; caller
19252
+ * continues with normal routing.
19253
+ */
19254
+ async dispatchHoneypotTrap(req, res) {
19255
+ if (!this.honeypotRegistry || !this.honeypotFindingStore || !this.honeypotAuditLog) {
19256
+ return false;
19257
+ }
19258
+ return handleHoneypotTriggerIfMatch(
19259
+ {
19260
+ registry: this.honeypotRegistry,
19261
+ findingStore: this.honeypotFindingStore,
19262
+ auditLog: this.honeypotAuditLog,
19263
+ operatorId: this.honeypotOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
19264
+ fortressId: this.honeypotFortressId ?? "fortress_default"
19265
+ },
19266
+ req,
19267
+ res
19268
+ );
19269
+ }
18632
19270
  /**
18633
19271
  * v1.1 dispatch entry point. Called from `handleRequest` before the
18634
19272
  * legacy route table. Returns true when the request was served by v1.1
@@ -19004,6 +19642,40 @@ var init_dashboard = __esm({
19004
19642
  res.end();
19005
19643
  return;
19006
19644
  }
19645
+ if (this.honeypotRegistry) {
19646
+ this.dispatchHoneypotTrap(req, res).then((handled) => {
19647
+ if (handled) return;
19648
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
19649
+ }).catch(() => {
19650
+ if (!res.headersSent) {
19651
+ res.writeHead(500, { "Content-Type": "application/json" });
19652
+ res.end(JSON.stringify({ error: "Internal server error" }));
19653
+ }
19654
+ });
19655
+ return;
19656
+ }
19657
+ this.continueHandleRequest(req, res, url, method, origin, selfOrigin);
19658
+ }
19659
+ /**
19660
+ * v1.3 WP-V1.3-5 Pi-1: post-honeypot-trap request continuation. The
19661
+ * front-of-dispatch trap-trigger hook may short-circuit a request;
19662
+ * when it does not, this method runs the original dispatch ladder.
19663
+ * Pulled out as a helper so the trap-hook + non-trap paths share
19664
+ * one code path through every downstream dispatcher.
19665
+ */
19666
+ continueHandleRequest(req, res, url, method, _origin, _selfOrigin) {
19667
+ if (this.honeypotRegistry && url.pathname.startsWith(HONEYPOT_API_PREFIX)) {
19668
+ this.dispatchHoneypot(req, res).then((handled) => {
19669
+ if (handled) return;
19670
+ this.handleLegacyRequest(req, res, url, method);
19671
+ }).catch(() => {
19672
+ if (!res.headersSent) {
19673
+ res.writeHead(500, { "Content-Type": "application/json" });
19674
+ res.end(JSON.stringify({ error: "Internal server error" }));
19675
+ }
19676
+ });
19677
+ return;
19678
+ }
19007
19679
  if (this.approvalAggregator && url.pathname.startsWith(APPROVAL_INBOX_API_PREFIX)) {
19008
19680
  this.dispatchApprovalInbox(req, res).then((handled) => {
19009
19681
  if (handled) return;
@@ -22436,7 +23108,7 @@ function proxyServerFromAuditEntry(entry) {
22436
23108
  return server;
22437
23109
  }
22438
23110
  var SENTINEL_SUMMARY_MAX_CHARS, SENTINEL_AUDIT_OPS, SENTINEL_OBSERVED_AUDIT_OPS;
22439
- var init_types3 = __esm({
23111
+ var init_types4 = __esm({
22440
23112
  "src/sentinel/types.ts"() {
22441
23113
  SENTINEL_SUMMARY_MAX_CHARS = 240;
22442
23114
  SENTINEL_AUDIT_OPS = {
@@ -22470,7 +23142,7 @@ var init_sentinel_finding_store = __esm({
22470
23142
  init_encryption();
22471
23143
  init_key_derivation();
22472
23144
  init_encoding();
22473
- init_types3();
23145
+ init_types4();
22474
23146
  SENTINEL_FINDING_NAMESPACE = "_sentinel_findings";
22475
23147
  SENTINEL_FINDING_KEY_PREFIX = "finding.";
22476
23148
  HKDF_INFO2 = "l2-sentinel-finding-v1";
@@ -22713,7 +23385,7 @@ var init_sentinel_registry = __esm({
22713
23385
  var DEFAULT_TICK_INTERVAL_MS, SentinelDispatcher;
22714
23386
  var init_sentinel_dispatcher = __esm({
22715
23387
  "src/sentinel/sentinel-dispatcher.ts"() {
22716
- init_types3();
23388
+ init_types4();
22717
23389
  DEFAULT_TICK_INTERVAL_MS = 6e4;
22718
23390
  SentinelDispatcher = class {
22719
23391
  registry;
@@ -23079,7 +23751,7 @@ function formatAnomalySummary(detector, classifier, vector, prediction, severity
23079
23751
  return `${detector.detectorId}/${classifier.classifierId} ${severity}: agent ${vector.agent_id} drifted ${prediction.anomaly_score.toFixed(2)} sigma from baseline. Top contributors: ${top || "(none)"}.`;
23080
23752
  }
23081
23753
  var AnomalyDetector, ANOMALY_SENTINEL_ID_PREFIX;
23082
- var init_types4 = __esm({
23754
+ var init_types5 = __esm({
23083
23755
  "src/anomaly-detection/types.ts"() {
23084
23756
  AnomalyDetector = class {
23085
23757
  /**
@@ -23207,7 +23879,7 @@ var init_anomaly_pipeline = __esm({
23207
23879
  "src/anomaly-detection/anomaly-pipeline.ts"() {
23208
23880
  init_cusum();
23209
23881
  init_psi();
23210
- init_types4();
23882
+ init_types5();
23211
23883
  ANOMALY_AUDIT_OPS = {
23212
23884
  DETECTOR_REGISTERED: "anomaly_detector_registered",
23213
23885
  DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
@@ -23570,6 +24242,198 @@ var init_workflow_state_tracker = __esm({
23570
24242
  }
23571
24243
  });
23572
24244
 
24245
+ // src/honeypot/trap-registry.ts
24246
+ function matchesTrap(spec, input) {
24247
+ if (spec.trigger.kind !== "http_endpoint") return false;
24248
+ const trigger = spec.trigger;
24249
+ if (trigger.method && trigger.method.toUpperCase() !== input.method.toUpperCase()) {
24250
+ return false;
24251
+ }
24252
+ const re = compileGlob(trigger.path_pattern);
24253
+ return re.test(input.path);
24254
+ }
24255
+ function compileGlob(pattern) {
24256
+ let out = "";
24257
+ let i = 0;
24258
+ while (i < pattern.length) {
24259
+ const ch = pattern[i];
24260
+ if (ch === "*" && pattern[i + 1] === "*") {
24261
+ out += ".*";
24262
+ i += 2;
24263
+ continue;
24264
+ }
24265
+ if (ch === "*") {
24266
+ out += "[^/]*";
24267
+ i += 1;
24268
+ continue;
24269
+ }
24270
+ if ("\\^$.|?+()[]{}".includes(ch)) {
24271
+ out += `\\${ch}`;
24272
+ } else {
24273
+ out += ch;
24274
+ }
24275
+ i += 1;
24276
+ }
24277
+ return new RegExp(`^${out}$`);
24278
+ }
24279
+ var TrapRegistry;
24280
+ var init_trap_registry = __esm({
24281
+ "src/honeypot/trap-registry.ts"() {
24282
+ TrapRegistry = class {
24283
+ traps = /* @__PURE__ */ new Map();
24284
+ /**
24285
+ * Deploy a trap. Idempotent on `trap_id`: re-deploying replaces the
24286
+ * previous spec for that id. Returns true on first deploy, false on
24287
+ * re-deploy (so callers can branch audit emission).
24288
+ */
24289
+ deploy(spec) {
24290
+ const isNew = !this.traps.has(spec.trap_id);
24291
+ this.traps.set(spec.trap_id, spec);
24292
+ return isNew;
24293
+ }
24294
+ /**
24295
+ * Undeploy by trap_id. Returns true when a trap was removed, false
24296
+ * when no trap had that id (idempotent).
24297
+ */
24298
+ undeploy(trapId) {
24299
+ return this.traps.delete(trapId);
24300
+ }
24301
+ /** List deployed traps. Returns a fresh array; mutation is safe. */
24302
+ list() {
24303
+ return [...this.traps.values()];
24304
+ }
24305
+ /** Look up a single trap by id. */
24306
+ get(trapId) {
24307
+ return this.traps.get(trapId);
24308
+ }
24309
+ /**
24310
+ * Find the first trap matching the request. Iteration order is
24311
+ * insertion order; operators who deploy multiple overlapping traps
24312
+ * see the earliest-deployed one fire. Tests cover this contract.
24313
+ */
24314
+ findMatching(input) {
24315
+ for (const spec of this.traps.values()) {
24316
+ if (matchesTrap(spec, input)) return spec;
24317
+ }
24318
+ return void 0;
24319
+ }
24320
+ /** Drop every trap. Tests use this between runs; not surfaced via API. */
24321
+ clear() {
24322
+ this.traps.clear();
24323
+ }
24324
+ };
24325
+ }
24326
+ });
24327
+
24328
+ // src/honeypot/trap-store.ts
24329
+ function trapKey(trapId) {
24330
+ return `${TRAP_STORE_KEY_PREFIX}${trapId}`;
24331
+ }
24332
+ function stripKeyPrefix3(key) {
24333
+ if (!key.startsWith(TRAP_STORE_KEY_PREFIX)) return null;
24334
+ return key.slice(TRAP_STORE_KEY_PREFIX.length);
24335
+ }
24336
+ var TRAP_STORE_NAMESPACE, TRAP_STORE_KEY_PREFIX, HKDF_INFO4, MAX_TRAP_BYTES, TrapStore;
24337
+ var init_trap_store = __esm({
24338
+ "src/honeypot/trap-store.ts"() {
24339
+ init_encryption();
24340
+ init_key_derivation();
24341
+ init_encoding();
24342
+ TRAP_STORE_NAMESPACE = "_honeypot_traps";
24343
+ TRAP_STORE_KEY_PREFIX = "trap.";
24344
+ HKDF_INFO4 = "l2-honeypot-trap-v1";
24345
+ MAX_TRAP_BYTES = 64 * 1024;
24346
+ TrapStore = class {
24347
+ storage;
24348
+ encryptionKey;
24349
+ fortressId;
24350
+ constructor(opts) {
24351
+ this.storage = opts.storage;
24352
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
24353
+ this.fortressId = opts.fortressId;
24354
+ }
24355
+ /** Persist (or overwrite) one trap. Returns the trap_id on success. */
24356
+ async save(spec) {
24357
+ const persisted = { version: 1, spec };
24358
+ const aad = stringToBytes(spec.trap_id);
24359
+ const plaintext = stringToBytes(JSON.stringify(persisted));
24360
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
24361
+ await this.storage.write(
24362
+ TRAP_STORE_NAMESPACE,
24363
+ trapKey(spec.trap_id),
24364
+ stringToBytes(JSON.stringify(envelope))
24365
+ );
24366
+ return spec.trap_id;
24367
+ }
24368
+ /**
24369
+ * Remove one trap by id. Returns true when a record was removed,
24370
+ * false when no record existed (idempotent).
24371
+ */
24372
+ async delete(trapId) {
24373
+ try {
24374
+ const raw = await this.storage.read(
24375
+ TRAP_STORE_NAMESPACE,
24376
+ trapKey(trapId)
24377
+ );
24378
+ if (!raw) return false;
24379
+ await this.storage.delete(TRAP_STORE_NAMESPACE, trapKey(trapId));
24380
+ return true;
24381
+ } catch {
24382
+ return false;
24383
+ }
24384
+ }
24385
+ /**
24386
+ * Load every persisted trap. Used at boot to repopulate the
24387
+ * in-memory TrapRegistry. Corrupted records are silently skipped
24388
+ * so one malformed entry never blocks the rest of the fortress's
24389
+ * traps from rehydrating.
24390
+ *
24391
+ * Returns the specs sorted by `compiled_at` ascending so the
24392
+ * in-memory registry's insertion order matches the original
24393
+ * deploy order (relevant for Pi-1's "first-deployed wins on
24394
+ * overlapping match" contract).
24395
+ */
24396
+ async loadAll() {
24397
+ const metas = await this.storage.list(
24398
+ TRAP_STORE_NAMESPACE,
24399
+ TRAP_STORE_KEY_PREFIX
24400
+ );
24401
+ const out = [];
24402
+ for (const meta of metas) {
24403
+ const trapId = stripKeyPrefix3(meta.key);
24404
+ if (trapId === null) continue;
24405
+ const raw = await this.storage.read(TRAP_STORE_NAMESPACE, meta.key);
24406
+ if (!raw) continue;
24407
+ if (raw.length > MAX_TRAP_BYTES) continue;
24408
+ const spec = this.decode(trapId, raw);
24409
+ if (spec !== null) out.push(spec);
24410
+ }
24411
+ out.sort(
24412
+ (a, b) => a.compiled_at < b.compiled_at ? -1 : a.compiled_at > b.compiled_at ? 1 : 0
24413
+ );
24414
+ return out;
24415
+ }
24416
+ /** Read-only fortress-id getter. */
24417
+ getFortressId() {
24418
+ return this.fortressId;
24419
+ }
24420
+ decode(trapId, raw) {
24421
+ try {
24422
+ const aad = stringToBytes(trapId);
24423
+ const envelope = JSON.parse(bytesToString(raw));
24424
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
24425
+ const persisted = JSON.parse(bytesToString(plaintext));
24426
+ if (persisted.version !== 1) return null;
24427
+ if (persisted.spec.trap_id !== trapId) return null;
24428
+ return persisted.spec;
24429
+ } catch {
24430
+ return null;
24431
+ }
24432
+ }
24433
+ };
24434
+ }
24435
+ });
24436
+
23573
24437
  // src/sentinel/sentinel.ts
23574
24438
  var Sentinel;
23575
24439
  var init_sentinel = __esm({
@@ -23610,7 +24474,7 @@ var EGRESS_VOLUME_SENTINEL_ID, WARN_SIGMA, ALERT_SIGMA, BASELINE_WINDOWS, QUERY_
23610
24474
  var init_egress_volume_watcher = __esm({
23611
24475
  "src/sentinel/sentinels/egress-volume-watcher.ts"() {
23612
24476
  init_sentinel();
23613
- init_types3();
24477
+ init_types4();
23614
24478
  EGRESS_VOLUME_SENTINEL_ID = "egress-volume";
23615
24479
  WARN_SIGMA = 3;
23616
24480
  ALERT_SIGMA = 6;
@@ -36514,7 +37378,7 @@ var init_recovery_key_disclosure = __esm({
36514
37378
  });
36515
37379
 
36516
37380
  // src/hub/types.ts
36517
- var init_types5 = __esm({
37381
+ var init_types6 = __esm({
36518
37382
  "src/hub/types.ts"() {
36519
37383
  }
36520
37384
  });
@@ -37553,7 +38417,7 @@ var init_hub = __esm({
37553
38417
  "src/hub/index.ts"() {
37554
38418
  init_constants3();
37555
38419
  init_errors4();
37556
- init_types5();
38420
+ init_types6();
37557
38421
  init_agent_registry();
37558
38422
  init_inbox_store();
37559
38423
  init_inbox_aggregator();
@@ -39281,7 +40145,7 @@ ${runningLines.join("\n")}`;
39281
40145
  function chatStorageKey(surface, threadKey) {
39282
40146
  return `${surface}.${threadKey}`;
39283
40147
  }
39284
- var OPERATOR_CHAT_NAMESPACE, HKDF_INFO4, OperatorChatStore;
40148
+ var OPERATOR_CHAT_NAMESPACE, HKDF_INFO5, OperatorChatStore;
39285
40149
  var init_operator_chat_store = __esm({
39286
40150
  "src/chat/operator-chat-store.ts"() {
39287
40151
  init_encryption();
@@ -39289,13 +40153,13 @@ var init_operator_chat_store = __esm({
39289
40153
  init_encoding();
39290
40154
  init_operator_chat_types();
39291
40155
  OPERATOR_CHAT_NAMESPACE = "_chat";
39292
- HKDF_INFO4 = "operator-chat-store-v1";
40156
+ HKDF_INFO5 = "operator-chat-store-v1";
39293
40157
  OperatorChatStore = class {
39294
40158
  storage;
39295
40159
  encryptionKey;
39296
40160
  constructor(storage, masterKey) {
39297
40161
  this.storage = storage;
39298
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO4);
40162
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
39299
40163
  }
39300
40164
  /**
39301
40165
  * Load a thread. Returns null if no record exists or if the on-disk
@@ -39380,7 +40244,7 @@ var init_operator_chat_store = __esm({
39380
40244
  function bundleKey(threadId) {
39381
40245
  return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
39382
40246
  }
39383
- function stripKeyPrefix3(key) {
40247
+ function stripKeyPrefix4(key) {
39384
40248
  if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
39385
40249
  return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
39386
40250
  }
@@ -39391,7 +40255,7 @@ function lastTurnId(bundle) {
39391
40255
  }
39392
40256
  return max;
39393
40257
  }
39394
- var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO5, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
40258
+ var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO6, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
39395
40259
  var init_concierge_memory_store = __esm({
39396
40260
  "src/chat/concierge-memory-store.ts"() {
39397
40261
  init_encryption();
@@ -39399,7 +40263,7 @@ var init_concierge_memory_store = __esm({
39399
40263
  init_encoding();
39400
40264
  CONCIERGE_MEMORY_NAMESPACE = "_chat";
39401
40265
  CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
39402
- HKDF_INFO5 = "concierge-memory-store-v1";
40266
+ HKDF_INFO6 = "concierge-memory-store-v1";
39403
40267
  DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
39404
40268
  MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
39405
40269
  ConciergeMemoryStore = class {
@@ -39410,7 +40274,7 @@ var init_concierge_memory_store = __esm({
39410
40274
  locks;
39411
40275
  constructor(opts) {
39412
40276
  this.storage = opts.storage;
39413
- this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO5);
40277
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO6);
39414
40278
  this.fortressId = opts.fortressId;
39415
40279
  this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
39416
40280
  this.locks = /* @__PURE__ */ new Map();
@@ -39536,7 +40400,7 @@ var init_concierge_memory_store = __esm({
39536
40400
  );
39537
40401
  const summaries = [];
39538
40402
  for (const meta of entries) {
39539
- const threadId = stripKeyPrefix3(meta.key);
40403
+ const threadId = stripKeyPrefix4(meta.key);
39540
40404
  if (threadId === null) continue;
39541
40405
  const bundle = await this.loadBundle(threadId);
39542
40406
  if (!bundle || bundle.turns.length === 0) continue;
@@ -39589,7 +40453,7 @@ var init_concierge_memory_store = __esm({
39589
40453
  );
39590
40454
  let pruned = 0;
39591
40455
  for (const meta of entries) {
39592
- const threadId = stripKeyPrefix3(meta.key);
40456
+ const threadId = stripKeyPrefix4(meta.key);
39593
40457
  if (threadId === null) continue;
39594
40458
  pruned += await this.withLock(threadId, async () => {
39595
40459
  const bundle = await this.loadBundle(threadId);
@@ -40059,7 +40923,7 @@ var init_defaults = __esm({
40059
40923
  });
40060
40924
 
40061
40925
  // src/intelligence/policy-store.ts
40062
- var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO6, IntelligenceConfigStore;
40926
+ var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO7, IntelligenceConfigStore;
40063
40927
  var init_policy_store = __esm({
40064
40928
  "src/intelligence/policy-store.ts"() {
40065
40929
  init_encryption();
@@ -40068,13 +40932,13 @@ var init_policy_store = __esm({
40068
40932
  init_defaults();
40069
40933
  INTELLIGENCE_NAMESPACE = "_intelligence";
40070
40934
  SUBSTRATE_CONFIG_KEY = "substrate-config";
40071
- HKDF_INFO6 = "intelligence-substrate-config";
40935
+ HKDF_INFO7 = "intelligence-substrate-config";
40072
40936
  IntelligenceConfigStore = class {
40073
40937
  storage;
40074
40938
  encryptionKey;
40075
40939
  constructor(storage, masterKey) {
40076
40940
  this.storage = storage;
40077
- this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO6);
40941
+ this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO7);
40078
40942
  }
40079
40943
  /**
40080
40944
  * Load the operator's substrate config from disk. Returns the config
@@ -41867,8 +42731,60 @@ var init_model_provenance = __esm({
41867
42731
  });
41868
42732
 
41869
42733
  // src/storage/memory.ts
42734
+ var MemoryStorage;
41870
42735
  var init_memory = __esm({
41871
42736
  "src/storage/memory.ts"() {
42737
+ MemoryStorage = class {
42738
+ store = /* @__PURE__ */ new Map();
42739
+ storageKey(namespace, key) {
42740
+ return `${namespace}/${key}`;
42741
+ }
42742
+ async write(namespace, key, data) {
42743
+ this.store.set(this.storageKey(namespace, key), {
42744
+ data: new Uint8Array(data),
42745
+ // Copy to prevent external mutation
42746
+ modified_at: (/* @__PURE__ */ new Date()).toISOString()
42747
+ });
42748
+ }
42749
+ async read(namespace, key) {
42750
+ const entry = this.store.get(this.storageKey(namespace, key));
42751
+ if (!entry) return null;
42752
+ return new Uint8Array(entry.data);
42753
+ }
42754
+ async delete(namespace, key, _secureOverwrite) {
42755
+ return this.store.delete(this.storageKey(namespace, key));
42756
+ }
42757
+ async list(namespace, prefix) {
42758
+ const entries = [];
42759
+ const nsPrefix = `${namespace}/`;
42760
+ for (const [storeKey, entry] of this.store) {
42761
+ if (!storeKey.startsWith(nsPrefix)) continue;
42762
+ const key = storeKey.slice(nsPrefix.length);
42763
+ if (prefix && !key.startsWith(prefix)) continue;
42764
+ entries.push({
42765
+ key,
42766
+ namespace,
42767
+ size_bytes: entry.data.length,
42768
+ modified_at: entry.modified_at
42769
+ });
42770
+ }
42771
+ return entries.sort((a, b) => a.key.localeCompare(b.key));
42772
+ }
42773
+ async exists(namespace, key) {
42774
+ return this.store.has(this.storageKey(namespace, key));
42775
+ }
42776
+ async totalSize() {
42777
+ let total = 0;
42778
+ for (const entry of this.store.values()) {
42779
+ total += entry.data.length;
42780
+ }
42781
+ return total;
42782
+ }
42783
+ /** Clear all stored data (useful in tests) */
42784
+ clear() {
42785
+ this.store.clear();
42786
+ }
42787
+ };
41872
42788
  }
41873
42789
  });
41874
42790
 
@@ -42115,7 +43031,55 @@ async function defaultFetcher(url, init) {
42115
43031
  json: () => response.json()
42116
43032
  };
42117
43033
  }
42118
- var DID_CONTEXT, DEFAULT_TIMEOUT_MS4, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE;
43034
+ async function loadFortressDidWebRecord(storagePath) {
43035
+ const persistPath = join(storagePath, FORTRESS_DID_WEB_REGISTRY_PATH);
43036
+ let raw;
43037
+ try {
43038
+ raw = await readFile(persistPath, "utf-8");
43039
+ } catch (err) {
43040
+ const code = err.code;
43041
+ if (code === "ENOENT") return null;
43042
+ throw err;
43043
+ }
43044
+ let parsed;
43045
+ try {
43046
+ parsed = JSON.parse(raw);
43047
+ } catch (e) {
43048
+ const message = e instanceof Error ? e.message : String(e);
43049
+ throw new Error(
43050
+ `did-web: fortress-config record at ${persistPath} is not valid JSON: ${message}`
43051
+ );
43052
+ }
43053
+ if (!isFortressDidWebRecord(parsed)) {
43054
+ throw new Error(
43055
+ `did-web: fortress-config record at ${persistPath} is malformed (expected version: 1 with identifier.did + identifier.authority_host)`
43056
+ );
43057
+ }
43058
+ return parsed;
43059
+ }
43060
+ function isFortressDidWebRecord(value) {
43061
+ if (!value || typeof value !== "object") return false;
43062
+ const v = value;
43063
+ if (v["version"] !== 1) return false;
43064
+ const id = v["identifier"];
43065
+ if (!id || typeof id !== "object") return false;
43066
+ if (typeof id["did"] !== "string" || !id["did"].startsWith("did:web:")) {
43067
+ return false;
43068
+ }
43069
+ if (typeof id["authority_host"] !== "string") return false;
43070
+ if (typeof id["fortress_id"] !== "string") return false;
43071
+ if (typeof id["created_at"] !== "string") return false;
43072
+ if (!id["did_document"] || typeof id["did_document"] !== "object") {
43073
+ return false;
43074
+ }
43075
+ const artifact = v["artifact"];
43076
+ if (!artifact || typeof artifact !== "object") return false;
43077
+ if (typeof artifact["url"] !== "string") return false;
43078
+ if (typeof artifact["publish_path"] !== "string") return false;
43079
+ if (typeof artifact["sha256"] !== "string") return false;
43080
+ return true;
43081
+ }
43082
+ var DID_CONTEXT, DEFAULT_TIMEOUT_MS4, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE, FORTRESS_DID_WEB_REGISTRY_PATH;
42119
43083
  var init_did_web = __esm({
42120
43084
  "src/recognition/did-web.ts"() {
42121
43085
  init_encoding();
@@ -42128,6 +43092,7 @@ var init_did_web = __esm({
42128
43092
  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;
42129
43093
  FORTRESS_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
42130
43094
  AGENT_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
43095
+ FORTRESS_DID_WEB_REGISTRY_PATH = "recognition/did-web.json";
42131
43096
  }
42132
43097
  });
42133
43098
 
@@ -43598,9 +44563,31 @@ Options:
43598
44563
  --accept-unverifiable-attestations
43599
44564
  On import: accept reputation attestations whose
43600
44565
  signer DID is not in the bundle (Tier 1 confirmation)
44566
+ --did-web <identifier> Embed a specific did:web identifier in the export
44567
+ manifest. Requires --did-web-authority-host.
44568
+ Overrides fortress-config auto-inclusion.
44569
+ --did-web-authority-host <host> Authority host for --did-web (required with it).
44570
+ --did-web-published-at <iso8601> Operator's claimed publication time for the DID
44571
+ Document (optional; ISO 8601).
44572
+ --no-did-web Explicit opt-out: skip did:web inclusion even if
44573
+ a fortress-config record exists. (Alias for
44574
+ --include-did-web=false.)
44575
+ --did-web-allowed-host <host> On import: host allowed for outbound did:web
44576
+ resolution; repeatable. Empty means refuse to
44577
+ resolve (no-outbound-by-default).
44578
+ --skip-did-web-verify On import: skip did:web resolution entirely.
43601
44579
  --json
43602
44580
  --yes, -y Explicit non-interactive Tier 1 approval
43603
44581
  --help, -h
44582
+
44583
+ did:web auto-inclusion (build 3):
44584
+ Running "sanctuary did-web issue --authority-host <host>" registers the
44585
+ operator's did:web identifier at <storage>/recognition/did-web.json.
44586
+ Subsequent "sanctuary exit export" runs auto-include this identifier in
44587
+ the manifest's identity_binding without requiring any --did-web flag.
44588
+ Per-fortress isolation is structural: the record lives under the
44589
+ fortress's storage_path, so different fortresses carry different
44590
+ registered identifiers.
43604
44591
  `);
43605
44592
  }
43606
44593
  async function runExitCommand(args) {
@@ -43699,12 +44686,15 @@ ${policyErr.message}
43699
44686
  throw policyErr;
43700
44687
  }
43701
44688
  const includeDidWebFlag = flagValue(argv, "--include-did-web");
43702
- const includeDidWebDisabled = includeDidWebFlag === "false";
44689
+ const explicitOptOut = hasFlag(argv, "--no-did-web") || includeDidWebFlag === "false";
43703
44690
  const didWebIdentifier = flagValue(argv, "--did-web");
43704
44691
  const didWebAuthorityHost = flagValue(argv, "--did-web-authority-host");
43705
44692
  const didWebPublishedAt = flagValue(argv, "--did-web-published-at");
43706
44693
  let exportDidWeb;
43707
- if (!includeDidWebDisabled && didWebIdentifier !== void 0) {
44694
+ let didWebSource;
44695
+ if (explicitOptOut) {
44696
+ didWebSource = "opted-out";
44697
+ } else if (didWebIdentifier !== void 0) {
43708
44698
  if (didWebAuthorityHost === void 0) {
43709
44699
  write(
43710
44700
  err,
@@ -43717,6 +44707,19 @@ ${policyErr.message}
43717
44707
  authority_host: didWebAuthorityHost,
43718
44708
  ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
43719
44709
  };
44710
+ didWebSource = "cli-override";
44711
+ } else {
44712
+ const record = await loadFortressDidWebRecord(ctx.storagePath);
44713
+ if (record !== null) {
44714
+ exportDidWeb = {
44715
+ identifier: record.identifier.did,
44716
+ authority_host: record.identifier.authority_host,
44717
+ ...didWebPublishedAt !== void 0 ? { published_at: didWebPublishedAt } : {}
44718
+ };
44719
+ didWebSource = "fortress-config";
44720
+ } else {
44721
+ didWebSource = "no-record";
44722
+ }
43720
44723
  }
43721
44724
  const result = await exportExitBundle({
43722
44725
  bundleDir: outDir,
@@ -43732,12 +44735,42 @@ ${policyErr.message}
43732
44735
  keySource: ctx.keySource,
43733
44736
  ...exportDidWeb !== void 0 ? { didWeb: exportDidWeb } : {}
43734
44737
  });
43735
- if (json) write(out, JSON.stringify(result, null, 2) + "\n");
43736
- else {
44738
+ if (json) {
44739
+ write(
44740
+ out,
44741
+ JSON.stringify(
44742
+ { ...result, did_web_source: didWebSource },
44743
+ null,
44744
+ 2
44745
+ ) + "\n"
44746
+ );
44747
+ } else {
43737
44748
  write(out, `exported: ${result.bundle_dir}
43738
44749
  `);
43739
44750
  write(out, `manifest_hash: ${result.manifest_hash}
43740
44751
  `);
44752
+ if (didWebSource === "fortress-config" && exportDidWeb) {
44753
+ write(
44754
+ out,
44755
+ `did:web: auto-included from fortress config (${exportDidWeb.identifier})
44756
+ `
44757
+ );
44758
+ } else if (didWebSource === "cli-override" && exportDidWeb) {
44759
+ write(
44760
+ out,
44761
+ `did:web: included via CLI override (${exportDidWeb.identifier})
44762
+ `
44763
+ );
44764
+ } else if (didWebSource === "opted-out") {
44765
+ write(out, `did:web: skipped (operator opt-out via --no-did-web)
44766
+ `);
44767
+ } else if (didWebSource === "no-record") {
44768
+ write(
44769
+ out,
44770
+ `did:web: not included (no fortress config; run "sanctuary did-web issue" to register)
44771
+ `
44772
+ );
44773
+ }
43741
44774
  for (const item of result.unsupported_artifacts) {
43742
44775
  write(out, `unsupported: ${item}
43743
44776
  `);
@@ -43888,6 +44921,7 @@ var init_cli = __esm({
43888
44921
  init_encoding();
43889
44922
  init_bundle();
43890
44923
  init_verifier2();
44924
+ init_did_web();
43891
44925
  }
43892
44926
  });
43893
44927
 
@@ -44442,6 +45476,7 @@ ${err.message}
44442
45476
  await baseline.load();
44443
45477
  let approvalChannel;
44444
45478
  let dashboard;
45479
+ let intelligenceSelector;
44445
45480
  if (config.dashboard.enabled) {
44446
45481
  let authToken = config.dashboard.auth_token;
44447
45482
  if (authToken === "auto") {
@@ -44468,7 +45503,6 @@ ${err.message}
44468
45503
  profileStore
44469
45504
  });
44470
45505
  const embeddedHubIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
44471
- let intelligenceSelector;
44472
45506
  try {
44473
45507
  intelligenceSelector = new SubstrateSelector({
44474
45508
  storage,
@@ -44625,6 +45659,44 @@ ${err.message}
44625
45659
  workflowStateTracker
44626
45660
  });
44627
45661
  }
45662
+ const honeypotRegistry = new TrapRegistry();
45663
+ const honeypotStore = new TrapStore({
45664
+ storage,
45665
+ masterKey,
45666
+ fortressId: fortressIdForAggregator
45667
+ });
45668
+ try {
45669
+ const persistedSpecs = await honeypotStore.loadAll();
45670
+ for (const spec of persistedSpecs) {
45671
+ honeypotRegistry.deploy(spec);
45672
+ }
45673
+ if (persistedSpecs.length > 0) {
45674
+ auditLog.append(
45675
+ "l2",
45676
+ HONEYPOT_AUDIT_OPS.LOADED,
45677
+ aggregatorIdentityId,
45678
+ {
45679
+ fortress_id: fortressIdForAggregator,
45680
+ trap_count: persistedSpecs.length
45681
+ }
45682
+ );
45683
+ }
45684
+ } catch (err) {
45685
+ console.error(
45686
+ ` Note: honeypot trap store unavailable (${err.message}). Deployed traps from prior runs will not be restored; re-deploy via the management API.`
45687
+ );
45688
+ }
45689
+ if (dashboard) {
45690
+ dashboard.setHoneypotRegistry({
45691
+ registry: honeypotRegistry,
45692
+ findingStore: sentinelFindingStore,
45693
+ auditLog,
45694
+ operatorId: aggregatorIdentityId,
45695
+ fortressId: fortressIdForAggregator,
45696
+ ...intelligenceSelector ? { selector: intelligenceSelector } : {},
45697
+ store: honeypotStore
45698
+ });
45699
+ }
44628
45700
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
44629
45701
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
44630
45702
  config,
@@ -44825,6 +45897,9 @@ var init_src = __esm({
44825
45897
  init_handoff_log();
44826
45898
  init_handoff_routes();
44827
45899
  init_workflow_state_tracker();
45900
+ init_trap_registry();
45901
+ init_trap_store();
45902
+ init_types3();
44828
45903
  init_sentinels();
44829
45904
  init_subscription_store();
44830
45905
  init_tools4();
@@ -50264,6 +51339,14 @@ Options:
50264
51339
  --json Output as JSON.
50265
51340
  --help, -h Show this help.
50266
51341
 
51342
+ Fortress-config auto-inclusion (build 3): "issue" persists the record
51343
+ at <storage>/recognition/did-web.json. This file IS the fortress's
51344
+ registered did:web identifier. Subsequent "sanctuary exit export"
51345
+ runs auto-include it in the manifest without any --did-web flag.
51346
+ Per-fortress isolation is structural (different storage paths carry
51347
+ different records). Use "sanctuary exit export --no-did-web" to
51348
+ opt out for a specific export without removing the registration.
51349
+
50267
51350
  Castle-walking note: did:web resolution is outbound HTTPS by design.
50268
51351
  This CLI never opens an outbound socket. The opt-in surface is your
50269
51352
  choice to run "did-web issue" with --authority-host; the resulting
@@ -50398,7 +51481,7 @@ async function cmdIssue(argv, out, err, env) {
50398
51481
  write3(out, JSON.stringify(record, null, 2) + "\n");
50399
51482
  return 0;
50400
51483
  }
50401
- write3(out, `did:web identifier issued.
51484
+ write3(out, `did:web identifier issued and registered on this fortress.
50402
51485
  `);
50403
51486
  write3(out, ` DID: ${identifier.did}
50404
51487
  `);
@@ -50420,6 +51503,13 @@ Next step: publish the DID Document to your HTTPS host.
50420
51503
  const artifactPath = join(persistDir, "did.json");
50421
51504
  await writeFile(artifactPath, artifact.artifact, { mode: 420 });
50422
51505
  write3(out, `
51506
+ Auto-inclusion: subsequent "sanctuary exit export" runs will
51507
+ `);
51508
+ write3(out, `auto-include this identifier in the bundle manifest. Pass
51509
+ `);
51510
+ write3(out, `"--no-did-web" to opt out for a specific export.
51511
+ `);
51512
+ write3(out, `
50423
51513
  Castle-walking note: this CLI never opens an outbound socket.
50424
51514
  `);
50425
51515
  write3(out, `Publishing the DID Document is your operation; serve the artifact
@@ -50454,7 +51544,7 @@ Run "sanctuary did-web issue --authority-host <host>" to issue one.
50454
51544
  return 0;
50455
51545
  }
50456
51546
  const parsed = JSON.parse(bytes.toString("utf-8"));
50457
- write3(out, `did:web identifier on this fortress:
51547
+ write3(out, `did:web identifier registered on this fortress:
50458
51548
  `);
50459
51549
  write3(out, ` DID: ${parsed.identifier.did}
50460
51550
  `);
@@ -50465,6 +51555,13 @@ Run "sanctuary did-web issue --authority-host <host>" to issue one.
50465
51555
  write3(out, ` Publish URL: ${parsed.artifact.url}
50466
51556
  `);
50467
51557
  write3(out, ` SHA-256: ${parsed.artifact.sha256}
51558
+ `);
51559
+ write3(out, `
51560
+ Auto-inclusion: subsequent "sanctuary exit export" runs auto-include
51561
+ `);
51562
+ write3(out, `this identifier in the bundle manifest without --did-web. Pass
51563
+ `);
51564
+ write3(out, `"--no-did-web" to opt out for a specific export.
50468
51565
  `);
50469
51566
  return 0;
50470
51567
  }
@@ -50678,7 +51775,7 @@ var init_per_agent_activity = __esm({
50678
51775
  var PER_AGENT_ACTIVITY_DETECTOR_ID, PerAgentActivityDetector, PendingClassifier;
50679
51776
  var init_per_agent_activity_detector = __esm({
50680
51777
  "src/anomaly-detection/detectors/per-agent-activity-detector.ts"() {
50681
- init_types4();
51778
+ init_types5();
50682
51779
  init_rolling_baseline();
50683
51780
  init_classifier_state_store();
50684
51781
  init_per_agent_activity();
@@ -51158,7 +52255,1064 @@ var init_anomaly = __esm({
51158
52255
  init_anomaly_catalog();
51159
52256
  init_anomaly_subscription_store();
51160
52257
  init_classifier_state_store();
51161
- init_types4();
52258
+ init_types5();
52259
+ }
52260
+ });
52261
+ function computeDraftId(draft) {
52262
+ return createHash("sha256").update(`${draft.english_text}|${draft.observed_at}|${draft.operator_id}`).digest("hex");
52263
+ }
52264
+ function compileDeterministic(text) {
52265
+ const normalized = text.trim().toLowerCase();
52266
+ const requireMatch = normalized.match(
52267
+ /^(always\s+)?require approval (?:for|on) ([a-z][a-z0-9_]*)\.?$/
52268
+ );
52269
+ if (requireMatch) {
52270
+ const op = requireMatch[2];
52271
+ return {
52272
+ rule: { kind: "tier1_add_operation", operation: op },
52273
+ explanation: `Adds "${op}" to the Tier 1 always-approve list. Every call to ${op} will require explicit human approval before it executes.`,
52274
+ confidence: "high",
52275
+ warnings: []
52276
+ };
52277
+ }
52278
+ const noAgentMatch = normalized.match(
52279
+ /^no agent should ([a-z][a-z0-9_]*)\.?$/
52280
+ );
52281
+ if (noAgentMatch) {
52282
+ const op = noAgentMatch[1];
52283
+ return {
52284
+ rule: { kind: "tier1_add_operation", operation: op },
52285
+ explanation: `Adds "${op}" to the Tier 1 always-approve list. The operator must approve every ${op} call before it executes.`,
52286
+ confidence: "high",
52287
+ warnings: []
52288
+ };
52289
+ }
52290
+ const allowMatch = normalized.match(
52291
+ /^(?:allow ([a-z][a-z0-9_]*) without approval|auto-allow ([a-z][a-z0-9_]*))\.?$/
52292
+ );
52293
+ if (allowMatch) {
52294
+ const op = allowMatch[1] ?? allowMatch[2];
52295
+ return {
52296
+ rule: { kind: "tier3_add_operation", operation: op },
52297
+ explanation: `Adds "${op}" to the Tier 3 always-allow list. Calls to ${op} will be audit-logged but never block on approval.`,
52298
+ confidence: "high",
52299
+ warnings: []
52300
+ };
52301
+ }
52302
+ const removeMatch = normalized.match(
52303
+ /^(?:remove ([a-z][a-z0-9_]*) from approval list|stop requiring approval for ([a-z][a-z0-9_]*))\.?$/
52304
+ );
52305
+ if (removeMatch) {
52306
+ const op = removeMatch[1] ?? removeMatch[2];
52307
+ return {
52308
+ rule: { kind: "tier1_remove_operation", operation: op },
52309
+ 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.`,
52310
+ confidence: "high",
52311
+ warnings: []
52312
+ };
52313
+ }
52314
+ const tier2Multiplier = normalized.match(
52315
+ /^set anomaly frequency multiplier to (\d+(?:\.\d+)?)\.?$/
52316
+ );
52317
+ if (tier2Multiplier) {
52318
+ const value = Number.parseFloat(tier2Multiplier[1]);
52319
+ return {
52320
+ rule: {
52321
+ kind: "tier2_set_field",
52322
+ tier2_update: { field: "frequency_spike_multiplier", value }
52323
+ },
52324
+ 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.`,
52325
+ confidence: "high",
52326
+ warnings: []
52327
+ };
52328
+ }
52329
+ const tier2MaxSigns = normalized.match(
52330
+ /^set max signs per minute to (\d+)\.?$/
52331
+ );
52332
+ if (tier2MaxSigns) {
52333
+ const value = Number.parseInt(tier2MaxSigns[1], 10);
52334
+ return {
52335
+ rule: {
52336
+ kind: "tier2_set_field",
52337
+ tier2_update: { field: "max_signs_per_minute", value }
52338
+ },
52339
+ explanation: `Caps Tier 2 signing operations at ${value} per minute. Crossing the cap fires an anomaly evaluation.`,
52340
+ confidence: "high",
52341
+ warnings: []
52342
+ };
52343
+ }
52344
+ return null;
52345
+ }
52346
+ function parseLlmOutput(text) {
52347
+ let parsed;
52348
+ try {
52349
+ const stripped = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
52350
+ parsed = JSON.parse(stripped);
52351
+ } catch (err) {
52352
+ return {
52353
+ ok: false,
52354
+ reason: `not valid JSON: ${err instanceof Error ? err.message : String(err)}`
52355
+ };
52356
+ }
52357
+ if (!parsed || typeof parsed !== "object") {
52358
+ return { ok: false, reason: "top-level is not an object" };
52359
+ }
52360
+ const obj = parsed;
52361
+ const ruleObj = obj["rule"];
52362
+ if (!ruleObj || typeof ruleObj !== "object") {
52363
+ return { ok: false, reason: "missing or non-object rule" };
52364
+ }
52365
+ const rule = ruleObj;
52366
+ const kindRaw = rule["kind"];
52367
+ if (typeof kindRaw !== "string" || !isValidKind(kindRaw)) {
52368
+ return { ok: false, reason: `invalid rule.kind: ${String(kindRaw)}` };
52369
+ }
52370
+ const compiledRule = { kind: kindRaw };
52371
+ if (typeof rule["operation"] === "string") {
52372
+ compiledRule.operation = rule["operation"];
52373
+ }
52374
+ const tier2 = rule["tier2_update"];
52375
+ if (tier2 && typeof tier2 === "object") {
52376
+ const fld = tier2["field"];
52377
+ const val = tier2["value"];
52378
+ if (typeof fld === "string" && (typeof val === "number" || typeof val === "string")) {
52379
+ compiledRule.tier2_update = {
52380
+ field: fld,
52381
+ value: val
52382
+ };
52383
+ }
52384
+ }
52385
+ if (kindRaw === "tier1_add_operation" || kindRaw === "tier1_remove_operation" || kindRaw === "tier3_add_operation" || kindRaw === "tier3_remove_operation") {
52386
+ if (typeof compiledRule.operation !== "string" || compiledRule.operation.length === 0) {
52387
+ return { ok: false, reason: `${kindRaw} requires operation` };
52388
+ }
52389
+ }
52390
+ if (kindRaw === "tier2_set_field" && compiledRule.tier2_update === void 0) {
52391
+ return { ok: false, reason: "tier2_set_field requires tier2_update" };
52392
+ }
52393
+ const explanation = typeof obj["explanation"] === "string" ? obj["explanation"] : "";
52394
+ if (!explanation) return { ok: false, reason: "missing explanation" };
52395
+ let confidence = "medium";
52396
+ const confRaw = obj["confidence"];
52397
+ if (confRaw === "high" || confRaw === "medium" || confRaw === "low") {
52398
+ confidence = confRaw;
52399
+ }
52400
+ const warnings = [];
52401
+ const warnRaw = obj["warnings"];
52402
+ if (Array.isArray(warnRaw)) {
52403
+ for (const w of warnRaw) {
52404
+ if (typeof w === "string") warnings.push(w);
52405
+ }
52406
+ }
52407
+ return { ok: true, rule: compiledRule, explanation, confidence, warnings };
52408
+ }
52409
+ function isValidKind(s) {
52410
+ return s === "tier1_add_operation" || s === "tier1_remove_operation" || s === "tier3_add_operation" || s === "tier3_remove_operation" || s === "tier2_set_field";
52411
+ }
52412
+ function buildLlmPromptContext() {
52413
+ return [
52414
+ "You compile a Sanctuary operator's plain-English policy statement into a structured rule.",
52415
+ "Output strictly a JSON object with these fields:",
52416
+ ' "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> }? }',
52417
+ ' "explanation": "<operator-facing prose paragraph>"',
52418
+ ' "confidence": "high" | "medium" | "low"',
52419
+ ' "warnings": [ "<short warning string>" ]',
52420
+ "Do NOT include any text outside the JSON. Do NOT activate the rule; this is a draft for operator review."
52421
+ ].join("\n");
52422
+ }
52423
+ function buildLlmPromptQuery(englishText) {
52424
+ return `Compile this operator policy statement: ${englishText}`;
52425
+ }
52426
+ var ENGLISH_POLICY_AUDIT_OPS, ENGLISH_POLICY_MAX_TEXT_CHARS, EnglishPolicyCompiler;
52427
+ var init_english_policy_compiler = __esm({
52428
+ "src/policy-engine/english-policy-compiler.ts"() {
52429
+ ENGLISH_POLICY_AUDIT_OPS = {
52430
+ DRAFTED: "english_policy_drafted",
52431
+ COMPILED: "english_policy_compiled",
52432
+ COMPILE_FAILED: "english_policy_compile_failed"
52433
+ };
52434
+ ENGLISH_POLICY_MAX_TEXT_CHARS = 2e3;
52435
+ EnglishPolicyCompiler = class {
52436
+ auditLog;
52437
+ fortressId;
52438
+ selector;
52439
+ now;
52440
+ constructor(deps) {
52441
+ this.auditLog = deps.auditLog;
52442
+ this.fortressId = deps.fortressId;
52443
+ this.selector = deps.selector ?? null;
52444
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
52445
+ }
52446
+ async compile(draft) {
52447
+ const draftId = computeDraftId(draft);
52448
+ this.auditLog.append(
52449
+ "l2",
52450
+ ENGLISH_POLICY_AUDIT_OPS.DRAFTED,
52451
+ draft.operator_id,
52452
+ {
52453
+ draft_id: draftId,
52454
+ text_length: draft.english_text.length,
52455
+ fortress_id: this.fortressId
52456
+ }
52457
+ );
52458
+ if (draft.english_text.length === 0) {
52459
+ return this.buildLowConfidence(
52460
+ draft,
52461
+ draftId,
52462
+ "Operator provided empty text.",
52463
+ "deterministic",
52464
+ ["empty input"]
52465
+ );
52466
+ }
52467
+ if (draft.english_text.length > ENGLISH_POLICY_MAX_TEXT_CHARS) {
52468
+ return this.buildLowConfidence(
52469
+ draft,
52470
+ draftId,
52471
+ `Operator text exceeded ${ENGLISH_POLICY_MAX_TEXT_CHARS} chars; refusing to compile to avoid prompt blowback.`,
52472
+ "deterministic",
52473
+ [`text exceeds ${ENGLISH_POLICY_MAX_TEXT_CHARS} chars`]
52474
+ );
52475
+ }
52476
+ const deterministic = compileDeterministic(draft.english_text);
52477
+ if (deterministic !== null) {
52478
+ const compiled = this.buildCompiled(
52479
+ draft,
52480
+ draftId,
52481
+ deterministic.rule,
52482
+ deterministic.explanation,
52483
+ deterministic.confidence,
52484
+ deterministic.warnings,
52485
+ "deterministic"
52486
+ );
52487
+ this.auditCompiled(compiled);
52488
+ return compiled;
52489
+ }
52490
+ if (this.selector === null) {
52491
+ const low = this.buildLowConfidence(
52492
+ draft,
52493
+ draftId,
52494
+ "No deterministic match; LLM-assist disabled.",
52495
+ "deterministic",
52496
+ ["no deterministic match", "LLM-assist disabled"]
52497
+ );
52498
+ this.auditCompileFailed(low, "no_llm_substrate");
52499
+ return low;
52500
+ }
52501
+ try {
52502
+ const resp = await this.selector.invokeSummarize("gate-explanation", {
52503
+ kind: "summarize",
52504
+ context: buildLlmPromptContext(),
52505
+ query: buildLlmPromptQuery(draft.english_text),
52506
+ maxTokens: 600
52507
+ });
52508
+ if (resp.failureClass) {
52509
+ const low = this.buildLowConfidence(
52510
+ draft,
52511
+ draftId,
52512
+ `LLM substrate failure: ${resp.failureClass}`,
52513
+ resp.servedBy,
52514
+ [`substrate failure: ${resp.failureClass}`]
52515
+ );
52516
+ this.auditCompileFailed(low, "substrate_failure");
52517
+ return low;
52518
+ }
52519
+ if (resp.body.kind !== "summarize") {
52520
+ const low = this.buildLowConfidence(
52521
+ draft,
52522
+ draftId,
52523
+ "LLM substrate returned a non-summarize response.",
52524
+ resp.servedBy,
52525
+ [`unexpected body kind: ${resp.body.kind}`]
52526
+ );
52527
+ this.auditCompileFailed(low, "unexpected_body");
52528
+ return low;
52529
+ }
52530
+ const parsed = parseLlmOutput(resp.body.text);
52531
+ if (!parsed.ok) {
52532
+ const low = this.buildLowConfidence(
52533
+ draft,
52534
+ draftId,
52535
+ `LLM output failed schema validation: ${parsed.reason}.`,
52536
+ resp.servedBy,
52537
+ [`schema validation failed: ${parsed.reason}`]
52538
+ );
52539
+ this.auditCompileFailed(low, "schema_validation_failed");
52540
+ return low;
52541
+ }
52542
+ const compiled = this.buildCompiled(
52543
+ draft,
52544
+ draftId,
52545
+ parsed.rule,
52546
+ parsed.explanation,
52547
+ parsed.confidence,
52548
+ parsed.warnings,
52549
+ resp.servedBy
52550
+ );
52551
+ this.auditCompiled(compiled);
52552
+ return compiled;
52553
+ } catch (err) {
52554
+ const msg = err instanceof Error ? err.message : String(err);
52555
+ const low = this.buildLowConfidence(
52556
+ draft,
52557
+ draftId,
52558
+ `LLM-assist threw: ${msg}`,
52559
+ "unknown",
52560
+ [msg]
52561
+ );
52562
+ this.auditCompileFailed(low, "llm_threw");
52563
+ return low;
52564
+ }
52565
+ }
52566
+ // ── helpers ────────────────────────────────────────────────────────
52567
+ buildCompiled(draft, draftId, rule, explanation, confidence, warnings, substrateUsed) {
52568
+ return {
52569
+ draft_id: draftId,
52570
+ english_text: draft.english_text,
52571
+ compiled_rule: rule,
52572
+ explanation_paragraph: explanation,
52573
+ compile_confidence: confidence,
52574
+ compile_warnings: warnings,
52575
+ substrate_used: substrateUsed,
52576
+ compiled_at: this.now().toISOString(),
52577
+ operator_id: draft.operator_id,
52578
+ fortress_id: this.fortressId
52579
+ };
52580
+ }
52581
+ buildLowConfidence(draft, draftId, explanation, substrateUsed, warnings) {
52582
+ return this.buildCompiled(
52583
+ draft,
52584
+ draftId,
52585
+ // Placeholder rule; operator inspects the draft + decides whether
52586
+ // to discard or re-author. Xi-2's activation flow refuses to
52587
+ // activate compile_confidence "low" without explicit operator
52588
+ // override.
52589
+ { kind: "tier1_add_operation", operation: "__low_confidence_placeholder__" },
52590
+ explanation,
52591
+ "low",
52592
+ warnings,
52593
+ substrateUsed
52594
+ );
52595
+ }
52596
+ auditCompiled(compiled) {
52597
+ this.auditLog.append(
52598
+ "l2",
52599
+ ENGLISH_POLICY_AUDIT_OPS.COMPILED,
52600
+ compiled.operator_id,
52601
+ {
52602
+ draft_id: compiled.draft_id,
52603
+ compile_confidence: compiled.compile_confidence,
52604
+ rule_kind: compiled.compiled_rule.kind,
52605
+ warnings_count: compiled.compile_warnings.length,
52606
+ substrate_used: compiled.substrate_used,
52607
+ fortress_id: compiled.fortress_id
52608
+ }
52609
+ );
52610
+ }
52611
+ auditCompileFailed(compiled, reason) {
52612
+ this.auditLog.append(
52613
+ "l2",
52614
+ ENGLISH_POLICY_AUDIT_OPS.COMPILE_FAILED,
52615
+ compiled.operator_id,
52616
+ {
52617
+ draft_id: compiled.draft_id,
52618
+ reason,
52619
+ warnings: compiled.compile_warnings,
52620
+ fortress_id: compiled.fortress_id
52621
+ },
52622
+ "failure"
52623
+ );
52624
+ }
52625
+ };
52626
+ }
52627
+ });
52628
+
52629
+ // src/cli/policy.ts
52630
+ var policy_exports = {};
52631
+ __export(policy_exports, {
52632
+ formatCompiledHumanReadable: () => formatCompiledHumanReadable,
52633
+ runPolicyCommand: () => runPolicyCommand
52634
+ });
52635
+ async function runPolicyCommand(args) {
52636
+ const out = args.out ?? process.stdout;
52637
+ const err = args.err ?? process.stderr;
52638
+ const [sub, ...rest] = args.argv;
52639
+ if (!sub || sub === "--help" || sub === "-h") {
52640
+ printUsage9(out);
52641
+ return 0;
52642
+ }
52643
+ try {
52644
+ switch (sub) {
52645
+ case "compile":
52646
+ return await cmdCompile(rest, { out, err });
52647
+ case "drafts":
52648
+ return await cmdDrafts(rest, { out, err });
52649
+ default:
52650
+ err.write(`Unknown subcommand: ${sub}
52651
+ `);
52652
+ printUsage9(err);
52653
+ return 2;
52654
+ }
52655
+ } catch (e) {
52656
+ const msg = e instanceof Error ? e.message : String(e);
52657
+ err.write(`sanctuary policy: ${msg}
52658
+ `);
52659
+ return 1;
52660
+ }
52661
+ }
52662
+ function printUsage9(s) {
52663
+ s.write(`Usage: sanctuary policy <command> [args]
52664
+
52665
+ compile "<English text>" Compile an operator policy statement
52666
+ to a structured rule + explanation.
52667
+ CLI mode runs deterministic matcher
52668
+ only; LLM-assist is server-only.
52669
+ drafts list Placeholder for Xi-2 persistence.
52670
+ drafts show <draft_id> Placeholder for Xi-2 persistence.
52671
+
52672
+ Xi-1 ships review-only; activation (Xi-2) is a separate flow.
52673
+
52674
+ `);
52675
+ }
52676
+ async function cmdCompile(argv, ctx) {
52677
+ const englishText = argv[0];
52678
+ if (!englishText) {
52679
+ ctx.err.write("compile requires the English text as a single argument:\n");
52680
+ ctx.err.write(' sanctuary policy compile "always require approval for state_export"\n');
52681
+ return 2;
52682
+ }
52683
+ const storage = new MemoryStorage();
52684
+ const masterKey = generateRandomKey();
52685
+ const auditLog = new AuditLog(storage, masterKey);
52686
+ const compiler = new EnglishPolicyCompiler({
52687
+ auditLog,
52688
+ fortressId: "cli-local",
52689
+ selector: null
52690
+ });
52691
+ const compiled = await compiler.compile({
52692
+ english_text: englishText,
52693
+ observed_at: (/* @__PURE__ */ new Date()).toISOString(),
52694
+ operator_id: "cli-operator"
52695
+ });
52696
+ ctx.out.write(formatCompiledHumanReadable(compiled) + "\n");
52697
+ return 0;
52698
+ }
52699
+ async function cmdDrafts(argv, ctx) {
52700
+ const [sub, ...rest] = argv;
52701
+ if (sub === "list" || sub === void 0) {
52702
+ ctx.out.write(
52703
+ "(no local drafts persisted in CLI mode; drafts live in the running server's in-memory store)\n"
52704
+ );
52705
+ ctx.out.write(
52706
+ 'Use "sanctuary policy compile \\"<text>\\"" to preview a compile result locally,\n'
52707
+ );
52708
+ ctx.out.write(
52709
+ "or POST to /api/policy/compile on the running fortress for the full surface.\n"
52710
+ );
52711
+ return 0;
52712
+ }
52713
+ if (sub === "show") {
52714
+ const draftId = rest[0];
52715
+ if (!draftId) {
52716
+ ctx.err.write("drafts show requires a draft_id\n");
52717
+ return 2;
52718
+ }
52719
+ ctx.out.write(
52720
+ `(CLI does not yet persist drafts; show ${draftId} via GET /api/policy/drafts/${draftId} on the running fortress)
52721
+ `
52722
+ );
52723
+ return 0;
52724
+ }
52725
+ ctx.err.write(`Unknown drafts subcommand: ${sub}
52726
+ `);
52727
+ return 2;
52728
+ }
52729
+ function formatCompiledHumanReadable(c) {
52730
+ const lines = [];
52731
+ lines.push(`draft_id: ${c.draft_id}`);
52732
+ lines.push(`compile_confidence: ${c.compile_confidence}`);
52733
+ lines.push(`substrate_used: ${c.substrate_used}`);
52734
+ lines.push(`compiled_at: ${c.compiled_at}`);
52735
+ lines.push(``);
52736
+ lines.push(`english_text:`);
52737
+ lines.push(` ${c.english_text}`);
52738
+ lines.push(``);
52739
+ lines.push(`compiled_rule:`);
52740
+ lines.push(` kind: ${c.compiled_rule.kind}`);
52741
+ if (c.compiled_rule.operation !== void 0) {
52742
+ lines.push(` operation: ${c.compiled_rule.operation}`);
52743
+ }
52744
+ if (c.compiled_rule.tier2_update !== void 0) {
52745
+ lines.push(` tier2_update.field: ${c.compiled_rule.tier2_update.field}`);
52746
+ lines.push(` tier2_update.value: ${String(c.compiled_rule.tier2_update.value)}`);
52747
+ }
52748
+ lines.push(``);
52749
+ lines.push(`explanation:`);
52750
+ lines.push(` ${c.explanation_paragraph}`);
52751
+ if (c.compile_warnings.length > 0) {
52752
+ lines.push(``);
52753
+ lines.push(`warnings:`);
52754
+ for (const w of c.compile_warnings) lines.push(` - ${w}`);
52755
+ }
52756
+ return lines.join("\n");
52757
+ }
52758
+ var init_policy2 = __esm({
52759
+ "src/cli/policy.ts"() {
52760
+ init_audit_log();
52761
+ init_memory();
52762
+ init_random();
52763
+ init_english_policy_compiler();
52764
+ }
52765
+ });
52766
+
52767
+ // src/auto-trigger/types.ts
52768
+ function defaultRuleConfig(ruleId, ruleType, fortressId, now = () => /* @__PURE__ */ new Date()) {
52769
+ return {
52770
+ rule_id: ruleId,
52771
+ rule_type: ruleType,
52772
+ fortress_id: fortressId,
52773
+ current_rung: 1,
52774
+ threshold_overrides: {},
52775
+ cancel_window_seconds: DEFAULT_CANCEL_WINDOW_SECONDS,
52776
+ history: [],
52777
+ updated_at: now().toISOString()
52778
+ };
52779
+ }
52780
+ function appendHistory(history, entry) {
52781
+ const next = [...history, entry];
52782
+ if (next.length <= MAX_HISTORY_PER_RULE) return next;
52783
+ return next.slice(next.length - MAX_HISTORY_PER_RULE);
52784
+ }
52785
+ var DEFAULT_CANCEL_WINDOW_SECONDS, MAX_HISTORY_PER_RULE, AutoTriggerError;
52786
+ var init_types7 = __esm({
52787
+ "src/auto-trigger/types.ts"() {
52788
+ DEFAULT_CANCEL_WINDOW_SECONDS = 60;
52789
+ MAX_HISTORY_PER_RULE = 200;
52790
+ AutoTriggerError = class extends Error {
52791
+ constructor(message, code) {
52792
+ super(message);
52793
+ this.code = code;
52794
+ this.name = "AutoTriggerError";
52795
+ }
52796
+ code;
52797
+ };
52798
+ }
52799
+ });
52800
+
52801
+ // src/auto-trigger/threshold-config-store.ts
52802
+ function ruleStorageKey(ruleId) {
52803
+ return `${AUTO_TRIGGER_RULE_KEY_PREFIX}${ruleId}`;
52804
+ }
52805
+ function aadFor2(ruleId, fortressId) {
52806
+ return `${ruleId}|${fortressId}`;
52807
+ }
52808
+ var AUTO_TRIGGER_RULES_NAMESPACE, AUTO_TRIGGER_RULE_KEY_PREFIX, HKDF_INFO8, MAX_RULE_BYTES, ThresholdConfigStore;
52809
+ var init_threshold_config_store = __esm({
52810
+ "src/auto-trigger/threshold-config-store.ts"() {
52811
+ init_encryption();
52812
+ init_key_derivation();
52813
+ init_encoding();
52814
+ init_types7();
52815
+ AUTO_TRIGGER_RULES_NAMESPACE = "_auto_trigger_rules";
52816
+ AUTO_TRIGGER_RULE_KEY_PREFIX = "rule.";
52817
+ HKDF_INFO8 = "l2-auto-trigger-rules-v1";
52818
+ MAX_RULE_BYTES = 256 * 1024;
52819
+ ThresholdConfigStore = class {
52820
+ storage;
52821
+ encryptionKey;
52822
+ fortressId;
52823
+ now;
52824
+ constructor(opts) {
52825
+ this.storage = opts.storage;
52826
+ this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO8);
52827
+ this.fortressId = opts.fortressId;
52828
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
52829
+ }
52830
+ /** Read a rule config. Returns null when absent. */
52831
+ async get(ruleId) {
52832
+ const key = ruleStorageKey(ruleId);
52833
+ let raw;
52834
+ try {
52835
+ raw = await this.storage.read(AUTO_TRIGGER_RULES_NAMESPACE, key);
52836
+ } catch {
52837
+ return null;
52838
+ }
52839
+ if (!raw) return null;
52840
+ if (raw.length > MAX_RULE_BYTES) return null;
52841
+ try {
52842
+ const aad = stringToBytes(aadFor2(ruleId, this.fortressId));
52843
+ const envelope = JSON.parse(bytesToString(raw));
52844
+ const plaintext = decrypt(envelope, this.encryptionKey, aad);
52845
+ const persisted = JSON.parse(
52846
+ bytesToString(plaintext)
52847
+ );
52848
+ if (persisted.version !== 1) return null;
52849
+ if (persisted.rule_id !== ruleId) return null;
52850
+ if (persisted.fortress_id !== this.fortressId) return null;
52851
+ return persisted.config;
52852
+ } catch {
52853
+ return null;
52854
+ }
52855
+ }
52856
+ /**
52857
+ * Read a rule config, creating a fresh Rung-1 default if absent. The
52858
+ * default is persisted on first access so the dispatcher sees a
52859
+ * consistent shape on subsequent reads.
52860
+ */
52861
+ async getOrInit(ruleId, ruleType) {
52862
+ const existing = await this.get(ruleId);
52863
+ if (existing) return existing;
52864
+ const fresh = defaultRuleConfig(
52865
+ ruleId,
52866
+ ruleType,
52867
+ this.fortressId,
52868
+ this.now
52869
+ );
52870
+ await this.set(fresh);
52871
+ return fresh;
52872
+ }
52873
+ /** Persist a rule config. AAD-binds to (rule_id, fortress_id). */
52874
+ async set(config) {
52875
+ if (config.fortress_id !== this.fortressId) {
52876
+ throw new Error(
52877
+ `ThresholdConfigStore: fortress_id mismatch (got ${config.fortress_id}, store bound to ${this.fortressId})`
52878
+ );
52879
+ }
52880
+ const persisted = {
52881
+ version: 1,
52882
+ rule_id: config.rule_id,
52883
+ fortress_id: this.fortressId,
52884
+ saved_at: this.now().toISOString(),
52885
+ config: {
52886
+ ...config,
52887
+ updated_at: this.now().toISOString()
52888
+ }
52889
+ };
52890
+ const aad = stringToBytes(aadFor2(config.rule_id, this.fortressId));
52891
+ const plaintext = stringToBytes(JSON.stringify(persisted));
52892
+ const envelope = encrypt(plaintext, this.encryptionKey, aad);
52893
+ await this.storage.write(
52894
+ AUTO_TRIGGER_RULES_NAMESPACE,
52895
+ ruleStorageKey(config.rule_id),
52896
+ stringToBytes(JSON.stringify(envelope))
52897
+ );
52898
+ }
52899
+ /** Promote a rule one rung up. Throws on ceiling (rung 3). */
52900
+ async promote(ruleId, ruleType) {
52901
+ const config = await this.getOrInit(ruleId, ruleType);
52902
+ if (config.current_rung >= 3) {
52903
+ throw new AutoTriggerError(
52904
+ `rule ${ruleId} already at rung 3 (ceiling)`,
52905
+ "rung_ceiling"
52906
+ );
52907
+ }
52908
+ const next = {
52909
+ ...config,
52910
+ current_rung: config.current_rung + 1,
52911
+ last_promoted_at: this.now().toISOString()
52912
+ };
52913
+ await this.set(next);
52914
+ return next;
52915
+ }
52916
+ /** Demote a rule one rung down. Throws on floor (rung 1). */
52917
+ async demote(ruleId, ruleType) {
52918
+ const config = await this.getOrInit(ruleId, ruleType);
52919
+ if (config.current_rung <= 1) {
52920
+ throw new AutoTriggerError(
52921
+ `rule ${ruleId} already at rung 1 (floor)`,
52922
+ "rung_floor"
52923
+ );
52924
+ }
52925
+ const next = {
52926
+ ...config,
52927
+ current_rung: config.current_rung - 1,
52928
+ last_demoted_at: this.now().toISOString()
52929
+ };
52930
+ await this.set(next);
52931
+ return next;
52932
+ }
52933
+ /** Patch the threshold overrides + optional cancel-window. */
52934
+ async updateConfig(ruleId, ruleType, patch) {
52935
+ const config = await this.getOrInit(ruleId, ruleType);
52936
+ const next = {
52937
+ ...config,
52938
+ threshold_overrides: patch.threshold_overrides ?? config.threshold_overrides,
52939
+ cancel_window_seconds: patch.cancel_window_seconds ?? config.cancel_window_seconds
52940
+ };
52941
+ await this.set(next);
52942
+ return next;
52943
+ }
52944
+ /**
52945
+ * Append an action history entry. Bounded ring buffer (max 200 per
52946
+ * rule); oldest entries drop.
52947
+ */
52948
+ async recordAction(ruleId, ruleType, entry) {
52949
+ const config = await this.getOrInit(ruleId, ruleType);
52950
+ const next = {
52951
+ ...config,
52952
+ history: appendHistory(config.history, entry)
52953
+ };
52954
+ await this.set(next);
52955
+ return next;
52956
+ }
52957
+ /**
52958
+ * Update the outcome of a specific pending history entry (e.g. when a
52959
+ * cancel-window expires or the operator cancels). Identified by
52960
+ * finding_id. No-op when the entry is absent. Returns the updated
52961
+ * config; the matched entry's outcome is replaced.
52962
+ */
52963
+ async updateActionOutcome(ruleId, ruleType, findingId, outcome) {
52964
+ const config = await this.getOrInit(ruleId, ruleType);
52965
+ const idx = config.history.findIndex((h) => h.finding_id === findingId);
52966
+ if (idx < 0) return config;
52967
+ const updatedHistory = [...config.history];
52968
+ updatedHistory[idx] = { ...updatedHistory[idx], outcome };
52969
+ const next = { ...config, history: updatedHistory };
52970
+ await this.set(next);
52971
+ return next;
52972
+ }
52973
+ /** Delete a rule config (and its history) by id. */
52974
+ async delete(ruleId) {
52975
+ const key = ruleStorageKey(ruleId);
52976
+ const exists = await this.storage.exists(
52977
+ AUTO_TRIGGER_RULES_NAMESPACE,
52978
+ key
52979
+ );
52980
+ if (!exists) return false;
52981
+ try {
52982
+ await this.storage.delete(AUTO_TRIGGER_RULES_NAMESPACE, key);
52983
+ } catch {
52984
+ return false;
52985
+ }
52986
+ return true;
52987
+ }
52988
+ /** Enumerate every rule id persisted for this fortress. */
52989
+ async listRuleIds() {
52990
+ const metas = await this.storage.list(
52991
+ AUTO_TRIGGER_RULES_NAMESPACE,
52992
+ AUTO_TRIGGER_RULE_KEY_PREFIX
52993
+ );
52994
+ const out = [];
52995
+ for (const meta of metas) {
52996
+ if (!meta.key.startsWith(AUTO_TRIGGER_RULE_KEY_PREFIX)) continue;
52997
+ out.push(meta.key.slice(AUTO_TRIGGER_RULE_KEY_PREFIX.length));
52998
+ }
52999
+ return out;
53000
+ }
53001
+ };
53002
+ }
53003
+ });
53004
+
53005
+ // src/cli/auto-trigger.ts
53006
+ var auto_trigger_exports = {};
53007
+ __export(auto_trigger_exports, {
53008
+ runAutoTriggerCommand: () => runAutoTriggerCommand
53009
+ });
53010
+ async function runAutoTriggerCommand(args) {
53011
+ const out = args.out ?? process.stdout;
53012
+ const err = args.err ?? process.stderr;
53013
+ const [sub, ...rest] = args.argv;
53014
+ if (!sub || sub === "--help" || sub === "-h") {
53015
+ printUsage10(out);
53016
+ return 0;
53017
+ }
53018
+ try {
53019
+ switch (sub) {
53020
+ case "rules":
53021
+ return await cmdRules(rest, { out, err, args });
53022
+ case "cancel":
53023
+ return await cmdCancel(rest, { out, err });
53024
+ default:
53025
+ err.write(`Unknown subcommand: ${sub}
53026
+ `);
53027
+ printUsage10(err);
53028
+ return 2;
53029
+ }
53030
+ } catch (cause) {
53031
+ const msg = cause instanceof Error ? cause.message : String(cause);
53032
+ err.write(`sanctuary auto-trigger: ${msg}
53033
+ `);
53034
+ return 1;
53035
+ }
53036
+ }
53037
+ function printUsage10(s) {
53038
+ s.write(`Usage: sanctuary auto-trigger <command> [args]
53039
+
53040
+ rules list List rules + current rungs.
53041
+ rules show <rule_id> Full detail + history.
53042
+ rules promote <rule_id> --rule-type <t> Rung N -> N+1.
53043
+ rules demote <rule_id> --rule-type <t> Rung N -> N-1.
53044
+ rules set-threshold <rule_id> --rule-type <t>
53045
+ [--warn-sigma <n>] [--alert-sigma <n>]
53046
+ [--cancel-window <s>] Update overrides + window.
53047
+ cancel <finding_id> Cancel a pending rung-2
53048
+ action (HTTP-delegated to
53049
+ the local dashboard).
53050
+
53051
+ Rule types: sentinel | anomaly | honeypot
53052
+
53053
+ Env (for cancel): SANCTUARY_DASHBOARD_URL, SANCTUARY_DASHBOARD_AUTH_TOKEN
53054
+ `);
53055
+ }
53056
+ async function cmdRules(argv, ctx) {
53057
+ const sub = argv[0];
53058
+ if (!sub) {
53059
+ ctx.err.write("rules subcommand required\n");
53060
+ return 2;
53061
+ }
53062
+ switch (sub) {
53063
+ case "list":
53064
+ return await cmdRulesList(ctx);
53065
+ case "show":
53066
+ return await cmdRulesShow(argv.slice(1), ctx);
53067
+ case "promote":
53068
+ return await cmdRulesPromote(argv.slice(1), ctx);
53069
+ case "demote":
53070
+ return await cmdRulesDemote(argv.slice(1), ctx);
53071
+ case "set-threshold":
53072
+ return await cmdRulesSetThreshold(argv.slice(1), ctx);
53073
+ default:
53074
+ ctx.err.write(`Unknown rules subcommand: ${sub}
53075
+ `);
53076
+ return 2;
53077
+ }
53078
+ }
53079
+ async function cmdRulesList(ctx) {
53080
+ const { store } = await openStore(ctx.args);
53081
+ const ids = await store.listRuleIds();
53082
+ if (ids.length === 0) {
53083
+ ctx.out.write("(no rules configured; defaults are Rung 1 / no overrides)\n");
53084
+ return 0;
53085
+ }
53086
+ for (const id of ids) {
53087
+ const config = await store.get(id);
53088
+ if (!config) continue;
53089
+ const overrides = Object.keys(config.threshold_overrides).length === 0 ? "defaults" : JSON.stringify(config.threshold_overrides);
53090
+ ctx.out.write(
53091
+ `${id} [type: ${config.rule_type}] rung=${config.current_rung} cancel-window=${config.cancel_window_seconds}s overrides=${overrides}
53092
+ `
53093
+ );
53094
+ }
53095
+ return 0;
53096
+ }
53097
+ async function cmdRulesShow(argv, ctx) {
53098
+ const ruleId = argv[0];
53099
+ if (!ruleId) {
53100
+ ctx.err.write("show requires a rule_id\n");
53101
+ return 2;
53102
+ }
53103
+ const { store } = await openStore(ctx.args);
53104
+ const config = await store.get(ruleId);
53105
+ if (!config) {
53106
+ ctx.err.write(`rule not found: ${ruleId}
53107
+ `);
53108
+ return 1;
53109
+ }
53110
+ ctx.out.write(JSON.stringify(config, null, 2) + "\n");
53111
+ return 0;
53112
+ }
53113
+ async function cmdRulesPromote(argv, ctx) {
53114
+ const ruleId = argv[0];
53115
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53116
+ if (!ruleId) {
53117
+ ctx.err.write("promote requires a rule_id\n");
53118
+ return 2;
53119
+ }
53120
+ if (!ruleType) {
53121
+ ctx.err.write(
53122
+ "promote requires --rule-type (sentinel|anomaly|honeypot)\n"
53123
+ );
53124
+ return 2;
53125
+ }
53126
+ const { store } = await openStore(ctx.args);
53127
+ try {
53128
+ const after = await store.promote(ruleId, ruleType);
53129
+ ctx.out.write(`Promoted ${ruleId} -> rung ${after.current_rung}
53130
+ `);
53131
+ return 0;
53132
+ } catch (err) {
53133
+ if (err instanceof AutoTriggerError) {
53134
+ ctx.err.write(`${err.code}: ${err.message}
53135
+ `);
53136
+ return 1;
53137
+ }
53138
+ throw err;
53139
+ }
53140
+ }
53141
+ async function cmdRulesDemote(argv, ctx) {
53142
+ const ruleId = argv[0];
53143
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53144
+ if (!ruleId) {
53145
+ ctx.err.write("demote requires a rule_id\n");
53146
+ return 2;
53147
+ }
53148
+ if (!ruleType) {
53149
+ ctx.err.write(
53150
+ "demote requires --rule-type (sentinel|anomaly|honeypot)\n"
53151
+ );
53152
+ return 2;
53153
+ }
53154
+ const { store } = await openStore(ctx.args);
53155
+ try {
53156
+ const after = await store.demote(ruleId, ruleType);
53157
+ ctx.out.write(`Demoted ${ruleId} -> rung ${after.current_rung}
53158
+ `);
53159
+ return 0;
53160
+ } catch (err) {
53161
+ if (err instanceof AutoTriggerError) {
53162
+ ctx.err.write(`${err.code}: ${err.message}
53163
+ `);
53164
+ return 1;
53165
+ }
53166
+ throw err;
53167
+ }
53168
+ }
53169
+ async function cmdRulesSetThreshold(argv, ctx) {
53170
+ const ruleId = argv[0];
53171
+ const ruleType = parseRuleType(flagValue5(argv, "--rule-type"));
53172
+ if (!ruleId) {
53173
+ ctx.err.write("set-threshold requires a rule_id\n");
53174
+ return 2;
53175
+ }
53176
+ if (!ruleType) {
53177
+ ctx.err.write(
53178
+ "set-threshold requires --rule-type (sentinel|anomaly|honeypot)\n"
53179
+ );
53180
+ return 2;
53181
+ }
53182
+ const warnSigma = parseNumberFlag(argv, "--warn-sigma");
53183
+ const alertSigma = parseNumberFlag(argv, "--alert-sigma");
53184
+ const cancelWindow = parseNumberFlag(argv, "--cancel-window");
53185
+ const overrides = {};
53186
+ if (warnSigma !== void 0) overrides.warn_sigma = warnSigma;
53187
+ if (alertSigma !== void 0) overrides.alert_sigma = alertSigma;
53188
+ if (Object.keys(overrides).length === 0 && cancelWindow === void 0) {
53189
+ ctx.err.write(
53190
+ "set-threshold requires at least one of --warn-sigma, --alert-sigma, --cancel-window\n"
53191
+ );
53192
+ return 2;
53193
+ }
53194
+ const { store } = await openStore(ctx.args);
53195
+ const patch = {};
53196
+ if (Object.keys(overrides).length > 0) patch.threshold_overrides = overrides;
53197
+ if (cancelWindow !== void 0) patch.cancel_window_seconds = cancelWindow;
53198
+ const after = await store.updateConfig(ruleId, ruleType, patch);
53199
+ ctx.out.write(
53200
+ `Updated ${ruleId}: overrides=${JSON.stringify(after.threshold_overrides)} cancel-window=${after.cancel_window_seconds}s
53201
+ `
53202
+ );
53203
+ return 0;
53204
+ }
53205
+ async function cmdCancel(argv, ctx) {
53206
+ const findingId = argv[0];
53207
+ if (!findingId) {
53208
+ ctx.err.write("cancel requires a finding_id\n");
53209
+ return 2;
53210
+ }
53211
+ const baseUrl = process.env["SANCTUARY_DASHBOARD_URL"] ?? "http://127.0.0.1:3501";
53212
+ const token = process.env["SANCTUARY_DASHBOARD_AUTH_TOKEN"];
53213
+ const headers = {
53214
+ "Content-Type": "application/json"
53215
+ };
53216
+ if (token) headers["Authorization"] = `Bearer ${token}`;
53217
+ let res;
53218
+ try {
53219
+ res = await fetch(`${baseUrl}/api/auto-trigger/cancel/${encodeURIComponent(findingId)}`, {
53220
+ method: "POST",
53221
+ headers
53222
+ });
53223
+ } catch (err) {
53224
+ const msg = err instanceof Error ? err.message : String(err);
53225
+ ctx.err.write(`cancel: cannot reach dashboard at ${baseUrl}: ${msg}
53226
+ `);
53227
+ return 1;
53228
+ }
53229
+ if (res.status === 200) {
53230
+ ctx.out.write(`Canceled pending action: ${findingId}
53231
+ `);
53232
+ return 0;
53233
+ }
53234
+ if (res.status === 404) {
53235
+ ctx.err.write(
53236
+ `No pending action for ${findingId} (window may have expired)
53237
+ `
53238
+ );
53239
+ return 1;
53240
+ }
53241
+ if (res.status === 401) {
53242
+ ctx.err.write(
53243
+ `cancel: unauthorized (set SANCTUARY_DASHBOARD_AUTH_TOKEN)
53244
+ `
53245
+ );
53246
+ return 1;
53247
+ }
53248
+ ctx.err.write(`cancel: HTTP ${res.status}
53249
+ `);
53250
+ return 1;
53251
+ }
53252
+ function flagValue5(argv, name) {
53253
+ const i = argv.indexOf(name);
53254
+ if (i === -1) return void 0;
53255
+ return argv[i + 1];
53256
+ }
53257
+ function parseNumberFlag(argv, name) {
53258
+ const raw = flagValue5(argv, name);
53259
+ if (raw === void 0) return void 0;
53260
+ const n = Number.parseFloat(raw);
53261
+ if (Number.isNaN(n)) return void 0;
53262
+ return n;
53263
+ }
53264
+ function parseRuleType(raw) {
53265
+ if (raw === "sentinel" || raw === "anomaly" || raw === "honeypot") return raw;
53266
+ return null;
53267
+ }
53268
+ async function resolveStoragePath4(args) {
53269
+ if (args.storagePath) return args.storagePath;
53270
+ const config = await loadConfig();
53271
+ return config.storage_path;
53272
+ }
53273
+ async function openStore(args) {
53274
+ const storagePath = await resolveStoragePath4(args);
53275
+ const storage = new FilesystemStorage(`${storagePath}/state`);
53276
+ let passphrase = args.passphrase ?? process.env["SANCTUARY_PASSPHRASE"];
53277
+ if (!passphrase) {
53278
+ const resolved = await getOrCreatePassphrase();
53279
+ passphrase = resolved.value;
53280
+ }
53281
+ let existingParams;
53282
+ try {
53283
+ const raw = await storage.read("_meta", "key-params");
53284
+ if (raw) existingParams = JSON.parse(bytesToString(raw));
53285
+ } catch {
53286
+ }
53287
+ const { key: masterKey, params } = await deriveMasterKey(
53288
+ passphrase,
53289
+ existingParams
53290
+ );
53291
+ if (!existingParams) {
53292
+ await storage.write(
53293
+ "_meta",
53294
+ "key-params",
53295
+ stringToBytes(JSON.stringify(params))
53296
+ );
53297
+ }
53298
+ const fortressId = fortressIdFromStoragePath(storagePath);
53299
+ const store = new ThresholdConfigStore({
53300
+ storage,
53301
+ masterKey,
53302
+ fortressId
53303
+ });
53304
+ return { store };
53305
+ }
53306
+ var init_auto_trigger = __esm({
53307
+ "src/cli/auto-trigger.ts"() {
53308
+ init_config();
53309
+ init_filesystem();
53310
+ init_key_derivation();
53311
+ init_encoding();
53312
+ init_passphrase();
53313
+ init_wiring();
53314
+ init_threshold_config_store();
53315
+ init_types7();
51162
53316
  }
51163
53317
  });
51164
53318
 
@@ -52036,6 +54190,16 @@ async function main() {
52036
54190
  const code = await runAnomalyCommand2({ argv: args.slice(1) });
52037
54191
  process.exit(code);
52038
54192
  }
54193
+ if (args[0] === "policy") {
54194
+ const { runPolicyCommand: runPolicyCommand2 } = await Promise.resolve().then(() => (init_policy2(), policy_exports));
54195
+ const code = await runPolicyCommand2({ argv: args.slice(1) });
54196
+ process.exit(code);
54197
+ }
54198
+ if (args[0] === "auto-trigger") {
54199
+ const { runAutoTriggerCommand: runAutoTriggerCommand2 } = await Promise.resolve().then(() => (init_auto_trigger(), auto_trigger_exports));
54200
+ const code = await runAutoTriggerCommand2({ argv: args.slice(1) });
54201
+ process.exit(code);
54202
+ }
52039
54203
  if (args[0] === "broker-server") {
52040
54204
  const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
52041
54205
  const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));