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