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