@cybedefend/vibedefend 1.2.4 → 1.3.0
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/README.md +8 -1
- package/dist/config.js +15 -5
- package/dist/config.js.map +1 -1
- package/dist/doctor.js +23 -6
- package/dist/doctor.js.map +1 -1
- package/dist/guards-evaluator/redact.js +25 -0
- package/dist/guards-evaluator/redact.js.map +1 -1
- package/dist/hook-runner.js +838 -96
- package/dist/hooks/install.js +1 -0
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/runtime/emit.js +35 -0
- package/dist/hooks/runtime/emit.js.map +1 -1
- package/dist/hooks/runtime/guard-rules-cache.js +24 -9
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
- package/dist/hooks/runtime/judge-cache.js +64 -0
- package/dist/hooks/runtime/judge-cache.js.map +1 -0
- package/dist/hooks/runtime/judge.js +45 -0
- package/dist/hooks/runtime/judge.js.map +1 -0
- package/dist/hooks/runtime/near-miss.js +362 -0
- package/dist/hooks/runtime/near-miss.js.map +1 -0
- package/dist/hooks/runtime/resolve.js +28 -15
- package/dist/hooks/runtime/resolve.js.map +1 -1
- package/dist/hooks/runtime/session-review.js +261 -28
- package/dist/hooks/runtime/session-review.js.map +1 -1
- package/dist/hooks/runtime/sniff.js +1 -1
- package/dist/hooks/runtime/sniff.js.map +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
- package/dist/prompts.js +13 -3
- package/dist/prompts.js.map +1 -1
- package/package.json +1 -1
package/dist/hook-runner.js
CHANGED
|
@@ -4850,18 +4850,20 @@ function resolveTokenLinux(mcpName) {
|
|
|
4850
4850
|
return null;
|
|
4851
4851
|
}
|
|
4852
4852
|
}
|
|
4853
|
-
async function resolveTokenWithRefresh(mcpName) {
|
|
4853
|
+
async function resolveTokenWithRefresh(mcpName, deps = {}) {
|
|
4854
|
+
const getValidAccessTokenFn = deps.getValidAccessToken ?? getValidAccessToken;
|
|
4855
|
+
const resolveTokenFn = deps.resolveTokenFn ?? resolveToken;
|
|
4854
4856
|
const envToken = process.env.CYBEDEFEND_TOKEN;
|
|
4855
4857
|
if (envToken && envToken.length > 0) return envToken;
|
|
4856
4858
|
try {
|
|
4857
|
-
const token = await
|
|
4859
|
+
const token = await getValidAccessTokenFn(mcpName);
|
|
4858
4860
|
if (token) return token;
|
|
4859
4861
|
} catch (e) {
|
|
4860
4862
|
if (e instanceof LoginRequiredError) {
|
|
4861
|
-
return
|
|
4863
|
+
return resolveTokenFn(mcpName);
|
|
4862
4864
|
}
|
|
4863
4865
|
}
|
|
4864
|
-
return
|
|
4866
|
+
return resolveTokenFn(mcpName);
|
|
4865
4867
|
}
|
|
4866
4868
|
|
|
4867
4869
|
// src/hooks/runtime/api.ts
|
|
@@ -4990,6 +4992,19 @@ function emitContext(body, client) {
|
|
|
4990
4992
|
}
|
|
4991
4993
|
emit(body, client);
|
|
4992
4994
|
}
|
|
4995
|
+
function emitStop(body, client, reason) {
|
|
4996
|
+
if (!body) return;
|
|
4997
|
+
if (client === "claude-code") {
|
|
4998
|
+
process.stdout.write(
|
|
4999
|
+
JSON.stringify({
|
|
5000
|
+
decision: "block",
|
|
5001
|
+
reason: reason && reason.trim() ? reason : body
|
|
5002
|
+
})
|
|
5003
|
+
);
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
emit(body, client);
|
|
5007
|
+
}
|
|
4993
5008
|
|
|
4994
5009
|
// src/hooks/runtime/config.ts
|
|
4995
5010
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -5469,7 +5484,7 @@ function readPackageMeta(opts = {}) {
|
|
|
5469
5484
|
}
|
|
5470
5485
|
}
|
|
5471
5486
|
var pkg = readPackageMeta({
|
|
5472
|
-
bakedVersion: true ? "1.
|
|
5487
|
+
bakedVersion: true ? "1.3.0" : void 0
|
|
5473
5488
|
});
|
|
5474
5489
|
|
|
5475
5490
|
// src/hooks/runtime/session-start.ts
|
|
@@ -5579,17 +5594,29 @@ var COUNTED_TOOLS = /* @__PURE__ */ new Set([
|
|
|
5579
5594
|
"apply_patch",
|
|
5580
5595
|
"pre_write_code"
|
|
5581
5596
|
]);
|
|
5597
|
+
var REVIEW_SENTINEL = "CybeDefend gap analysis (Stop hook";
|
|
5598
|
+
function transcriptHasReviewSentinel(transcript) {
|
|
5599
|
+
try {
|
|
5600
|
+
return JSON.stringify(transcript).includes(REVIEW_SENTINEL);
|
|
5601
|
+
} catch {
|
|
5602
|
+
return false;
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5582
5605
|
async function sessionReviewHook(deps = {}) {
|
|
5583
5606
|
const readStdin = deps.readStdin ?? readStdinJson;
|
|
5584
5607
|
const loadConfig = deps.loadConfig ?? loadRuntimeConfig;
|
|
5585
|
-
const emitFn = deps.emitFn ??
|
|
5608
|
+
const emitFn = deps.emitFn ?? emitStop;
|
|
5586
5609
|
const readTranscriptFile = deps.readTranscriptFile ?? defaultReadTranscript;
|
|
5587
5610
|
const stdin = await readStdin();
|
|
5588
5611
|
const event = sniff(stdin);
|
|
5612
|
+
if (stdin.stop_hook_active === true || stdin.stop_hook_active === "true") {
|
|
5613
|
+
return;
|
|
5614
|
+
}
|
|
5589
5615
|
const config = loadConfig();
|
|
5590
5616
|
if (!config) return;
|
|
5591
5617
|
if (!config.hooks.enableSessionReview) return;
|
|
5592
5618
|
const transcript = loadTranscript(stdin, readTranscriptFile);
|
|
5619
|
+
if (transcriptHasReviewSentinel(transcript)) return;
|
|
5593
5620
|
let editCount = countEdits(transcript);
|
|
5594
5621
|
if (process.env.CYBEDEFEND_DRY_RUN === "1") {
|
|
5595
5622
|
editCount = parseInt(process.env.CYBEDEFEND_DRY_RUN_EDITS ?? "5", 10);
|
|
@@ -5597,13 +5624,18 @@ async function sessionReviewHook(deps = {}) {
|
|
|
5597
5624
|
if (editCount === 0) return;
|
|
5598
5625
|
if (editCount < config.hooks.reviewThreshold) return;
|
|
5599
5626
|
const userPrompt = extractFirstUserPrompt(transcript);
|
|
5600
|
-
|
|
5627
|
+
let body = composeReviewBody(
|
|
5601
5628
|
editCount,
|
|
5602
5629
|
event.client,
|
|
5603
5630
|
userPrompt,
|
|
5604
5631
|
config.hooks.autoProposeMode
|
|
5605
5632
|
);
|
|
5606
|
-
|
|
5633
|
+
const withDiffScan = config.hooks.enableDiffScan !== false;
|
|
5634
|
+
if (withDiffScan) {
|
|
5635
|
+
const cwd = typeof stdin.cwd === "string" && stdin.cwd ? stdin.cwd : process.cwd();
|
|
5636
|
+
body += "\n" + composeDiffScanBody(extractChangedFiles(transcript, void 0, cwd));
|
|
5637
|
+
}
|
|
5638
|
+
emitFn(body, event.client, composeStopReason(withDiffScan));
|
|
5607
5639
|
}
|
|
5608
5640
|
function loadTranscript(stdin, readFile) {
|
|
5609
5641
|
if (Array.isArray(stdin.transcript)) return stdin.transcript;
|
|
@@ -5666,6 +5698,53 @@ function countEdits(transcript) {
|
|
|
5666
5698
|
}
|
|
5667
5699
|
return n;
|
|
5668
5700
|
}
|
|
5701
|
+
var CHANGED_FILES_MAX = 300;
|
|
5702
|
+
var CHANGED_FILES_DISPLAY = 50;
|
|
5703
|
+
function toolInputOf(obj) {
|
|
5704
|
+
const input = obj.input ?? obj.tool_input;
|
|
5705
|
+
return input && typeof input === "object" ? input : {};
|
|
5706
|
+
}
|
|
5707
|
+
function filePathOfToolUse(obj) {
|
|
5708
|
+
const input = toolInputOf(obj);
|
|
5709
|
+
if (typeof input.file_path === "string" && input.file_path) {
|
|
5710
|
+
return input.file_path;
|
|
5711
|
+
}
|
|
5712
|
+
if (typeof input.path === "string" && input.path) return input.path;
|
|
5713
|
+
if (typeof input.patch === "string" && input.patch) {
|
|
5714
|
+
return filePathFromPatch(input.patch);
|
|
5715
|
+
}
|
|
5716
|
+
return "";
|
|
5717
|
+
}
|
|
5718
|
+
function collectChangedFilesDeep(node, depth, out) {
|
|
5719
|
+
if (depth > 8 || node === null || typeof node !== "object") return;
|
|
5720
|
+
if (Array.isArray(node)) {
|
|
5721
|
+
for (const item of node) collectChangedFilesDeep(item, depth + 1, out);
|
|
5722
|
+
return;
|
|
5723
|
+
}
|
|
5724
|
+
const obj = node;
|
|
5725
|
+
if (COUNTED_TOOLS.has(toolNameOf(obj))) {
|
|
5726
|
+
const p = filePathOfToolUse(obj);
|
|
5727
|
+
if (p) out.add(p);
|
|
5728
|
+
return;
|
|
5729
|
+
}
|
|
5730
|
+
for (const value of Object.values(obj)) {
|
|
5731
|
+
if (value && typeof value === "object") {
|
|
5732
|
+
collectChangedFilesDeep(value, depth + 1, out);
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
function relativizePath(p, cwd) {
|
|
5737
|
+
if (cwd && p.startsWith(cwd.endsWith("/") ? cwd : cwd + "/")) {
|
|
5738
|
+
return p.slice(cwd.endsWith("/") ? cwd.length : cwd.length + 1);
|
|
5739
|
+
}
|
|
5740
|
+
return p;
|
|
5741
|
+
}
|
|
5742
|
+
function extractChangedFiles(transcript, cap = CHANGED_FILES_MAX, cwd) {
|
|
5743
|
+
const out = /* @__PURE__ */ new Set();
|
|
5744
|
+
for (const event of transcript) collectChangedFilesDeep(event, 0, out);
|
|
5745
|
+
const arr = cwd ? Array.from(out).map((p) => relativizePath(p, cwd)) : Array.from(out);
|
|
5746
|
+
return arr.slice(0, cap);
|
|
5747
|
+
}
|
|
5669
5748
|
var COMMAND_WRAPPER_PREFIXES = [
|
|
5670
5749
|
"<local-command-",
|
|
5671
5750
|
"<command-name>",
|
|
@@ -5719,43 +5798,96 @@ function extractFirstUserPrompt(transcript) {
|
|
|
5719
5798
|
function composeReviewBody(editCount, client, userPrompt, autoPropose) {
|
|
5720
5799
|
const safePrompt = userPrompt || "(could not extract \u2014 re-read transcript)";
|
|
5721
5800
|
const proposalLine = autoPropose ? [
|
|
5722
|
-
" **Auto-propose mode is ON** (configured at install).
|
|
5723
|
-
"
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
"
|
|
5801
|
+
" **Auto-propose mode is ON** (configured at install). If \u2014 and ONLY",
|
|
5802
|
+
" if \u2014 one gap clears the bar above, call WITHOUT asking the user:",
|
|
5803
|
+
" `cybe_rules_report_missing(projectId, { rule_text, evidence_files,",
|
|
5804
|
+
' rationale: "<quote user prompt>", confirmed_by_user: true })` for',
|
|
5805
|
+
" that single rule. The user validates it at the next session start",
|
|
5806
|
+
" via the Accept/Reject picker. If nothing clears the bar, propose",
|
|
5807
|
+
" nothing."
|
|
5727
5808
|
].join("\n") : [
|
|
5728
5809
|
" Ask the user via AskUserQuestion:",
|
|
5729
|
-
' > "Your prompt
|
|
5730
|
-
' > Propose
|
|
5731
|
-
' ONLY on explicit "yes": call `cybe_rules_report_missing(projectId,',
|
|
5810
|
+
' > "Your prompt implied <concern>, but no business rule covers it.',
|
|
5811
|
+
' > Propose it as a standing rule for the project?" \u2014 Yes / No / Edit.',
|
|
5812
|
+
' ONLY on an explicit "yes": call `cybe_rules_report_missing(projectId,',
|
|
5732
5813
|
' { rule_text, evidence_files, rationale: "<quote user prompt>",',
|
|
5733
5814
|
" confirmed_by_user: true })`."
|
|
5734
5815
|
].join("\n");
|
|
5735
5816
|
return [
|
|
5736
5817
|
`## \u{1F6E1}\uFE0F CybeDefend gap analysis (Stop hook \u2014 ${editCount} edits this session, client=${client})`,
|
|
5737
5818
|
"",
|
|
5738
|
-
"
|
|
5739
|
-
"business
|
|
5819
|
+
"Check whether this session revealed a MISSING standing rule \u2014 a durable,",
|
|
5820
|
+
"reusable security/business invariant the project should enforce on FUTURE",
|
|
5821
|
+
"changes. Proposing is the exception, not a reflex: **most sessions \u2014",
|
|
5822
|
+
"especially small edits \u2014 end with no proposal, and that is the expected",
|
|
5823
|
+
"outcome.**",
|
|
5740
5824
|
"",
|
|
5741
5825
|
"**Original user prompt (first 600 chars):**",
|
|
5742
5826
|
`> ${safePrompt}`,
|
|
5743
5827
|
"",
|
|
5744
|
-
"###
|
|
5745
|
-
"1.
|
|
5746
|
-
"2. List CybeDefend rules returned during
|
|
5747
|
-
"3.
|
|
5748
|
-
"
|
|
5749
|
-
"
|
|
5750
|
-
"
|
|
5751
|
-
"
|
|
5828
|
+
"### How to decide",
|
|
5829
|
+
"1. List the security/business concerns the prompt mentions or implies.",
|
|
5830
|
+
"2. List the CybeDefend rules that were returned during this session.",
|
|
5831
|
+
"3. Find concerns with NO matching rule, then propose **at most 1** \u2014 the",
|
|
5832
|
+
" single most valuable \u2014 and ONLY if it is a durable, reusable invariant:",
|
|
5833
|
+
" it must apply to future changes (not a one-off), not be stylistic, and",
|
|
5834
|
+
" not already be covered. Over-proposing causes proposal fatigue and",
|
|
5835
|
+
" users end up rejecting everything \u2014 when in doubt, propose nothing.",
|
|
5836
|
+
"4. If (and only if) one gap clears that bar:",
|
|
5752
5837
|
proposalLine,
|
|
5753
|
-
"5.
|
|
5754
|
-
' "\u{1F6E1}\uFE0F CybeDefend gap analysis:
|
|
5755
|
-
'
|
|
5838
|
+
"5. Otherwise \u2014 the normal case \u2014 propose nothing and say so:",
|
|
5839
|
+
' "\u{1F6E1}\uFE0F CybeDefend gap analysis: nothing this session warrants a new',
|
|
5840
|
+
' standing rule. No proposals."',
|
|
5841
|
+
""
|
|
5842
|
+
].join("\n");
|
|
5843
|
+
}
|
|
5844
|
+
function composeDiffScanBody(changedFiles) {
|
|
5845
|
+
const total = changedFiles.length;
|
|
5846
|
+
const shown = changedFiles.slice(0, CHANGED_FILES_DISPLAY);
|
|
5847
|
+
const heading = total > CHANGED_FILES_DISPLAY ? `**Changed files this session (showing first ${CHANGED_FILES_DISPLAY} of ${total}):**` : `**Changed files this session (${total}):**`;
|
|
5848
|
+
const fileLines = total > 0 ? [
|
|
5849
|
+
heading,
|
|
5850
|
+
...shown.map((f) => `- \`${f}\``),
|
|
5851
|
+
"(If you changed other files not listed above, include them too.)"
|
|
5852
|
+
] : [
|
|
5853
|
+
"Use the repo-relative paths of every file you created or edited this",
|
|
5854
|
+
"session as the `paths`."
|
|
5855
|
+
];
|
|
5856
|
+
return [
|
|
5857
|
+
"## \u{1F50D} CybeDefend pre-commit diff scan",
|
|
5858
|
+
"",
|
|
5859
|
+
"Before the user commits, run a best-effort security scan (SAST / IaC /",
|
|
5860
|
+
"secrets) limited to the files you changed this session.",
|
|
5861
|
+
"",
|
|
5862
|
+
...fileLines,
|
|
5863
|
+
"",
|
|
5864
|
+
"### Required next-turn action",
|
|
5865
|
+
"1. Call `cybe_scan_diff(projectId, { paths })` with those repo-relative",
|
|
5866
|
+
" PATHS (paths only \u2014 NOT the file contents). It returns",
|
|
5867
|
+
" `{ scanId, uploadUrl, expiresAt }`.",
|
|
5868
|
+
"2. Build a ZIP of those exact files (full current content, repo-relative",
|
|
5869
|
+
" entry names \u2014 e.g. `zip -q diff.zip <paths>`) and upload it with an HTTP",
|
|
5870
|
+
" PUT to `uploadUrl` (header `Content-Type: application/zip`) BEFORE",
|
|
5871
|
+
' `expiresAt` (e.g. `curl -X PUT -H "Content-Type: application/zip"',
|
|
5872
|
+
' --data-binary @diff.zip "<uploadUrl>"`). The contents go straight to the',
|
|
5873
|
+
" bucket, never through the MCP tool.",
|
|
5874
|
+
"3. Poll `cybe_scan_status(projectId, scanId)` until it is completed, then",
|
|
5875
|
+
" read `cybe_scan_findings(projectId, scanId)`.",
|
|
5876
|
+
"4. Fix or justify every finding. For a confident false positive, add a",
|
|
5877
|
+
" `// cybedefend-ignore: <reason>` comment on the offending line so it",
|
|
5878
|
+
" stays silenced on the full scan too.",
|
|
5879
|
+
"",
|
|
5880
|
+
"This diff scan is best-effort \u2014 it COMPLEMENTS, never replaces, the full",
|
|
5881
|
+
"scan (secrets are head-only; cross-file SAST context may be missing). Caps:",
|
|
5882
|
+
"300 files / 1 MB per file / 3 MB total \u2014 over the cap the scan fails, so run",
|
|
5883
|
+
"a full scan instead.",
|
|
5756
5884
|
""
|
|
5757
5885
|
].join("\n");
|
|
5758
5886
|
}
|
|
5887
|
+
function composeStopReason(withDiffScan) {
|
|
5888
|
+
const scan = withDiffScan ? ", then run a pre-commit `cybe_scan_diff` on the files you changed (poll `cybe_scan_status`, read `cybe_scan_findings`) and fix or justify each finding" : "";
|
|
5889
|
+
return "\u{1F6E1}\uFE0F CybeDefend \u2014 before wrapping up, run the end-of-session review: gap-analyse the request against this session's business rules and, for any uncovered security/data concern, offer to propose a rule via AskUserQuestion (`cybe_rules_report_missing` only on an explicit yes)" + scan + ".";
|
|
5890
|
+
}
|
|
5759
5891
|
|
|
5760
5892
|
// src/hooks/runtime/pre-compact.ts
|
|
5761
5893
|
async function preCompactHook(deps = {}) {
|
|
@@ -6382,6 +6514,31 @@ var PATTERNS = [
|
|
|
6382
6514
|
re: /AKIA[0-9A-Z]{16}/g,
|
|
6383
6515
|
captureIndex: null
|
|
6384
6516
|
},
|
|
6517
|
+
// 4b. Anthropic API key (matched before the generic sk- pattern below)
|
|
6518
|
+
{
|
|
6519
|
+
re: /sk-ant-[0-9A-Za-z_-]{16,}/g,
|
|
6520
|
+
captureIndex: null
|
|
6521
|
+
},
|
|
6522
|
+
// 4c. OpenAI API key
|
|
6523
|
+
{
|
|
6524
|
+
re: /sk-[0-9A-Za-z]{20,}/g,
|
|
6525
|
+
captureIndex: null
|
|
6526
|
+
},
|
|
6527
|
+
// 4d. Google API key
|
|
6528
|
+
{
|
|
6529
|
+
re: /AIza[0-9A-Za-z_-]{35,}/g,
|
|
6530
|
+
captureIndex: null
|
|
6531
|
+
},
|
|
6532
|
+
// 4e. Slack token (bot/user/app/refresh/legacy)
|
|
6533
|
+
{
|
|
6534
|
+
re: /xox[baprs]-[0-9A-Za-z-]{10,}/g,
|
|
6535
|
+
captureIndex: null
|
|
6536
|
+
},
|
|
6537
|
+
// 4f. Credentials embedded in a URL: scheme://user:pass@host — redact user:pass
|
|
6538
|
+
{
|
|
6539
|
+
re: /(?<=:\/\/)[^/\s:@]+:[^/\s:@]+(?=@)/g,
|
|
6540
|
+
captureIndex: null
|
|
6541
|
+
},
|
|
6385
6542
|
// 5. Bearer token — preserve the "Bearer " keyword, replace only the token.
|
|
6386
6543
|
// Group 1 = the prefix ("Bearer "), group 2 = the token value.
|
|
6387
6544
|
{
|
|
@@ -6439,6 +6596,280 @@ function hashTarget(text) {
|
|
|
6439
6596
|
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
6440
6597
|
}
|
|
6441
6598
|
|
|
6599
|
+
// src/hooks/runtime/near-miss.ts
|
|
6600
|
+
var RE_CLASS_ESCAPES = /* @__PURE__ */ new Set([
|
|
6601
|
+
"s",
|
|
6602
|
+
"S",
|
|
6603
|
+
"d",
|
|
6604
|
+
"D",
|
|
6605
|
+
"w",
|
|
6606
|
+
"W",
|
|
6607
|
+
"b",
|
|
6608
|
+
"B",
|
|
6609
|
+
"n",
|
|
6610
|
+
"r",
|
|
6611
|
+
"t",
|
|
6612
|
+
"f",
|
|
6613
|
+
"v",
|
|
6614
|
+
"A",
|
|
6615
|
+
"Z",
|
|
6616
|
+
"z",
|
|
6617
|
+
"G"
|
|
6618
|
+
]);
|
|
6619
|
+
var TOKEN_CHARS = /[A-Za-z0-9_\-/=:.@]/;
|
|
6620
|
+
function dedupe(xs) {
|
|
6621
|
+
return [...new Set(xs.filter((x) => x.length > 0))];
|
|
6622
|
+
}
|
|
6623
|
+
function basename(p) {
|
|
6624
|
+
const parts = p.split("/");
|
|
6625
|
+
return parts[parts.length - 1] ?? p;
|
|
6626
|
+
}
|
|
6627
|
+
function expandTilde(p, homeDir) {
|
|
6628
|
+
if (p === "~") return homeDir;
|
|
6629
|
+
if (p.startsWith("~/")) return `${homeDir}/${p.slice(2)}`;
|
|
6630
|
+
return p;
|
|
6631
|
+
}
|
|
6632
|
+
function extractCommandAnchors(src) {
|
|
6633
|
+
const tokens = [];
|
|
6634
|
+
const opLiterals = [];
|
|
6635
|
+
let buf = "";
|
|
6636
|
+
const flush = () => {
|
|
6637
|
+
if (buf) tokens.push(buf);
|
|
6638
|
+
buf = "";
|
|
6639
|
+
};
|
|
6640
|
+
for (let i = 0; i < src.length; i++) {
|
|
6641
|
+
const c = src[i];
|
|
6642
|
+
if (c === "\\") {
|
|
6643
|
+
const nxt = src[i + 1] ?? "";
|
|
6644
|
+
i++;
|
|
6645
|
+
if ("|><&".includes(nxt)) {
|
|
6646
|
+
flush();
|
|
6647
|
+
opLiterals.push(nxt);
|
|
6648
|
+
} else if (RE_CLASS_ESCAPES.has(nxt)) {
|
|
6649
|
+
flush();
|
|
6650
|
+
} else {
|
|
6651
|
+
buf += nxt;
|
|
6652
|
+
}
|
|
6653
|
+
continue;
|
|
6654
|
+
}
|
|
6655
|
+
if (c === "[") {
|
|
6656
|
+
flush();
|
|
6657
|
+
while (i < src.length && src[i] !== "]") i++;
|
|
6658
|
+
continue;
|
|
6659
|
+
}
|
|
6660
|
+
if (c === ".") {
|
|
6661
|
+
flush();
|
|
6662
|
+
continue;
|
|
6663
|
+
}
|
|
6664
|
+
if (TOKEN_CHARS.test(c)) {
|
|
6665
|
+
buf += c;
|
|
6666
|
+
continue;
|
|
6667
|
+
}
|
|
6668
|
+
flush();
|
|
6669
|
+
}
|
|
6670
|
+
flush();
|
|
6671
|
+
const commands = [];
|
|
6672
|
+
const flags = [];
|
|
6673
|
+
const literals = [];
|
|
6674
|
+
for (const raw of tokens) {
|
|
6675
|
+
const t = raw;
|
|
6676
|
+
if (/^--?[A-Za-z][A-Za-z0-9-]*$/.test(t)) {
|
|
6677
|
+
flags.push(t);
|
|
6678
|
+
} else if (/[/=:@]/.test(t) || /\d{1,3}(\.\d{1,3}){2,}/.test(t)) {
|
|
6679
|
+
literals.push(t);
|
|
6680
|
+
} else if (/^[A-Za-z][A-Za-z0-9_-]{1,}$/.test(t)) {
|
|
6681
|
+
commands.push(t);
|
|
6682
|
+
} else if (t.length >= 3) {
|
|
6683
|
+
literals.push(t);
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
const subLiterals = [];
|
|
6687
|
+
for (const lit of literals) {
|
|
6688
|
+
const m = lit.match(/\/[A-Za-z]{2,}\//g);
|
|
6689
|
+
if (m) subLiterals.push(...m);
|
|
6690
|
+
}
|
|
6691
|
+
return {
|
|
6692
|
+
commands: dedupe(commands),
|
|
6693
|
+
flags: dedupe(flags),
|
|
6694
|
+
literals: dedupe([...literals, ...subLiterals, ...opLiterals])
|
|
6695
|
+
};
|
|
6696
|
+
}
|
|
6697
|
+
var GENERIC_SEGMENTS = /* @__PURE__ */ new Set([
|
|
6698
|
+
"config",
|
|
6699
|
+
"conf",
|
|
6700
|
+
"configs",
|
|
6701
|
+
"settings",
|
|
6702
|
+
"data",
|
|
6703
|
+
"home",
|
|
6704
|
+
"user",
|
|
6705
|
+
"users",
|
|
6706
|
+
"usr",
|
|
6707
|
+
"var",
|
|
6708
|
+
"tmp",
|
|
6709
|
+
"opt",
|
|
6710
|
+
"etc",
|
|
6711
|
+
"app",
|
|
6712
|
+
"apps",
|
|
6713
|
+
"src",
|
|
6714
|
+
"lib",
|
|
6715
|
+
"bin",
|
|
6716
|
+
"dist",
|
|
6717
|
+
"build",
|
|
6718
|
+
"out",
|
|
6719
|
+
"target",
|
|
6720
|
+
"public",
|
|
6721
|
+
"static",
|
|
6722
|
+
"assets",
|
|
6723
|
+
"main",
|
|
6724
|
+
"index",
|
|
6725
|
+
"test",
|
|
6726
|
+
"tests",
|
|
6727
|
+
"docs",
|
|
6728
|
+
"node_modules",
|
|
6729
|
+
"vendor",
|
|
6730
|
+
"project"
|
|
6731
|
+
]);
|
|
6732
|
+
function extractGlobAnchors(glob) {
|
|
6733
|
+
const segs = glob.split("/");
|
|
6734
|
+
const literals = [];
|
|
6735
|
+
let ext = null;
|
|
6736
|
+
for (const seg of segs) {
|
|
6737
|
+
if (!seg || seg === "**" || seg === "*") continue;
|
|
6738
|
+
const bare = seg.replace(/[*?]/g, "");
|
|
6739
|
+
if (!bare) continue;
|
|
6740
|
+
if (bare.startsWith(".") && !bare.slice(1).includes(".")) {
|
|
6741
|
+
literals.push(bare);
|
|
6742
|
+
if (bare.length <= 6) ext = bare;
|
|
6743
|
+
} else if (bare.includes(".")) {
|
|
6744
|
+
literals.push(bare);
|
|
6745
|
+
} else if (bare.length >= 3 && !GENERIC_SEGMENTS.has(bare.toLowerCase())) {
|
|
6746
|
+
literals.push(bare);
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
return { ext, literals: dedupe(literals) };
|
|
6750
|
+
}
|
|
6751
|
+
function nearMissCommand(target, rule) {
|
|
6752
|
+
if (!rule.commandRegex) return false;
|
|
6753
|
+
const { commands, flags, literals } = extractCommandAnchors(
|
|
6754
|
+
rule.commandRegex
|
|
6755
|
+
);
|
|
6756
|
+
if (commands.length === 0 && flags.length === 0 && literals.length === 0)
|
|
6757
|
+
return false;
|
|
6758
|
+
const cmd = target.command;
|
|
6759
|
+
const base = basename(cmd);
|
|
6760
|
+
const joined = [cmd, ...target.argv].join(" ");
|
|
6761
|
+
const flagHits = (fl) => {
|
|
6762
|
+
if (fl.startsWith("--")) return joined.includes(fl);
|
|
6763
|
+
const letters = fl.replace(/^-+/, "").split("");
|
|
6764
|
+
if (letters.length === 0) return false;
|
|
6765
|
+
return target.argv.some(
|
|
6766
|
+
(t) => t.startsWith("-") && letters.every((ch) => t.includes(ch))
|
|
6767
|
+
);
|
|
6768
|
+
};
|
|
6769
|
+
const cmdHitCount = commands.filter(
|
|
6770
|
+
(a) => cmd === a || base === a || target.argv.includes(a)
|
|
6771
|
+
).length;
|
|
6772
|
+
const flagHitCount = flags.filter(flagHits).length;
|
|
6773
|
+
const litHitCount = literals.filter((l) => joined.includes(l)).length;
|
|
6774
|
+
const core = cmdHitCount > 0 || litHitCount > 0;
|
|
6775
|
+
const totalAnchors = commands.length + flags.length + literals.length;
|
|
6776
|
+
const signalCount = cmdHitCount + flagHitCount + litHitCount;
|
|
6777
|
+
const threshold = totalAnchors >= 2 ? 2 : 1;
|
|
6778
|
+
return core && signalCount >= threshold;
|
|
6779
|
+
}
|
|
6780
|
+
function nearMissFile(target, rule, homeDir) {
|
|
6781
|
+
const globs = rule.filePathGlobs;
|
|
6782
|
+
if (!globs || globs.length === 0) return false;
|
|
6783
|
+
const path = expandTilde(target.path, homeDir);
|
|
6784
|
+
const base = basename(path);
|
|
6785
|
+
const segs = path.split("/");
|
|
6786
|
+
for (const g of globs) {
|
|
6787
|
+
const { ext, literals } = extractGlobAnchors(g);
|
|
6788
|
+
if (ext && path.endsWith(ext)) return true;
|
|
6789
|
+
for (const l of literals) {
|
|
6790
|
+
if (l.startsWith(".")) {
|
|
6791
|
+
if (base.includes(l) || path.includes(`/${l}`) || path.endsWith(l))
|
|
6792
|
+
return true;
|
|
6793
|
+
} else if (segs.includes(l) || base.includes(l)) {
|
|
6794
|
+
return true;
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
}
|
|
6798
|
+
return false;
|
|
6799
|
+
}
|
|
6800
|
+
function literalsFromGlobList(globs) {
|
|
6801
|
+
const out = [];
|
|
6802
|
+
for (const g of globs) {
|
|
6803
|
+
for (const part of g.split(/[*?]/)) {
|
|
6804
|
+
const p = part.replace(/^[._-]+|[._-]+$/g, "");
|
|
6805
|
+
if (p.length >= 3) out.push(part.length >= 3 ? part : p);
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
return dedupe(out);
|
|
6809
|
+
}
|
|
6810
|
+
function nearMissEnv(target, rule) {
|
|
6811
|
+
const globs = rule.envNameGlobs;
|
|
6812
|
+
if (!globs || globs.length === 0) return false;
|
|
6813
|
+
const name = target.name;
|
|
6814
|
+
for (const g of globs) {
|
|
6815
|
+
const lead = g.startsWith("*");
|
|
6816
|
+
const trail = g.endsWith("*");
|
|
6817
|
+
const core = g.replace(/[*?]/g, "");
|
|
6818
|
+
if (core.length < 3) continue;
|
|
6819
|
+
if (lead && trail) {
|
|
6820
|
+
if (name.includes(core)) return true;
|
|
6821
|
+
} else if (trail) {
|
|
6822
|
+
if (name.startsWith(core)) return true;
|
|
6823
|
+
} else if (lead) {
|
|
6824
|
+
if (name.endsWith(core)) return true;
|
|
6825
|
+
} else if (name === core) {
|
|
6826
|
+
return true;
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
return false;
|
|
6830
|
+
}
|
|
6831
|
+
function nearMissHttp(target, rule) {
|
|
6832
|
+
const globs = rule.httpHostGlobs;
|
|
6833
|
+
if (!globs || globs.length === 0) return false;
|
|
6834
|
+
let hostname;
|
|
6835
|
+
try {
|
|
6836
|
+
hostname = new URL(target.url).hostname;
|
|
6837
|
+
} catch {
|
|
6838
|
+
hostname = target.url;
|
|
6839
|
+
}
|
|
6840
|
+
const literals = literalsFromGlobList(globs);
|
|
6841
|
+
const prefixes = [];
|
|
6842
|
+
for (const l of literals) {
|
|
6843
|
+
const m = l.match(/^(\d{1,3}\.\d{1,3})\.\d/);
|
|
6844
|
+
if (m) prefixes.push(`${m[1]}.`);
|
|
6845
|
+
}
|
|
6846
|
+
return [...literals, ...prefixes].some((l) => hostname.includes(l));
|
|
6847
|
+
}
|
|
6848
|
+
function nearMissGit(target, rule) {
|
|
6849
|
+
if (rule.gitSubcommand && rule.gitSubcommand.length > 0) {
|
|
6850
|
+
return rule.gitSubcommand.includes(target.subcommand);
|
|
6851
|
+
}
|
|
6852
|
+
return false;
|
|
6853
|
+
}
|
|
6854
|
+
function nearMiss(req, rule) {
|
|
6855
|
+
if (rule.toolMatch && rule.toolMatch.length > 0 && !rule.toolMatch.includes(req.tool)) {
|
|
6856
|
+
return false;
|
|
6857
|
+
}
|
|
6858
|
+
const { target, homeDir } = req;
|
|
6859
|
+
switch (target.type) {
|
|
6860
|
+
case "command":
|
|
6861
|
+
return nearMissCommand(target, rule);
|
|
6862
|
+
case "file":
|
|
6863
|
+
return nearMissFile(target, rule, homeDir);
|
|
6864
|
+
case "env":
|
|
6865
|
+
return nearMissEnv(target, rule);
|
|
6866
|
+
case "http":
|
|
6867
|
+
return nearMissHttp(target, rule);
|
|
6868
|
+
case "git":
|
|
6869
|
+
return nearMissGit(target, rule);
|
|
6870
|
+
}
|
|
6871
|
+
}
|
|
6872
|
+
|
|
6442
6873
|
// src/hooks/runtime/guard-rules-cache.ts
|
|
6443
6874
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
|
|
6444
6875
|
import { dirname as dirname3 } from "node:path";
|
|
@@ -6489,7 +6920,9 @@ function snakeToCamelRule(raw) {
|
|
|
6489
6920
|
gitSubcommand: raw["git_subcommand"] ?? raw["gitSubcommand"],
|
|
6490
6921
|
denyMessage: raw["deny_message"] ?? raw["denyMessage"],
|
|
6491
6922
|
warnMessage: raw["warn_message"] ?? raw["warnMessage"],
|
|
6492
|
-
defaultAction: raw["default_action"] ?? raw["defaultAction"]
|
|
6923
|
+
defaultAction: raw["default_action"] ?? raw["defaultAction"],
|
|
6924
|
+
llmJudgeEnabled: raw["llm_judge_enabled"] ?? raw["llmJudgeEnabled"],
|
|
6925
|
+
catchEligible: raw["catch_eligible"] ?? raw["catchEligible"]
|
|
6493
6926
|
};
|
|
6494
6927
|
}
|
|
6495
6928
|
function makeGuardRulesCache(opts) {
|
|
@@ -6500,7 +6933,7 @@ function makeGuardRulesCache(opts) {
|
|
|
6500
6933
|
const entry = readCache(ctx.projectId);
|
|
6501
6934
|
const ageSec = entry ? (Date.now() - entry.fetchedAt) / 1e3 : Infinity;
|
|
6502
6935
|
if (entry && ageSec < staleSec) {
|
|
6503
|
-
return { kind: "rules", rules: entry.rules };
|
|
6936
|
+
return { kind: "rules", rules: entry.rules, llmJudge: entry.llmJudge };
|
|
6504
6937
|
}
|
|
6505
6938
|
try {
|
|
6506
6939
|
const result = await fetchFn({
|
|
@@ -6512,19 +6945,18 @@ function makeGuardRulesCache(opts) {
|
|
|
6512
6945
|
...entry,
|
|
6513
6946
|
fetchedAt: Date.now()
|
|
6514
6947
|
});
|
|
6515
|
-
return { kind: "rules", rules: entry.rules };
|
|
6948
|
+
return { kind: "rules", rules: entry.rules, llmJudge: entry.llmJudge };
|
|
6516
6949
|
}
|
|
6517
|
-
const transformedRules = result.rules.map(
|
|
6518
|
-
(r) => snakeToCamelRule(r)
|
|
6519
|
-
);
|
|
6950
|
+
const transformedRules = result.rules.map((r) => snakeToCamelRule(r));
|
|
6520
6951
|
const fresh = {
|
|
6521
6952
|
projectId: ctx.projectId,
|
|
6522
6953
|
rules: transformedRules,
|
|
6523
6954
|
etag: result.etag,
|
|
6524
|
-
fetchedAt: Date.now()
|
|
6955
|
+
fetchedAt: Date.now(),
|
|
6956
|
+
llmJudge: result.llmJudge ?? null
|
|
6525
6957
|
};
|
|
6526
6958
|
writeCache(fresh);
|
|
6527
|
-
return { kind: "rules", rules: fresh.rules };
|
|
6959
|
+
return { kind: "rules", rules: fresh.rules, llmJudge: fresh.llmJudge };
|
|
6528
6960
|
} catch (e) {
|
|
6529
6961
|
const msg = e instanceof Error ? e.message : String(e);
|
|
6530
6962
|
if (entry?.rules?.length) {
|
|
@@ -6533,7 +6965,7 @@ function makeGuardRulesCache(opts) {
|
|
|
6533
6965
|
`[VibeDefend] Action Guards: rule refresh failed (${msg}). Using cached rules (age=${ageSec2}s).
|
|
6534
6966
|
`
|
|
6535
6967
|
);
|
|
6536
|
-
return { kind: "rules", rules: entry.rules };
|
|
6968
|
+
return { kind: "rules", rules: entry.rules, llmJudge: entry.llmJudge };
|
|
6537
6969
|
}
|
|
6538
6970
|
return {
|
|
6539
6971
|
kind: "unavailable",
|
|
@@ -6593,22 +7025,127 @@ async function fetchEffectiveRulesFromGateway(ctx, opts = {}) {
|
|
|
6593
7025
|
const data = body.data ?? body;
|
|
6594
7026
|
const rules = Array.isArray(data.rules) ? data.rules : [];
|
|
6595
7027
|
const etag = res.headers.get("ETag");
|
|
6596
|
-
|
|
7028
|
+
const llmJudge = parseLlmJudgeHint(data["llm_judge"]);
|
|
7029
|
+
return { rules, etag, notModified: false, llmJudge };
|
|
7030
|
+
}
|
|
7031
|
+
function parseLlmJudgeHint(raw) {
|
|
7032
|
+
if (!raw || typeof raw !== "object") return null;
|
|
7033
|
+
const o = raw;
|
|
7034
|
+
return {
|
|
7035
|
+
enabled: Boolean(o["enabled"]),
|
|
7036
|
+
quotaRemaining: typeof o["quota_remaining"] === "number" ? o["quota_remaining"] : -1,
|
|
7037
|
+
quotaExhausted: Boolean(o["quota_exhausted"])
|
|
7038
|
+
};
|
|
7039
|
+
}
|
|
7040
|
+
|
|
7041
|
+
// src/hooks/runtime/judge.ts
|
|
7042
|
+
var DEFAULT_TIMEOUT_MS3 = 1500;
|
|
7043
|
+
async function callJudge(input, opts = {}) {
|
|
7044
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
7045
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
7046
|
+
const url = `${input.apiBaseResolved}/project/${encodeURIComponent(input.projectId)}/action-guards/judge`;
|
|
7047
|
+
try {
|
|
7048
|
+
const res = await f(url, {
|
|
7049
|
+
method: "POST",
|
|
7050
|
+
headers: {
|
|
7051
|
+
Authorization: `Bearer ${input.token}`,
|
|
7052
|
+
"Content-Type": "application/json"
|
|
7053
|
+
},
|
|
7054
|
+
body: JSON.stringify({
|
|
7055
|
+
tool: input.tool,
|
|
7056
|
+
agent: input.agent,
|
|
7057
|
+
session_id: input.sessionId,
|
|
7058
|
+
phase: input.phase ?? "confirm",
|
|
7059
|
+
action_summary: input.actionSummary,
|
|
7060
|
+
action_hash: input.actionHash,
|
|
7061
|
+
rules: input.rules
|
|
7062
|
+
}),
|
|
7063
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7064
|
+
});
|
|
7065
|
+
if (!res.ok) return null;
|
|
7066
|
+
const body = await res.json();
|
|
7067
|
+
const data = body.data ?? body;
|
|
7068
|
+
const decision = data["decision"];
|
|
7069
|
+
if (decision !== "block" && decision !== "allow") return null;
|
|
7070
|
+
return {
|
|
7071
|
+
decision,
|
|
7072
|
+
reason: typeof data["reason"] === "string" ? data["reason"] : "",
|
|
7073
|
+
model: typeof data["model"] === "string" ? data["model"] : "",
|
|
7074
|
+
quotaRemaining: typeof data["quota_remaining"] === "number" ? data["quota_remaining"] : -1,
|
|
7075
|
+
quotaExceeded: Boolean(data["quota_exceeded"])
|
|
7076
|
+
};
|
|
7077
|
+
} catch {
|
|
7078
|
+
return null;
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
|
|
7082
|
+
// src/hooks/runtime/judge-cache.ts
|
|
7083
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync5 } from "node:fs";
|
|
7084
|
+
import { dirname as dirname4, join as join8 } from "node:path";
|
|
7085
|
+
import { homedir as homedir6 } from "node:os";
|
|
7086
|
+
var DEFAULT_JUDGE_CACHE_FILE = join8(
|
|
7087
|
+
homedir6(),
|
|
7088
|
+
".cybedefend",
|
|
7089
|
+
"guards-judge-cache.json"
|
|
7090
|
+
);
|
|
7091
|
+
var DEFAULT_JUDGE_TTL_MS = 12e4;
|
|
7092
|
+
function makeJudgeCache(opts = {}) {
|
|
7093
|
+
const cacheFile = opts.cacheFile ?? DEFAULT_JUDGE_CACHE_FILE;
|
|
7094
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_JUDGE_TTL_MS;
|
|
7095
|
+
const now = opts.now ?? (() => Date.now());
|
|
7096
|
+
function readAll() {
|
|
7097
|
+
if (!existsSync10(cacheFile)) return {};
|
|
7098
|
+
try {
|
|
7099
|
+
return JSON.parse(readFileSync10(cacheFile, "utf8"));
|
|
7100
|
+
} catch {
|
|
7101
|
+
return {};
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
function get(key) {
|
|
7105
|
+
const entry = readAll()[key];
|
|
7106
|
+
if (!entry) return null;
|
|
7107
|
+
if (now() - entry.at > ttlMs) return null;
|
|
7108
|
+
return entry.res;
|
|
7109
|
+
}
|
|
7110
|
+
function set(key, res) {
|
|
7111
|
+
try {
|
|
7112
|
+
const all = readAll();
|
|
7113
|
+
const t = now();
|
|
7114
|
+
for (const k of Object.keys(all)) {
|
|
7115
|
+
if (t - all[k].at > ttlMs) delete all[k];
|
|
7116
|
+
}
|
|
7117
|
+
all[key] = { res, at: t };
|
|
7118
|
+
ensureDirFor3(cacheFile);
|
|
7119
|
+
writeFileSync6(cacheFile, JSON.stringify(all), "utf8");
|
|
7120
|
+
} catch {
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
return { get, set };
|
|
7124
|
+
}
|
|
7125
|
+
function ensureDirFor3(filePath) {
|
|
7126
|
+
const dir = dirname4(filePath);
|
|
7127
|
+
if (!existsSync10(dir)) {
|
|
7128
|
+
mkdirSync5(dir, { recursive: true });
|
|
7129
|
+
}
|
|
6597
7130
|
}
|
|
6598
7131
|
|
|
7132
|
+
// src/hooks/runtime/guard-check.ts
|
|
7133
|
+
import { homedir as homedir8 } from "node:os";
|
|
7134
|
+
import { join as join10 } from "node:path";
|
|
7135
|
+
|
|
6599
7136
|
// src/hooks/runtime/guard-violations-buffer.ts
|
|
6600
7137
|
import {
|
|
6601
7138
|
appendFileSync,
|
|
6602
|
-
existsSync as
|
|
6603
|
-
readFileSync as
|
|
6604
|
-
writeFileSync as
|
|
6605
|
-
mkdirSync as
|
|
7139
|
+
existsSync as existsSync11,
|
|
7140
|
+
readFileSync as readFileSync11,
|
|
7141
|
+
writeFileSync as writeFileSync7,
|
|
7142
|
+
mkdirSync as mkdirSync6
|
|
6606
7143
|
} from "node:fs";
|
|
6607
|
-
import { dirname as
|
|
6608
|
-
import { homedir as
|
|
6609
|
-
import { join as
|
|
6610
|
-
var DEFAULT_BUFFER_FILE =
|
|
6611
|
-
|
|
7144
|
+
import { dirname as dirname5 } from "node:path";
|
|
7145
|
+
import { homedir as homedir7 } from "node:os";
|
|
7146
|
+
import { join as join9 } from "node:path";
|
|
7147
|
+
var DEFAULT_BUFFER_FILE = join9(
|
|
7148
|
+
homedir7(),
|
|
6612
7149
|
".cybedefend",
|
|
6613
7150
|
"guards-violations-buffer.jsonl"
|
|
6614
7151
|
);
|
|
@@ -6621,16 +7158,16 @@ function makeViolationsBuffer(opts = {}) {
|
|
|
6621
7158
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6622
7159
|
};
|
|
6623
7160
|
try {
|
|
6624
|
-
|
|
7161
|
+
ensureDirFor4(bufferFile);
|
|
6625
7162
|
appendFileSync(bufferFile, JSON.stringify(record) + "\n", "utf8");
|
|
6626
7163
|
} catch {
|
|
6627
7164
|
}
|
|
6628
7165
|
}
|
|
6629
7166
|
async function flushViolations2(ctx, postFn) {
|
|
6630
|
-
if (!
|
|
7167
|
+
if (!existsSync11(bufferFile)) return;
|
|
6631
7168
|
let content;
|
|
6632
7169
|
try {
|
|
6633
|
-
content =
|
|
7170
|
+
content = readFileSync11(bufferFile, "utf8").trim();
|
|
6634
7171
|
} catch {
|
|
6635
7172
|
return;
|
|
6636
7173
|
}
|
|
@@ -6649,7 +7186,7 @@ function makeViolationsBuffer(opts = {}) {
|
|
|
6649
7186
|
}
|
|
6650
7187
|
if (records.length === 0) {
|
|
6651
7188
|
try {
|
|
6652
|
-
|
|
7189
|
+
writeFileSync7(bufferFile, "", "utf8");
|
|
6653
7190
|
} catch {
|
|
6654
7191
|
}
|
|
6655
7192
|
return;
|
|
@@ -6679,20 +7216,20 @@ function makeViolationsBuffer(opts = {}) {
|
|
|
6679
7216
|
}
|
|
6680
7217
|
try {
|
|
6681
7218
|
if (failed.length === 0) {
|
|
6682
|
-
|
|
7219
|
+
writeFileSync7(bufferFile, "", "utf8");
|
|
6683
7220
|
} else {
|
|
6684
7221
|
const serialised = failed.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
6685
|
-
|
|
7222
|
+
writeFileSync7(bufferFile, serialised, "utf8");
|
|
6686
7223
|
}
|
|
6687
7224
|
} catch {
|
|
6688
7225
|
}
|
|
6689
7226
|
}
|
|
6690
7227
|
return { bufferViolation: bufferViolation2, flushViolations: flushViolations2 };
|
|
6691
7228
|
}
|
|
6692
|
-
function
|
|
6693
|
-
const dir =
|
|
6694
|
-
if (!
|
|
6695
|
-
|
|
7229
|
+
function ensureDirFor4(filePath) {
|
|
7230
|
+
const dir = dirname5(filePath);
|
|
7231
|
+
if (!existsSync11(dir)) {
|
|
7232
|
+
mkdirSync6(dir, { recursive: true });
|
|
6696
7233
|
}
|
|
6697
7234
|
}
|
|
6698
7235
|
var _defaultBuffer = makeViolationsBuffer();
|
|
@@ -6718,11 +7255,11 @@ function appendDebugLog(entry) {
|
|
|
6718
7255
|
try {
|
|
6719
7256
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6720
7257
|
if (!homeDir) return;
|
|
6721
|
-
const { mkdirSync:
|
|
6722
|
-
const { join:
|
|
6723
|
-
const dir =
|
|
6724
|
-
|
|
6725
|
-
appendFileSync2(
|
|
7258
|
+
const { mkdirSync: mkdirSync7, appendFileSync: appendFileSync2 } = __require("node:fs");
|
|
7259
|
+
const { join: join11 } = __require("node:path");
|
|
7260
|
+
const dir = join11(homeDir, ".cybedefend");
|
|
7261
|
+
mkdirSync7(dir, { recursive: true });
|
|
7262
|
+
appendFileSync2(join11(dir, "hook-debug.log"), entry + "\n", "utf8");
|
|
6726
7263
|
} catch {
|
|
6727
7264
|
}
|
|
6728
7265
|
}
|
|
@@ -6893,36 +7430,171 @@ function stringifyTarget(t) {
|
|
|
6893
7430
|
return `git ${t.subcommand}`;
|
|
6894
7431
|
}
|
|
6895
7432
|
}
|
|
7433
|
+
async function runJudgeStep(args) {
|
|
7434
|
+
const {
|
|
7435
|
+
judge,
|
|
7436
|
+
llmJudgeHint,
|
|
7437
|
+
rules,
|
|
7438
|
+
decision,
|
|
7439
|
+
projectContext,
|
|
7440
|
+
request,
|
|
7441
|
+
sessionId
|
|
7442
|
+
} = args;
|
|
7443
|
+
const matched = decision.matchedRuleIds.map((id) => rules.find((r) => r.id === id)).filter((r) => r !== void 0);
|
|
7444
|
+
const llmRules = matched.filter(
|
|
7445
|
+
(r) => r.action === "deny" && r.llmJudgeEnabled
|
|
7446
|
+
);
|
|
7447
|
+
if (llmRules.length === 0) return void 0;
|
|
7448
|
+
if (decision.maxSeverity === "critical") return void 0;
|
|
7449
|
+
if (llmJudgeHint?.enabled === false) return void 0;
|
|
7450
|
+
const targetStr = stringifyTarget(request.target);
|
|
7451
|
+
const actionHash = hashTarget(targetStr);
|
|
7452
|
+
const ruleIds = llmRules.map((r) => r.id).sort().join(",");
|
|
7453
|
+
const cacheKey = `${actionHash}|${ruleIds}|confirm`;
|
|
7454
|
+
let res = judge.cacheGet(cacheKey);
|
|
7455
|
+
if (!res) {
|
|
7456
|
+
res = await judge.callJudge({
|
|
7457
|
+
apiBaseResolved: projectContext.apiBaseResolved,
|
|
7458
|
+
token: projectContext.token,
|
|
7459
|
+
projectId: request.projectId,
|
|
7460
|
+
tool: request.tool,
|
|
7461
|
+
agent: "claude-code",
|
|
7462
|
+
sessionId,
|
|
7463
|
+
phase: "confirm",
|
|
7464
|
+
// Judge over the FULL (schema-bounded 2048) redacted action — the default
|
|
7465
|
+
// redactTarget cap of 256 (right for the DB-stored violation summary) would
|
|
7466
|
+
// truncate a long dangerous action and risk an overturn on a partial view.
|
|
7467
|
+
actionSummary: redactTarget(targetStr, 2048),
|
|
7468
|
+
actionHash,
|
|
7469
|
+
rules: llmRules.map((r) => ({
|
|
7470
|
+
id: r.id,
|
|
7471
|
+
name: r.name ?? r.id,
|
|
7472
|
+
description: r.description ?? "",
|
|
7473
|
+
severity: r.severity
|
|
7474
|
+
}))
|
|
7475
|
+
});
|
|
7476
|
+
if (res) judge.cacheSet(cacheKey, res);
|
|
7477
|
+
}
|
|
7478
|
+
if (!res) {
|
|
7479
|
+
return {
|
|
7480
|
+
engine: "llm",
|
|
7481
|
+
outcome: "fallback",
|
|
7482
|
+
fallback_reason: "unavailable"
|
|
7483
|
+
};
|
|
7484
|
+
}
|
|
7485
|
+
if (res.quotaExceeded) {
|
|
7486
|
+
judge.warnOnce(
|
|
7487
|
+
sessionId,
|
|
7488
|
+
"VibeDefend judge quota exhausted \u2014 regex-only enforcement for this session."
|
|
7489
|
+
);
|
|
7490
|
+
return { engine: "llm", outcome: "quota_exceeded", model: res.model };
|
|
7491
|
+
}
|
|
7492
|
+
if (res.decision === "allow") {
|
|
7493
|
+
return { engine: "llm", outcome: "overturned", model: res.model };
|
|
7494
|
+
}
|
|
7495
|
+
return { engine: "llm", outcome: "confirmed", model: res.model };
|
|
7496
|
+
}
|
|
7497
|
+
async function runCatchJudgeStep(args) {
|
|
7498
|
+
const {
|
|
7499
|
+
judge,
|
|
7500
|
+
llmJudgeHint,
|
|
7501
|
+
rules,
|
|
7502
|
+
decision,
|
|
7503
|
+
projectContext,
|
|
7504
|
+
request,
|
|
7505
|
+
sessionId
|
|
7506
|
+
} = args;
|
|
7507
|
+
if (llmJudgeHint?.enabled === false) return void 0;
|
|
7508
|
+
const matchedIds = new Set(decision.matchedRuleIds);
|
|
7509
|
+
const candidates = rules.filter(
|
|
7510
|
+
(r) => r.action === "deny" && r.llmJudgeEnabled && r.catchEligible && r.severity !== "critical" && !matchedIds.has(r.id)
|
|
7511
|
+
);
|
|
7512
|
+
if (candidates.length === 0) return void 0;
|
|
7513
|
+
const near = candidates.filter((r) => nearMiss(request, r));
|
|
7514
|
+
if (near.length === 0) return void 0;
|
|
7515
|
+
const targetStr = stringifyTarget(request.target);
|
|
7516
|
+
const actionHash = hashTarget(targetStr);
|
|
7517
|
+
const ruleIds = near.map((r) => r.id).sort();
|
|
7518
|
+
const cacheKey = `${actionHash}|${ruleIds.join(",")}|catch`;
|
|
7519
|
+
let res = judge.cacheGet(cacheKey);
|
|
7520
|
+
if (!res) {
|
|
7521
|
+
res = await judge.callJudge({
|
|
7522
|
+
apiBaseResolved: projectContext.apiBaseResolved,
|
|
7523
|
+
token: projectContext.token,
|
|
7524
|
+
projectId: request.projectId,
|
|
7525
|
+
tool: request.tool,
|
|
7526
|
+
agent: "claude-code",
|
|
7527
|
+
sessionId,
|
|
7528
|
+
phase: "catch",
|
|
7529
|
+
actionSummary: redactTarget(targetStr, 2048),
|
|
7530
|
+
actionHash,
|
|
7531
|
+
rules: near.map((r) => ({
|
|
7532
|
+
id: r.id,
|
|
7533
|
+
name: r.name ?? r.id,
|
|
7534
|
+
description: r.description ?? "",
|
|
7535
|
+
severity: r.severity
|
|
7536
|
+
}))
|
|
7537
|
+
});
|
|
7538
|
+
if (res) judge.cacheSet(cacheKey, res);
|
|
7539
|
+
}
|
|
7540
|
+
if (!res) return void 0;
|
|
7541
|
+
if (res.quotaExceeded) {
|
|
7542
|
+
judge.warnOnce(
|
|
7543
|
+
sessionId,
|
|
7544
|
+
"VibeDefend judge quota exhausted \u2014 regex-only enforcement for this session."
|
|
7545
|
+
);
|
|
7546
|
+
return void 0;
|
|
7547
|
+
}
|
|
7548
|
+
if (res.decision !== "block") return void 0;
|
|
7549
|
+
const top = near[0];
|
|
7550
|
+
const message = `Blocked by the VibeDefend judge: this action resembles a dangerous pattern (${top.name ?? top.id}) the rules did not catch.`;
|
|
7551
|
+
return {
|
|
7552
|
+
audit: { engine: "llm", outcome: "caught", model: res.model },
|
|
7553
|
+
ruleIds,
|
|
7554
|
+
message
|
|
7555
|
+
};
|
|
7556
|
+
}
|
|
7557
|
+
function emitUnverifiedBanner(reason) {
|
|
7558
|
+
const banner = [
|
|
7559
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
|
7560
|
+
"\u2588 VibeDefend Action Guards \u2014 UNVERIFIED \u2588",
|
|
7561
|
+
"\u2588\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2588",
|
|
7562
|
+
"\u2588 Action Guards could NOT be enforced: \u2588",
|
|
7563
|
+
`\u2588 ${reason.slice(0, 76).padEnd(76)}\u2588`,
|
|
7564
|
+
"\u2588 \u2588",
|
|
7565
|
+
"\u2588 Your tool call is going through WITHOUT enforcement. This means: \u2588",
|
|
7566
|
+
"\u2588 \u2022 sensitive files / commands / URLs are NOT being blocked \u2588",
|
|
7567
|
+
"\u2588 \u2022 Action Guards rules are NOT being applied \u2588",
|
|
7568
|
+
"\u2588 \u2588",
|
|
7569
|
+
"\u2588 Fix this immediately: \u2588",
|
|
7570
|
+
"\u2588 vibedefend login \u2588",
|
|
7571
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
|
|
7572
|
+
].join("\n") + "\n";
|
|
7573
|
+
process.stderr.write(banner);
|
|
7574
|
+
const isCodex = typeof process.env.CODEX_SANDBOX !== "undefined" || typeof process.env.CODEX_AGENT !== "undefined";
|
|
7575
|
+
if (isCodex) {
|
|
7576
|
+
try {
|
|
7577
|
+
const ctx = JSON.stringify({
|
|
7578
|
+
additionalContext: `VibeDefend Action Guards UNVERIFIED \u2014 ${reason} Tool call allowed WITHOUT enforcement. Run \`vibedefend login\` to restore protection.`
|
|
7579
|
+
});
|
|
7580
|
+
process.stdout.write(ctx + "\n");
|
|
7581
|
+
} catch {
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
}
|
|
6896
7585
|
async function runPreToolUseGuardWith(opts) {
|
|
6897
|
-
const {
|
|
7586
|
+
const {
|
|
7587
|
+
stdinEnvelope,
|
|
7588
|
+
projectContext,
|
|
7589
|
+
rulesOutcome,
|
|
7590
|
+
evaluateFn,
|
|
7591
|
+
bufferViolationFn
|
|
7592
|
+
} = opts;
|
|
6898
7593
|
if (!projectContext) return 0;
|
|
6899
7594
|
if (rulesOutcome.kind === "unavailable") {
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
"\u2588\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2588",
|
|
6904
|
-
"\u2588 The policy engine could not be reached: \u2588",
|
|
6905
|
-
`\u2588 ${rulesOutcome.reason.slice(0, 76).padEnd(76)}\u2588`,
|
|
6906
|
-
"\u2588 \u2588",
|
|
6907
|
-
"\u2588 Your tool call is going through WITHOUT enforcement. This means: \u2588",
|
|
6908
|
-
"\u2588 \u2022 sensitive files / commands / URLs are NOT being blocked \u2588",
|
|
6909
|
-
"\u2588 \u2022 Action Guards rules are NOT being applied \u2588",
|
|
6910
|
-
"\u2588 \u2588",
|
|
6911
|
-
"\u2588 Fix this immediately: \u2588",
|
|
6912
|
-
"\u2588 vibedefend login \u2588",
|
|
6913
|
-
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"
|
|
6914
|
-
].join("\n") + "\n";
|
|
6915
|
-
process.stderr.write(banner);
|
|
6916
|
-
const isCodex = typeof process.env.CODEX_SANDBOX !== "undefined" || typeof process.env.CODEX_AGENT !== "undefined";
|
|
6917
|
-
if (isCodex) {
|
|
6918
|
-
try {
|
|
6919
|
-
const ctx = JSON.stringify({
|
|
6920
|
-
additionalContext: `VibeDefend Action Guards UNVERIFIED \u2014 policy engine unreachable (${rulesOutcome.reason}). Tool call allowed WITHOUT enforcement. Run \`vibedefend login\` to restore protection.`
|
|
6921
|
-
});
|
|
6922
|
-
process.stdout.write(ctx + "\n");
|
|
6923
|
-
} catch {
|
|
6924
|
-
}
|
|
6925
|
-
}
|
|
7595
|
+
emitUnverifiedBanner(
|
|
7596
|
+
`Policy engine unreachable: ${rulesOutcome.reason}`
|
|
7597
|
+
);
|
|
6926
7598
|
return 0;
|
|
6927
7599
|
}
|
|
6928
7600
|
const rules = rulesOutcome.rules;
|
|
@@ -6934,17 +7606,54 @@ async function runPreToolUseGuardWith(opts) {
|
|
|
6934
7606
|
projectId: projectContext.projectId,
|
|
6935
7607
|
sessionId
|
|
6936
7608
|
};
|
|
6937
|
-
|
|
6938
|
-
|
|
7609
|
+
let decision = evaluateFn(rules, guardRequest);
|
|
7610
|
+
let judgeAudit;
|
|
7611
|
+
if (opts.judge && decision.verdict === "deny" && decision.matchedRuleIds.length > 0) {
|
|
7612
|
+
judgeAudit = await runJudgeStep({
|
|
7613
|
+
judge: opts.judge,
|
|
7614
|
+
llmJudgeHint: rulesOutcome.llmJudge,
|
|
7615
|
+
rules,
|
|
7616
|
+
decision,
|
|
7617
|
+
projectContext,
|
|
7618
|
+
request: guardRequest,
|
|
7619
|
+
sessionId
|
|
7620
|
+
});
|
|
7621
|
+
if (judgeAudit?.outcome === "overturned") {
|
|
7622
|
+
decision = { ...decision, verdict: "allow", message: void 0 };
|
|
7623
|
+
}
|
|
7624
|
+
}
|
|
7625
|
+
if (opts.judge && opts.judgeCatch && decision.verdict === "allow") {
|
|
7626
|
+
const caught = await runCatchJudgeStep({
|
|
7627
|
+
judge: opts.judge,
|
|
7628
|
+
llmJudgeHint: rulesOutcome.llmJudge,
|
|
7629
|
+
rules,
|
|
7630
|
+
decision,
|
|
7631
|
+
projectContext,
|
|
7632
|
+
request: guardRequest,
|
|
7633
|
+
sessionId
|
|
7634
|
+
});
|
|
7635
|
+
if (caught) {
|
|
7636
|
+
decision = {
|
|
7637
|
+
...decision,
|
|
7638
|
+
verdict: "deny",
|
|
7639
|
+
matchedRuleIds: caught.ruleIds,
|
|
7640
|
+
message: caught.message
|
|
7641
|
+
};
|
|
7642
|
+
judgeAudit = caught.audit;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
const isOverturn = judgeAudit?.outcome === "overturned";
|
|
7646
|
+
if (decision.verdict !== "allow" && decision.matchedRuleIds.length > 0 || isOverturn) {
|
|
6939
7647
|
const targetStr = stringifyTarget(guardRequest.target);
|
|
6940
7648
|
const violation = {
|
|
6941
7649
|
ruleId: decision.matchedRuleIds[0],
|
|
6942
7650
|
agent: "claude-code",
|
|
6943
7651
|
sessionId: sessionId ?? null,
|
|
6944
7652
|
tool: guardRequest.tool,
|
|
6945
|
-
outcome: decision.verdict === "deny" ? "denied" : "warned",
|
|
7653
|
+
outcome: decision.verdict === "deny" || isOverturn ? "denied" : "warned",
|
|
6946
7654
|
targetSummary: redactTarget(targetStr),
|
|
6947
|
-
targetHash: hashTarget(targetStr)
|
|
7655
|
+
targetHash: hashTarget(targetStr),
|
|
7656
|
+
...judgeAudit ? { extra: { judge: judgeAudit } } : {}
|
|
6948
7657
|
};
|
|
6949
7658
|
try {
|
|
6950
7659
|
bufferViolationFn(projectContext, violation);
|
|
@@ -7005,6 +7714,27 @@ function emitAgentMessage(verdict, message) {
|
|
|
7005
7714
|
} catch {
|
|
7006
7715
|
}
|
|
7007
7716
|
}
|
|
7717
|
+
function makeProductionJudgeDeps() {
|
|
7718
|
+
const cache = makeJudgeCache();
|
|
7719
|
+
const warnDir = process.env.CYBEDEFEND_BASELINE_DIR ?? join10(homedir8(), ".cybedefend");
|
|
7720
|
+
const warnTracker = defaultSessionTracker(
|
|
7721
|
+
join10(warnDir, "guards-judge-warned-sessions.json")
|
|
7722
|
+
);
|
|
7723
|
+
return {
|
|
7724
|
+
callJudge,
|
|
7725
|
+
cacheGet: (k) => cache.get(k),
|
|
7726
|
+
cacheSet: (k, r) => cache.set(k, r),
|
|
7727
|
+
warnOnce: (sessionId, message) => {
|
|
7728
|
+
if (sessionId && warnTracker.hasSeen(sessionId)) return;
|
|
7729
|
+
try {
|
|
7730
|
+
process.stderr.write(`[VibeDefend Action Guards] ${message}
|
|
7731
|
+
`);
|
|
7732
|
+
} catch {
|
|
7733
|
+
}
|
|
7734
|
+
if (sessionId) void warnTracker.markSeen(sessionId);
|
|
7735
|
+
}
|
|
7736
|
+
};
|
|
7737
|
+
}
|
|
7008
7738
|
async function runPreToolUseGuard() {
|
|
7009
7739
|
try {
|
|
7010
7740
|
const envelope = await readStdinJson();
|
|
@@ -7014,7 +7744,12 @@ async function runPreToolUseGuard() {
|
|
|
7014
7744
|
if (!projectId) return 0;
|
|
7015
7745
|
const apiBaseResolved = process.env.CYBEDEFEND_API_BASE ?? configApiBase ?? config.region.apiBase;
|
|
7016
7746
|
const token = await resolveTokenWithRefresh(config.region.mcpName);
|
|
7017
|
-
if (!token)
|
|
7747
|
+
if (!token) {
|
|
7748
|
+
emitUnverifiedBanner(
|
|
7749
|
+
"Not authenticated \u2014 run `vibedefend login` to restore Action Guards."
|
|
7750
|
+
);
|
|
7751
|
+
return 0;
|
|
7752
|
+
}
|
|
7018
7753
|
const projectContext = {
|
|
7019
7754
|
projectId,
|
|
7020
7755
|
apiBase: configApiBase,
|
|
@@ -7035,7 +7770,11 @@ async function runPreToolUseGuard() {
|
|
|
7035
7770
|
projectContext,
|
|
7036
7771
|
rulesOutcome,
|
|
7037
7772
|
evaluateFn: evaluate,
|
|
7038
|
-
bufferViolationFn: buffer.bufferViolation.bind(buffer)
|
|
7773
|
+
bufferViolationFn: buffer.bufferViolation.bind(buffer),
|
|
7774
|
+
judge: makeProductionJudgeDeps(),
|
|
7775
|
+
// v1.1 catch — OFF by default; opt in with CYBEDEFEND_JUDGE_CATCH=1 once
|
|
7776
|
+
// the call-rate gate is validated in the field.
|
|
7777
|
+
judgeCatch: process.env.CYBEDEFEND_JUDGE_CATCH === "1"
|
|
7039
7778
|
});
|
|
7040
7779
|
try {
|
|
7041
7780
|
await buffer.flushViolations(
|
|
@@ -7082,6 +7821,9 @@ function toApiViolation(r) {
|
|
|
7082
7821
|
if (r.sessionId) {
|
|
7083
7822
|
out.session_id = r.sessionId;
|
|
7084
7823
|
}
|
|
7824
|
+
if (r.extra && Object.keys(r.extra).length > 0) {
|
|
7825
|
+
out.extra = r.extra;
|
|
7826
|
+
}
|
|
7085
7827
|
return out;
|
|
7086
7828
|
}
|
|
7087
7829
|
|