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