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