@cybedefend/vibedefend 1.4.6 → 1.4.7

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.
@@ -5808,7 +5808,165 @@ async function resolveProjectIdWithApi(cwd = process.cwd(), deps = {}) {
5808
5808
  }
5809
5809
  }
5810
5810
 
5811
+ // src/hooks/runtime/usage-block.ts
5812
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5813
+ import { homedir as homedir6 } from "node:os";
5814
+ import { dirname as dirname3, join as join5 } from "node:path";
5815
+ var BLOCKED_CODE = "VIBEDEFEND_BLOCKED";
5816
+ var USAGE_WARNING_HEADER = "x-vibedefend-usage-warning";
5817
+ var UNITS = ["injection", "judgment", "diff_scan"];
5818
+ var REASONS = [
5819
+ "free_tier_exhausted",
5820
+ "cap_reached",
5821
+ "unpaid",
5822
+ "product_inactive"
5823
+ ];
5824
+ var WARNINGS = [
5825
+ "approaching_limit",
5826
+ "limit_reached"
5827
+ ];
5828
+ function pick(allowed, value) {
5829
+ return allowed.find((v) => v === value);
5830
+ }
5831
+ async function readVibedefendBlock(res) {
5832
+ if (res.status !== 402) return null;
5833
+ let body;
5834
+ try {
5835
+ body = await res.json();
5836
+ } catch {
5837
+ return null;
5838
+ }
5839
+ if (!body || typeof body !== "object") return null;
5840
+ const root = body;
5841
+ const data = root.data && typeof root.data === "object" ? root.data : root;
5842
+ const source = data.code === BLOCKED_CODE ? data : root;
5843
+ if (source.code !== BLOCKED_CODE) return null;
5844
+ return {
5845
+ reason: pick(REASONS, source.reason) ?? "unknown",
5846
+ unit: pick(UNITS, source.unit) ?? "unknown"
5847
+ };
5848
+ }
5849
+ function readUsageWarning(res) {
5850
+ let raw;
5851
+ try {
5852
+ raw = res.headers?.get?.(USAGE_WARNING_HEADER);
5853
+ } catch {
5854
+ return null;
5855
+ }
5856
+ if (typeof raw !== "string") return null;
5857
+ const parts = raw.trim().split(":");
5858
+ if (parts.length !== 2) return null;
5859
+ const unit = pick(UNITS, parts[0]);
5860
+ const warning = pick(WARNINGS, parts[1]);
5861
+ if (!unit || !warning) return null;
5862
+ return { unit, warning };
5863
+ }
5864
+ var UNIT_LABEL = {
5865
+ injection: "Security rules injection",
5866
+ judgment: "Action Guards judgments",
5867
+ diff_scan: "Diff scans",
5868
+ unknown: "Part of VibeDefend"
5869
+ };
5870
+ var UNIT_EFFECT = {
5871
+ injection: "no security or business rules are added to your edits",
5872
+ judgment: "Action Guards are off for this session",
5873
+ diff_scan: "no diff scan can be started",
5874
+ unknown: "part of VibeDefend is paused"
5875
+ };
5876
+ function blockSentence(block) {
5877
+ const label = UNIT_LABEL[block.unit];
5878
+ const effect = UNIT_EFFECT[block.unit];
5879
+ switch (block.reason) {
5880
+ case "free_tier_exhausted":
5881
+ return `VibeDefend paused: ${label} used up this month's free tier, so ${effect}. An administrator of your organisation can add a card in the VibeDefend usage page to continue.`;
5882
+ case "cap_reached":
5883
+ return `VibeDefend paused: ${label} reached the monthly spending cap of your organisation, so ${effect}. An administrator can raise the cap in the VibeDefend usage page.`;
5884
+ case "unpaid":
5885
+ return "VibeDefend paused: your organisation's last VibeDefend invoice is unpaid, so rules, Action Guards and diff scans are off. An administrator can update the card in the VibeDefend usage page.";
5886
+ case "product_inactive":
5887
+ return "VibeDefend is not active for your organisation, so rules, Action Guards and diff scans are off. An administrator can activate it in the CybeDefend dashboard.";
5888
+ default:
5889
+ return `VibeDefend paused: ${label} is blocked for your organisation, so ${effect}. An administrator can check the VibeDefend usage page.`;
5890
+ }
5891
+ }
5892
+ function warningSentence(w) {
5893
+ const label = UNIT_LABEL[w.unit];
5894
+ if (w.warning === "approaching_limit") {
5895
+ return `VibeDefend: ${label} has used 80 % of this month's allowance of your organisation. An administrator can follow it in the VibeDefend usage page.`;
5896
+ }
5897
+ return `VibeDefend: ${label} has reached this month's limit of your organisation; the next requests will be refused. An administrator can add a card or raise the cap in the VibeDefend usage page.`;
5898
+ }
5899
+ var ORG_WIDE_REASONS = /* @__PURE__ */ new Set([
5900
+ "unpaid",
5901
+ "product_inactive"
5902
+ ]);
5903
+ function blockKey(block) {
5904
+ return ORG_WIDE_REASONS.has(block.reason) ? `block:${block.reason}` : `block:${block.reason}:${block.unit}`;
5905
+ }
5906
+ var NOTICES_CAP = 300;
5907
+ function defaultUsageNoticesFile() {
5908
+ const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join5(homedir6(), ".cybedefend");
5909
+ return join5(dir, "usage-notices-sessions.json");
5910
+ }
5911
+ function makeUsageNotifier(opts = {}) {
5912
+ const file = opts.file ?? defaultUsageNoticesFile();
5913
+ const write = opts.write ?? ((line) => {
5914
+ try {
5915
+ process.stderr.write(line);
5916
+ } catch {
5917
+ }
5918
+ });
5919
+ function load() {
5920
+ try {
5921
+ if (!existsSync7(file)) return [];
5922
+ const data = JSON.parse(readFileSync6(file, "utf8"));
5923
+ return Array.isArray(data.seen) ? data.seen.filter((s) => typeof s === "string") : [];
5924
+ } catch {
5925
+ return [];
5926
+ }
5927
+ }
5928
+ function once(sessionId, key, sentence) {
5929
+ if (sessionId) {
5930
+ const marker = `${sessionId}|${key}`;
5931
+ const seen = load();
5932
+ if (seen.includes(marker)) return null;
5933
+ try {
5934
+ mkdirSync4(dirname3(file), { recursive: true });
5935
+ writeFileSync3(
5936
+ file,
5937
+ JSON.stringify({ seen: [...seen, marker].slice(-NOTICES_CAP) }),
5938
+ "utf8"
5939
+ );
5940
+ } catch {
5941
+ }
5942
+ }
5943
+ write(`[VibeDefend] ${sentence}
5944
+ `);
5945
+ return sentence;
5946
+ }
5947
+ return {
5948
+ notifyBlock: (sessionId, block) => once(sessionId, blockKey(block), blockSentence(block)),
5949
+ notifyWarning: (sessionId, w) => once(sessionId, `warning:${w.unit}:${w.warning}`, warningSentence(w))
5950
+ };
5951
+ }
5952
+
5811
5953
  // src/hooks/runtime/api.ts
5954
+ async function reportUsageSignal(res, listener) {
5955
+ const emit2 = (signal) => {
5956
+ try {
5957
+ listener?.(signal);
5958
+ } catch {
5959
+ }
5960
+ };
5961
+ if (!res.ok) {
5962
+ const block = await readVibedefendBlock(res);
5963
+ if (block) emit2({ kind: "blocked", block });
5964
+ return false;
5965
+ }
5966
+ const warning = readUsageWarning(res);
5967
+ if (warning) emit2({ kind: "warning", warning });
5968
+ return true;
5969
+ }
5812
5970
  var DEFAULT_TIMEOUT_MS = 1e4;
5813
5971
  async function fetchBusinessRules(opts) {
5814
5972
  const f = opts.fetchImpl ?? globalThis.fetch;
@@ -5832,7 +5990,7 @@ async function fetchBusinessRules(opts) {
5832
5990
  } catch {
5833
5991
  return null;
5834
5992
  }
5835
- if (!res.ok) return null;
5993
+ if (!await reportUsageSignal(res, opts.onUsageSignal)) return null;
5836
5994
  let body;
5837
5995
  try {
5838
5996
  body = await res.json();
@@ -5867,7 +6025,7 @@ async function fetchSecurityRules(opts) {
5867
6025
  } catch {
5868
6026
  return null;
5869
6027
  }
5870
- if (!res.ok) return null;
6028
+ if (!await reportUsageSignal(res, opts.onUsageSignal)) return null;
5871
6029
  let body;
5872
6030
  try {
5873
6031
  body = await res.json();
@@ -5971,6 +6129,7 @@ async function fetchRulesHook(deps = {}) {
5971
6129
  const fetchRules = deps.fetchRulesFn ?? fetchBusinessRules;
5972
6130
  const fetchSecRules = deps.fetchSecurityRulesFn ?? fetchSecurityRules;
5973
6131
  const emitFn = deps.emitFn ?? emitPreToolUseContext;
6132
+ const usageNotifier = deps.usageNotifier ?? makeUsageNotifier();
5974
6133
  const stdin = await readStdin();
5975
6134
  const event = sniff(stdin);
5976
6135
  if (!INTERESTED_TOOLS.has(event.toolName)) return;
@@ -5987,13 +6146,20 @@ async function fetchRulesHook(deps = {}) {
5987
6146
  const intent = buildIntent(event);
5988
6147
  const files = event.filePaths.length > 0 ? event.filePaths : [event.filePath];
5989
6148
  const fileLabel = files.join(", ");
6149
+ const sessionId = sessionIdOf(stdin);
6150
+ const usageNotices = [];
6151
+ const onUsageSignal = (signal) => {
6152
+ const sentence = signal.kind === "blocked" ? usageNotifier.notifyBlock(sessionId, signal.block) : usageNotifier.notifyWarning(sessionId, signal.warning);
6153
+ if (sentence) usageNotices.push(sentence);
6154
+ };
5990
6155
  const fetchOpts = {
5991
6156
  apiBase,
5992
6157
  projectId,
5993
6158
  token,
5994
6159
  files,
5995
6160
  intent,
5996
- topK: 5
6161
+ topK: 5,
6162
+ onUsageSignal
5997
6163
  };
5998
6164
  const [businessResult, securityResult] = await Promise.all([
5999
6165
  fetchRules(fetchOpts),
@@ -6017,8 +6183,18 @@ async function fetchRulesHook(deps = {}) {
6017
6183
  )
6018
6184
  );
6019
6185
  }
6186
+ if (usageNotices.length > 0) {
6187
+ blocks.push(
6188
+ ["## CybeDefend VibeDefend usage", ...usageNotices.map((n) => `- ${n}`), ""].join("\n")
6189
+ );
6190
+ }
6020
6191
  if (blocks.length > 0) emitFn(blocks.join("\n"), event.client);
6021
6192
  }
6193
+ function sessionIdOf(stdin) {
6194
+ if (typeof stdin.session_id === "string" && stdin.session_id) return stdin.session_id;
6195
+ if (typeof stdin.sessionId === "string" && stdin.sessionId) return stdin.sessionId;
6196
+ return void 0;
6197
+ }
6022
6198
  function buildIntent(event) {
6023
6199
  const limit = parseInt(process.env.CYBEDEFEND_INTENT_BYTES ?? "800", 10);
6024
6200
  const snippet = event.newContent.slice(0, Number.isFinite(limit) ? limit : 800).replace(/\s+/g, " ").trim();
@@ -6099,9 +6275,9 @@ var DIRECT_PROPOSE_DIRECTIVES = {
6099
6275
 
6100
6276
  // src/hooks/runtime/self-update-check.ts
6101
6277
  import { spawn } from "node:child_process";
6102
- import { existsSync as existsSync8, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
6103
- import { homedir as homedir6 } from "node:os";
6104
- import { join as join5 } from "node:path";
6278
+ import { existsSync as existsSync9, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
6279
+ import { homedir as homedir7 } from "node:os";
6280
+ import { join as join6 } from "node:path";
6105
6281
 
6106
6282
  // src/hooks/runtime/semver.ts
6107
6283
  function parseVersion(input) {
@@ -6129,7 +6305,7 @@ function isNewer(latest, current) {
6129
6305
 
6130
6306
  // src/self-update.ts
6131
6307
  import { fileURLToPath } from "node:url";
6132
- import { existsSync as existsSync7, realpathSync } from "node:fs";
6308
+ import { existsSync as existsSync8, realpathSync } from "node:fs";
6133
6309
  var GLOBAL_MODES = /* @__PURE__ */ new Set([
6134
6310
  "npm-global",
6135
6311
  "pnpm-global",
@@ -6157,7 +6333,7 @@ function detectInstallMode(scriptPath) {
6157
6333
  }
6158
6334
  function resolveScriptPath() {
6159
6335
  const fromArgv = process.argv[1];
6160
- if (fromArgv && existsSync7(fromArgv)) {
6336
+ if (fromArgv && existsSync8(fromArgv)) {
6161
6337
  try {
6162
6338
  return realpathSync(fromArgv);
6163
6339
  } catch {
@@ -6179,9 +6355,9 @@ function isValidRegistryVersion(value) {
6179
6355
  }
6180
6356
  async function selfUpdateCheck(deps) {
6181
6357
  const now = deps.now ?? (() => Date.now());
6182
- const dir = deps.cybeDir ?? join5(process.env.CYBEDEFEND_HOME ?? homedir6(), ".cybedefend");
6183
- const throttlePath = join5(dir, "update-check.json");
6184
- const logPath = join5(dir, "last-update.log");
6358
+ const dir = deps.cybeDir ?? join6(process.env.CYBEDEFEND_HOME ?? homedir7(), ".cybedefend");
6359
+ const throttlePath = join6(dir, "update-check.json");
6360
+ const logPath = join6(dir, "last-update.log");
6185
6361
  const installMode = deps.installMode ?? (deps.runnerSource ? detectInstallMode(deps.runnerSource) : detectInstallMode(resolveScriptPath()));
6186
6362
  const priorFailure = takeUpdateLogNotice(logPath);
6187
6363
  const throttle = readThrottle(throttlePath);
@@ -6214,8 +6390,8 @@ function applyOrNotify(deps, installMode, latest, logPath) {
6214
6390
  }
6215
6391
  function readThrottle(path) {
6216
6392
  try {
6217
- if (!existsSync8(path)) return null;
6218
- const data = JSON.parse(readFileSync6(path, "utf8"));
6393
+ if (!existsSync9(path)) return null;
6394
+ const data = JSON.parse(readFileSync7(path, "utf8"));
6219
6395
  if (typeof data.lastCheckedAt !== "number") return null;
6220
6396
  return { lastCheckedAt: data.lastCheckedAt };
6221
6397
  } catch {
@@ -6225,16 +6401,16 @@ function readThrottle(path) {
6225
6401
  function writeThrottle(path, throttle) {
6226
6402
  try {
6227
6403
  ensureDirFor(path);
6228
- writeFileSync3(path, JSON.stringify(throttle) + "\n");
6404
+ writeFileSync4(path, JSON.stringify(throttle) + "\n");
6229
6405
  } catch {
6230
6406
  }
6231
6407
  }
6232
6408
  function takeUpdateLogNotice(logPath) {
6233
6409
  try {
6234
- if (!existsSync8(logPath)) return null;
6410
+ if (!existsSync9(logPath)) return null;
6235
6411
  let parsed = {};
6236
6412
  try {
6237
- parsed = JSON.parse(readFileSync6(logPath, "utf8"));
6413
+ parsed = JSON.parse(readFileSync7(logPath, "utf8"));
6238
6414
  } catch {
6239
6415
  parsed = {};
6240
6416
  }
@@ -6309,18 +6485,18 @@ function pmCommand(mode, spec) {
6309
6485
 
6310
6486
  // src/hooks/runtime/hook-refresh-trigger.ts
6311
6487
  import { spawn as spawn2 } from "node:child_process";
6312
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
6313
- import { homedir as homedir7 } from "node:os";
6314
- import { dirname as dirname3, join as join6 } from "node:path";
6488
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
6489
+ import { homedir as homedir8 } from "node:os";
6490
+ import { dirname as dirname4, join as join7 } from "node:path";
6315
6491
  function maybeRefreshStaleWiring(deps) {
6316
6492
  try {
6317
6493
  const { config, runnerVersion } = deps;
6318
6494
  if (!config.runnerSource || config.installedVersion === runnerVersion) {
6319
6495
  return false;
6320
6496
  }
6321
- const cliEntry = join6(dirname3(dirname3(config.runnerSource)), "bin", "vibedefend.js");
6322
- if (!(deps.exists ?? existsSync9)(cliEntry)) return false;
6323
- const markerPath = deps.markerPath ?? join6(process.env.CYBEDEFEND_HOME ?? homedir7(), ".cybedefend", "hook-refresh.json");
6497
+ const cliEntry = join7(dirname4(dirname4(config.runnerSource)), "bin", "vibedefend.js");
6498
+ if (!(deps.exists ?? existsSync10)(cliEntry)) return false;
6499
+ const markerPath = deps.markerPath ?? join7(process.env.CYBEDEFEND_HOME ?? homedir8(), ".cybedefend", "hook-refresh.json");
6324
6500
  if (readMarkerVersion(markerPath) === runnerVersion) return false;
6325
6501
  (deps.spawnRefresh ?? defaultSpawnRefresh)(cliEntry);
6326
6502
  writeMarker(markerPath, runnerVersion);
@@ -6331,8 +6507,8 @@ function maybeRefreshStaleWiring(deps) {
6331
6507
  }
6332
6508
  function readMarkerVersion(path) {
6333
6509
  try {
6334
- if (!existsSync9(path)) return null;
6335
- const data = JSON.parse(readFileSync7(path, "utf8"));
6510
+ if (!existsSync10(path)) return null;
6511
+ const data = JSON.parse(readFileSync8(path, "utf8"));
6336
6512
  return typeof data.version === "string" ? data.version : null;
6337
6513
  } catch {
6338
6514
  return null;
@@ -6341,7 +6517,7 @@ function readMarkerVersion(path) {
6341
6517
  function writeMarker(path, version) {
6342
6518
  try {
6343
6519
  ensureDirFor(path);
6344
- writeFileSync4(path, JSON.stringify({ version, spawnedAt: Date.now() }) + "\n");
6520
+ writeFileSync5(path, JSON.stringify({ version, spawnedAt: Date.now() }) + "\n");
6345
6521
  } catch {
6346
6522
  }
6347
6523
  }
@@ -6355,18 +6531,18 @@ function defaultSpawnRefresh(cliEntry) {
6355
6531
  }
6356
6532
 
6357
6533
  // src/version.ts
6358
- import { readFileSync as readFileSync8 } from "node:fs";
6534
+ import { readFileSync as readFileSync9 } from "node:fs";
6359
6535
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6360
- import { dirname as dirname4, join as join7 } from "node:path";
6536
+ import { dirname as dirname5, join as join8 } from "node:path";
6361
6537
  var PKG_NAME = "@cybedefend/vibedefend";
6362
6538
  function defaultPkgPath() {
6363
- return join7(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
6539
+ return join8(dirname5(fileURLToPath2(import.meta.url)), "..", "package.json");
6364
6540
  }
6365
6541
  function readPackageMeta(opts = {}) {
6366
6542
  if (typeof opts.bakedVersion === "string" && opts.bakedVersion.length > 0) {
6367
6543
  return { name: PKG_NAME, version: opts.bakedVersion };
6368
6544
  }
6369
- const readFile = opts.readFile ?? ((p) => readFileSync8(p, "utf8"));
6545
+ const readFile = opts.readFile ?? ((p) => readFileSync9(p, "utf8"));
6370
6546
  const pkgPath = opts.pkgPath ?? defaultPkgPath();
6371
6547
  try {
6372
6548
  return JSON.parse(readFile(pkgPath));
@@ -6375,7 +6551,7 @@ function readPackageMeta(opts = {}) {
6375
6551
  }
6376
6552
  }
6377
6553
  var pkg = readPackageMeta({
6378
- bakedVersion: true ? "1.4.6" : void 0
6554
+ bakedVersion: true ? "1.4.7" : void 0
6379
6555
  });
6380
6556
 
6381
6557
  // src/hooks/runtime/session-start.ts
@@ -6491,7 +6667,7 @@ async function sessionStartHook(deps = {}) {
6491
6667
  }
6492
6668
 
6493
6669
  // src/hooks/runtime/session-review.ts
6494
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
6670
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
6495
6671
  var COUNTED_TOOLS = /* @__PURE__ */ new Set([
6496
6672
  "Edit",
6497
6673
  "Write",
@@ -6575,9 +6751,9 @@ function loadTranscript(stdin, readFile) {
6575
6751
  return out;
6576
6752
  }
6577
6753
  function defaultReadTranscript(path) {
6578
- if (!existsSync10(path)) return null;
6754
+ if (!existsSync11(path)) return null;
6579
6755
  try {
6580
- return readFileSync9(path, "utf8");
6756
+ return readFileSync10(path, "utf8");
6581
6757
  } catch {
6582
6758
  return null;
6583
6759
  }
@@ -6837,19 +7013,19 @@ async function preCompactHook(deps = {}) {
6837
7013
  }
6838
7014
 
6839
7015
  // src/hooks/runtime/user-prompt-submit.ts
6840
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
6841
- import { homedir as homedir8 } from "node:os";
6842
- import { join as join8 } from "node:path";
7016
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
7017
+ import { homedir as homedir9 } from "node:os";
7018
+ import { join as join9 } from "node:path";
6843
7019
  var SESSIONS_CAP = 100;
6844
7020
  function defaultSessionsFile() {
6845
- const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join8(homedir8(), ".cybedefend");
6846
- return join8(dir, "user-prompt-seen-sessions.json");
7021
+ const dir = process.env.CYBEDEFEND_BASELINE_DIR ?? join9(homedir9(), ".cybedefend");
7022
+ return join9(dir, "user-prompt-seen-sessions.json");
6847
7023
  }
6848
7024
  function defaultSessionTracker(file = defaultSessionsFile()) {
6849
7025
  function load() {
6850
- if (!existsSync11(file)) return [];
7026
+ if (!existsSync12(file)) return [];
6851
7027
  try {
6852
- const raw = readFileSync10(file, "utf8");
7028
+ const raw = readFileSync11(file, "utf8");
6853
7029
  const data = JSON.parse(raw);
6854
7030
  if (!Array.isArray(data.sessions)) return [];
6855
7031
  return data.sessions.filter((s) => typeof s === "string");
@@ -6866,8 +7042,8 @@ function defaultSessionTracker(file = defaultSessionsFile()) {
6866
7042
  if (existing.includes(sessionId)) return;
6867
7043
  const next = [...existing, sessionId].slice(-SESSIONS_CAP);
6868
7044
  try {
6869
- mkdirSync4(join8(file, ".."), { recursive: true });
6870
- writeFileSync5(file, JSON.stringify({ sessions: next }), "utf8");
7045
+ mkdirSync5(join9(file, ".."), { recursive: true });
7046
+ writeFileSync6(file, JSON.stringify({ sessions: next }), "utf8");
6871
7047
  } catch {
6872
7048
  }
6873
7049
  }
@@ -7938,10 +8114,10 @@ function nearMiss(req, rule) {
7938
8114
  }
7939
8115
 
7940
8116
  // src/hooks/runtime/guard-rules-cache.ts
7941
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync12, mkdirSync as mkdirSync5 } from "node:fs";
7942
- import { dirname as dirname5 } from "node:path";
7943
- import { homedir as homedir9 } from "node:os";
7944
- import { join as join9 } from "node:path";
8117
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "node:fs";
8118
+ import { dirname as dirname6 } from "node:path";
8119
+ import { homedir as homedir10 } from "node:os";
8120
+ import { join as join10 } from "node:path";
7945
8121
 
7946
8122
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
7947
8123
  var external_exports = {};
@@ -12053,8 +12229,8 @@ function parseRuleFields(raw) {
12053
12229
  }
12054
12230
 
12055
12231
  // src/hooks/runtime/guard-rules-cache.ts
12056
- var DEFAULT_CACHE_FILE = join9(
12057
- homedir9(),
12232
+ var DEFAULT_CACHE_FILE = join10(
12233
+ homedir10(),
12058
12234
  ".cybedefend",
12059
12235
  "guards-cache.json"
12060
12236
  );
@@ -12134,14 +12310,26 @@ function makeGuardRulesCache(opts) {
12134
12310
  const entry = readCache(ctx.projectId);
12135
12311
  const ageSec = entry ? (Date.now() - entry.fetchedAt) / 1e3 : Infinity;
12136
12312
  if (entry && ageSec < staleSec) {
12313
+ if (entry.blocked) return { kind: "blocked", block: entry.blocked };
12137
12314
  return { kind: "rules", rules: entry.rules, llmJudge: entry.llmJudge };
12138
12315
  }
12139
12316
  try {
12140
12317
  const result = await fetchFn({
12141
12318
  ...ctx,
12142
- etag: entry?.etag ?? null
12319
+ etag: entry?.blocked ? null : entry?.etag ?? null
12143
12320
  });
12144
- if (result.notModified && entry) {
12321
+ if (result.blocked) {
12322
+ writeCache({
12323
+ projectId: ctx.projectId,
12324
+ rules: [],
12325
+ etag: null,
12326
+ fetchedAt: Date.now(),
12327
+ llmJudge: null,
12328
+ blocked: result.blocked
12329
+ });
12330
+ return { kind: "blocked", block: result.blocked };
12331
+ }
12332
+ if (result.notModified && entry && !entry.blocked) {
12145
12333
  writeCache({
12146
12334
  ...entry,
12147
12335
  fetchedAt: Date.now()
@@ -12163,7 +12351,7 @@ function makeGuardRulesCache(opts) {
12163
12351
  return { kind: "rules", rules: fresh.rules, llmJudge: fresh.llmJudge };
12164
12352
  } catch (e) {
12165
12353
  const msg = e instanceof Error ? e.message : String(e);
12166
- if (entry?.rules?.length) {
12354
+ if (entry?.rules?.length && !entry.blocked) {
12167
12355
  const ageSec2 = Math.round((Date.now() - entry.fetchedAt) / 1e3);
12168
12356
  process.stderr.write(
12169
12357
  `[VibeDefend] Action Guards: rule refresh failed (${msg}). Using cached rules (age=${ageSec2}s).
@@ -12178,9 +12366,9 @@ function makeGuardRulesCache(opts) {
12178
12366
  }
12179
12367
  }
12180
12368
  function readCache(projectId) {
12181
- if (!existsSync12(cacheFile)) return null;
12369
+ if (!existsSync13(cacheFile)) return null;
12182
12370
  try {
12183
- const raw = readFileSync11(cacheFile, "utf8");
12371
+ const raw = readFileSync12(cacheFile, "utf8");
12184
12372
  const data = JSON.parse(raw);
12185
12373
  if (data.projectId !== projectId) return null;
12186
12374
  return data;
@@ -12191,16 +12379,16 @@ function makeGuardRulesCache(opts) {
12191
12379
  function writeCache(entry) {
12192
12380
  try {
12193
12381
  ensureDirFor2(cacheFile);
12194
- writeFileSync6(cacheFile, JSON.stringify(entry), "utf8");
12382
+ writeFileSync7(cacheFile, JSON.stringify(entry), "utf8");
12195
12383
  } catch {
12196
12384
  }
12197
12385
  }
12198
12386
  return { refreshIfStale };
12199
12387
  }
12200
12388
  function ensureDirFor2(filePath) {
12201
- const dir = dirname5(filePath);
12202
- if (!existsSync12(dir)) {
12203
- mkdirSync5(dir, { recursive: true });
12389
+ const dir = dirname6(filePath);
12390
+ if (!existsSync13(dir)) {
12391
+ mkdirSync6(dir, { recursive: true });
12204
12392
  }
12205
12393
  }
12206
12394
  var DEFAULT_TIMEOUT_MS2 = 8e3;
@@ -12223,6 +12411,10 @@ async function fetchEffectiveRulesFromGateway(ctx, opts = {}) {
12223
12411
  return { rules: [], etag: ctx.etag ?? null, notModified: true };
12224
12412
  }
12225
12413
  if (!res.ok) {
12414
+ const blocked = await readVibedefendBlock(res);
12415
+ if (blocked) {
12416
+ return { rules: [], etag: null, notModified: false, blocked };
12417
+ }
12226
12418
  throw new Error(`HTTP ${res.status} from effective-rules endpoint`);
12227
12419
  }
12228
12420
  const body = await res.json();
@@ -12243,6 +12435,9 @@ function parseLlmJudgeHint(raw) {
12243
12435
  }
12244
12436
 
12245
12437
  // src/hooks/runtime/judge.ts
12438
+ function isJudgeBlocked(res) {
12439
+ return res !== null && "blocked" in res;
12440
+ }
12246
12441
  var DEFAULT_TIMEOUT_MS3 = 1500;
12247
12442
  async function callJudge(input, opts = {}) {
12248
12443
  const f = opts.fetchImpl ?? globalThis.fetch;
@@ -12266,7 +12461,11 @@ async function callJudge(input, opts = {}) {
12266
12461
  }),
12267
12462
  signal: AbortSignal.timeout(timeoutMs)
12268
12463
  });
12269
- if (!res.ok) return null;
12464
+ if (!res.ok) {
12465
+ const blocked = await readVibedefendBlock(res);
12466
+ return blocked ? { blocked } : null;
12467
+ }
12468
+ const usageWarning = readUsageWarning(res);
12270
12469
  const body = await res.json();
12271
12470
  const data = body.data ?? body;
12272
12471
  const decision = data["decision"];
@@ -12276,7 +12475,8 @@ async function callJudge(input, opts = {}) {
12276
12475
  reason: typeof data["reason"] === "string" ? data["reason"] : "",
12277
12476
  model: typeof data["model"] === "string" ? data["model"] : "",
12278
12477
  quotaRemaining: typeof data["quota_remaining"] === "number" ? data["quota_remaining"] : -1,
12279
- quotaExceeded: Boolean(data["quota_exceeded"])
12478
+ quotaExceeded: Boolean(data["quota_exceeded"]),
12479
+ ...usageWarning ? { usageWarning } : {}
12280
12480
  };
12281
12481
  } catch {
12282
12482
  return null;
@@ -12321,11 +12521,11 @@ function detectPromptInjection(text) {
12321
12521
  }
12322
12522
 
12323
12523
  // src/hooks/runtime/judge-cache.ts
12324
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "node:fs";
12325
- import { dirname as dirname6, join as join10 } from "node:path";
12326
- import { homedir as homedir10 } from "node:os";
12327
- var DEFAULT_JUDGE_CACHE_FILE = join10(
12328
- homedir10(),
12524
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
12525
+ import { dirname as dirname7, join as join11 } from "node:path";
12526
+ import { homedir as homedir11 } from "node:os";
12527
+ var DEFAULT_JUDGE_CACHE_FILE = join11(
12528
+ homedir11(),
12329
12529
  ".cybedefend",
12330
12530
  "guards-judge-cache.json"
12331
12531
  );
@@ -12335,9 +12535,9 @@ function makeJudgeCache(opts = {}) {
12335
12535
  const ttlMs = opts.ttlMs ?? DEFAULT_JUDGE_TTL_MS;
12336
12536
  const now = opts.now ?? (() => Date.now());
12337
12537
  function readAll() {
12338
- if (!existsSync13(cacheFile)) return {};
12538
+ if (!existsSync14(cacheFile)) return {};
12339
12539
  try {
12340
- return JSON.parse(readFileSync12(cacheFile, "utf8"));
12540
+ return JSON.parse(readFileSync13(cacheFile, "utf8"));
12341
12541
  } catch {
12342
12542
  return {};
12343
12543
  }
@@ -12357,36 +12557,36 @@ function makeJudgeCache(opts = {}) {
12357
12557
  }
12358
12558
  all[key] = { res, at: t };
12359
12559
  ensureDirFor3(cacheFile);
12360
- writeFileSync7(cacheFile, JSON.stringify(all), "utf8");
12560
+ writeFileSync8(cacheFile, JSON.stringify(all), "utf8");
12361
12561
  } catch {
12362
12562
  }
12363
12563
  }
12364
12564
  return { get, set };
12365
12565
  }
12366
12566
  function ensureDirFor3(filePath) {
12367
- const dir = dirname6(filePath);
12368
- if (!existsSync13(dir)) {
12369
- mkdirSync6(dir, { recursive: true });
12567
+ const dir = dirname7(filePath);
12568
+ if (!existsSync14(dir)) {
12569
+ mkdirSync7(dir, { recursive: true });
12370
12570
  }
12371
12571
  }
12372
12572
 
12373
12573
  // src/hooks/runtime/guard-check.ts
12374
- import { homedir as homedir12 } from "node:os";
12375
- import { join as join12 } from "node:path";
12574
+ import { homedir as homedir13 } from "node:os";
12575
+ import { join as join13 } from "node:path";
12376
12576
 
12377
12577
  // src/hooks/runtime/guard-violations-buffer.ts
12378
12578
  import {
12379
12579
  appendFileSync,
12380
- existsSync as existsSync14,
12381
- readFileSync as readFileSync13,
12382
- writeFileSync as writeFileSync8,
12383
- mkdirSync as mkdirSync7
12580
+ existsSync as existsSync15,
12581
+ readFileSync as readFileSync14,
12582
+ writeFileSync as writeFileSync9,
12583
+ mkdirSync as mkdirSync8
12384
12584
  } from "node:fs";
12385
- import { dirname as dirname7 } from "node:path";
12386
- import { homedir as homedir11 } from "node:os";
12387
- import { join as join11 } from "node:path";
12388
- var DEFAULT_BUFFER_FILE = join11(
12389
- homedir11(),
12585
+ import { dirname as dirname8 } from "node:path";
12586
+ import { homedir as homedir12 } from "node:os";
12587
+ import { join as join12 } from "node:path";
12588
+ var DEFAULT_BUFFER_FILE = join12(
12589
+ homedir12(),
12390
12590
  ".cybedefend",
12391
12591
  "guards-violations-buffer.jsonl"
12392
12592
  );
@@ -12405,10 +12605,10 @@ function makeViolationsBuffer(opts = {}) {
12405
12605
  }
12406
12606
  }
12407
12607
  async function flushViolations2(ctx, postFn) {
12408
- if (!existsSync14(bufferFile)) return;
12608
+ if (!existsSync15(bufferFile)) return;
12409
12609
  let content;
12410
12610
  try {
12411
- content = readFileSync13(bufferFile, "utf8").trim();
12611
+ content = readFileSync14(bufferFile, "utf8").trim();
12412
12612
  } catch {
12413
12613
  return;
12414
12614
  }
@@ -12427,7 +12627,7 @@ function makeViolationsBuffer(opts = {}) {
12427
12627
  }
12428
12628
  if (records.length === 0) {
12429
12629
  try {
12430
- writeFileSync8(bufferFile, "", "utf8");
12630
+ writeFileSync9(bufferFile, "", "utf8");
12431
12631
  } catch {
12432
12632
  }
12433
12633
  return;
@@ -12457,10 +12657,10 @@ function makeViolationsBuffer(opts = {}) {
12457
12657
  }
12458
12658
  try {
12459
12659
  if (failed.length === 0) {
12460
- writeFileSync8(bufferFile, "", "utf8");
12660
+ writeFileSync9(bufferFile, "", "utf8");
12461
12661
  } else {
12462
12662
  const serialised = failed.map((r) => JSON.stringify(r)).join("\n") + "\n";
12463
- writeFileSync8(bufferFile, serialised, "utf8");
12663
+ writeFileSync9(bufferFile, serialised, "utf8");
12464
12664
  }
12465
12665
  } catch {
12466
12666
  }
@@ -12468,9 +12668,9 @@ function makeViolationsBuffer(opts = {}) {
12468
12668
  return { bufferViolation: bufferViolation2, flushViolations: flushViolations2 };
12469
12669
  }
12470
12670
  function ensureDirFor4(filePath) {
12471
- const dir = dirname7(filePath);
12472
- if (!existsSync14(dir)) {
12473
- mkdirSync7(dir, { recursive: true });
12671
+ const dir = dirname8(filePath);
12672
+ if (!existsSync15(dir)) {
12673
+ mkdirSync8(dir, { recursive: true });
12474
12674
  }
12475
12675
  }
12476
12676
  var _defaultBuffer = makeViolationsBuffer();
@@ -12496,11 +12696,11 @@ function appendDebugLog(entry) {
12496
12696
  try {
12497
12697
  const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
12498
12698
  if (!homeDir) return;
12499
- const { mkdirSync: mkdirSync8, appendFileSync: appendFileSync2 } = __require("node:fs");
12500
- const { join: join13 } = __require("node:path");
12501
- const dir = join13(homeDir, ".cybedefend");
12502
- mkdirSync8(dir, { recursive: true });
12503
- appendFileSync2(join13(dir, "hook-debug.log"), entry + "\n", "utf8");
12699
+ const { mkdirSync: mkdirSync9, appendFileSync: appendFileSync2 } = __require("node:fs");
12700
+ const { join: join14 } = __require("node:path");
12701
+ const dir = join14(homeDir, ".cybedefend");
12702
+ mkdirSync9(dir, { recursive: true });
12703
+ appendFileSync2(join14(dir, "hook-debug.log"), entry + "\n", "utf8");
12504
12704
  } catch {
12505
12705
  }
12506
12706
  }
@@ -12671,6 +12871,28 @@ function stringifyTarget(t) {
12671
12871
  return `git ${t.subcommand}`;
12672
12872
  }
12673
12873
  }
12874
+ async function consultJudge(judge, sessionId, cacheKey, input) {
12875
+ const cached = judge.cacheGet(cacheKey);
12876
+ if (cached) return { kind: "result", res: cached };
12877
+ if (judge.usage?.isBlocked(sessionId)) return { kind: "blocked" };
12878
+ const answer = await judge.callJudge(input);
12879
+ if (isJudgeBlocked(answer)) {
12880
+ if (judge.usage) {
12881
+ judge.usage.markBlocked(sessionId);
12882
+ judge.usage.notifier.notifyBlock(sessionId, answer.blocked);
12883
+ } else {
12884
+ judge.warnOnce(sessionId, blockSentence(answer.blocked));
12885
+ }
12886
+ return { kind: "blocked" };
12887
+ }
12888
+ if (!answer) return { kind: "result", res: null };
12889
+ const { usageWarning, ...res } = answer;
12890
+ if (usageWarning) {
12891
+ judge.usage?.notifier.notifyWarning(sessionId, usageWarning);
12892
+ }
12893
+ judge.cacheSet(cacheKey, res);
12894
+ return { kind: "result", res };
12895
+ }
12674
12896
  async function runJudgeStep(args) {
12675
12897
  const {
12676
12898
  judge,
@@ -12699,30 +12921,30 @@ async function runJudgeStep(args) {
12699
12921
  const actionHash = hashTarget(targetStr);
12700
12922
  const ruleIds = llmRules.map((r) => r.id).sort().join(",");
12701
12923
  const cacheKey = `${actionHash}|${ruleIds}|confirm`;
12702
- let res = judge.cacheGet(cacheKey);
12703
- if (!res) {
12704
- res = await judge.callJudge({
12705
- apiBaseResolved: projectContext.apiBaseResolved,
12706
- token: projectContext.token,
12707
- projectId: request.projectId,
12708
- tool: request.tool,
12709
- agent: request.agent,
12710
- sessionId,
12711
- phase: "confirm",
12712
- // Judge over the FULL (schema-bounded 2048) redacted action — the default
12713
- // redactTarget cap of 256 (right for the DB-stored violation summary) would
12714
- // truncate a long dangerous action and risk an overturn on a partial view.
12715
- actionSummary: redactTarget(targetStr, 2048),
12716
- actionHash,
12717
- rules: llmRules.map((r) => ({
12718
- id: r.id,
12719
- name: r.name ?? r.id,
12720
- description: r.description ?? "",
12721
- severity: r.severity
12722
- }))
12723
- });
12724
- if (res) judge.cacheSet(cacheKey, res);
12924
+ const consultation = await consultJudge(judge, sessionId, cacheKey, {
12925
+ apiBaseResolved: projectContext.apiBaseResolved,
12926
+ token: projectContext.token,
12927
+ projectId: request.projectId,
12928
+ tool: request.tool,
12929
+ agent: request.agent,
12930
+ sessionId,
12931
+ phase: "confirm",
12932
+ // Judge over the FULL (schema-bounded 2048) redacted action — the default
12933
+ // redactTarget cap of 256 (right for the DB-stored violation summary) would
12934
+ // truncate a long dangerous action and risk an overturn on a partial view.
12935
+ actionSummary: redactTarget(targetStr, 2048),
12936
+ actionHash,
12937
+ rules: llmRules.map((r) => ({
12938
+ id: r.id,
12939
+ name: r.name ?? r.id,
12940
+ description: r.description ?? "",
12941
+ severity: r.severity
12942
+ }))
12943
+ });
12944
+ if (consultation.kind === "blocked") {
12945
+ return { engine: "llm", outcome: "quota_exceeded" };
12725
12946
  }
12947
+ const res = consultation.res;
12726
12948
  if (!res) {
12727
12949
  return {
12728
12950
  engine: "llm",
@@ -12785,27 +13007,25 @@ async function runCatchJudgeStep(args) {
12785
13007
  const actionHash = hashTarget(targetStr);
12786
13008
  const ruleIds = near.map((r) => r.id).sort();
12787
13009
  const cacheKey = `${actionHash}|${ruleIds.join(",")}|catch`;
12788
- let res = judge.cacheGet(cacheKey);
12789
- if (!res) {
12790
- res = await judge.callJudge({
12791
- apiBaseResolved: projectContext.apiBaseResolved,
12792
- token: projectContext.token,
12793
- projectId: request.projectId,
12794
- tool: request.tool,
12795
- agent: request.agent,
12796
- sessionId,
12797
- phase: "catch",
12798
- actionSummary: redactTarget(targetStr, 2048),
12799
- actionHash,
12800
- rules: near.map((r) => ({
12801
- id: r.id,
12802
- name: r.name ?? r.id,
12803
- description: r.description ?? "",
12804
- severity: r.severity
12805
- }))
12806
- });
12807
- if (res) judge.cacheSet(cacheKey, res);
12808
- }
13010
+ const consultation = await consultJudge(judge, sessionId, cacheKey, {
13011
+ apiBaseResolved: projectContext.apiBaseResolved,
13012
+ token: projectContext.token,
13013
+ projectId: request.projectId,
13014
+ tool: request.tool,
13015
+ agent: request.agent,
13016
+ sessionId,
13017
+ phase: "catch",
13018
+ actionSummary: redactTarget(targetStr, 2048),
13019
+ actionHash,
13020
+ rules: near.map((r) => ({
13021
+ id: r.id,
13022
+ name: r.name ?? r.id,
13023
+ description: r.description ?? "",
13024
+ severity: r.severity
13025
+ }))
13026
+ });
13027
+ if (consultation.kind === "blocked") return void 0;
13028
+ const res = consultation.res;
12809
13029
  if (!res) return void 0;
12810
13030
  if (res.quotaExceeded) {
12811
13031
  judge.warnOnce(
@@ -12851,6 +13071,9 @@ function emitUnverifiedBanner(reason) {
12851
13071
  }
12852
13072
  }
12853
13073
  }
13074
+ function sessionIdOf2(envelope) {
13075
+ return typeof envelope.session_id === "string" ? envelope.session_id : typeof envelope.sessionId === "string" ? envelope.sessionId : void 0;
13076
+ }
12854
13077
  async function runPreToolUseGuardWith(opts) {
12855
13078
  const {
12856
13079
  stdinEnvelope,
@@ -12866,11 +13089,19 @@ async function runPreToolUseGuardWith(opts) {
12866
13089
  );
12867
13090
  return 0;
12868
13091
  }
13092
+ if (rulesOutcome.kind === "blocked") {
13093
+ const sentence = opts.usageNotifier?.notifyBlock(
13094
+ sessionIdOf2(stdinEnvelope),
13095
+ rulesOutcome.block
13096
+ );
13097
+ if (sentence) emitAgentMessage("warn", sentence);
13098
+ return 0;
13099
+ }
12869
13100
  const rules = rulesOutcome.rules;
12870
13101
  const caller = sniff(stdinEnvelope);
12871
13102
  const partial = mapEnvelopeToGuardRequest(stdinEnvelope, caller.client);
12872
13103
  if (partial === null) return 0;
12873
- const sessionId = typeof stdinEnvelope.session_id === "string" ? stdinEnvelope.session_id : typeof stdinEnvelope.sessionId === "string" ? stdinEnvelope.sessionId : void 0;
13104
+ const sessionId = sessionIdOf2(stdinEnvelope);
12874
13105
  const guardRequest = {
12875
13106
  ...partial,
12876
13107
  projectId: projectContext.projectId,
@@ -13014,11 +13245,21 @@ function emitAgentMessage(verdict, message) {
13014
13245
  }
13015
13246
  function makeProductionJudgeDeps() {
13016
13247
  const cache = makeJudgeCache();
13017
- const warnDir = process.env.CYBEDEFEND_BASELINE_DIR ?? join12(homedir12(), ".cybedefend");
13248
+ const warnDir = process.env.CYBEDEFEND_BASELINE_DIR ?? join13(homedir13(), ".cybedefend");
13018
13249
  const warnTracker = defaultSessionTracker(
13019
- join12(warnDir, "guards-judge-warned-sessions.json")
13250
+ join13(warnDir, "guards-judge-warned-sessions.json")
13251
+ );
13252
+ const blockedTracker = defaultSessionTracker(
13253
+ join13(warnDir, "guards-judge-blocked-sessions.json")
13020
13254
  );
13021
13255
  return {
13256
+ usage: {
13257
+ notifier: makeUsageNotifier(),
13258
+ isBlocked: (sessionId) => sessionId ? blockedTracker.hasSeen(sessionId) : false,
13259
+ markBlocked: (sessionId) => {
13260
+ if (sessionId) void blockedTracker.markSeen(sessionId);
13261
+ }
13262
+ },
13022
13263
  callJudge,
13023
13264
  cacheGet: (k) => cache.get(k),
13024
13265
  cacheSet: (k, r) => cache.set(k, r),
@@ -13076,7 +13317,10 @@ async function runPreToolUseGuard() {
13076
13317
  judge: makeProductionJudgeDeps(),
13077
13318
  // v1.1 catch — OFF by default; opt in with CYBEDEFEND_JUDGE_CATCH=1 once
13078
13319
  // the call-rate gate is validated in the field.
13079
- judgeCatch: process.env.CYBEDEFEND_JUDGE_CATCH === "1"
13320
+ judgeCatch: process.env.CYBEDEFEND_JUDGE_CATCH === "1",
13321
+ // The refusal sentence goes out through the agent-message channels
13322
+ // (stderr included), so this notifier itself writes nothing.
13323
+ usageNotifier: makeUsageNotifier({ write: () => void 0 })
13080
13324
  });
13081
13325
  try {
13082
13326
  await buffer.flushViolations(