@node9/proxy 1.66.0 → 1.67.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -376,6 +376,16 @@ var init_config_schema = __esm({
376
376
  pii: import_zod.z.enum(["off", "block"]).optional(),
377
377
  reviewAction: import_zod.z.enum(["review", "block"]).optional()
378
378
  }).optional(),
379
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
380
+ // deliberately exclude 'off' — tighten-only.
381
+ commandChecks: import_zod.z.object({
382
+ inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
383
+ rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
384
+ chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
385
+ sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
386
+ evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
387
+ pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
388
+ }).optional(),
379
389
  egress: import_zod.z.object({
380
390
  enabled: import_zod.z.boolean().optional(),
381
391
  mode: import_zod.z.enum(["off", "review", "block"]).optional(),
@@ -1513,6 +1523,45 @@ function evaluateSmartConditions(args, rule) {
1513
1523
  });
1514
1524
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1515
1525
  }
1526
+ function resolveCheck(v) {
1527
+ return v === "off" || v === "block" ? v : "review";
1528
+ }
1529
+ function resolveCheckTight(v) {
1530
+ return v === "block" ? "block" : "review";
1531
+ }
1532
+ function detectInlineExec(command) {
1533
+ const pipeFed = command.includes("|");
1534
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
1535
+ for (const rawSeg of segments) {
1536
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
1537
+ let i = 0;
1538
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
1539
+ if (i >= tokens.length) continue;
1540
+ const base = tokens[i].split("/").pop() ?? tokens[i];
1541
+ if (!INLINE_INTERP.test(base)) continue;
1542
+ const args = tokens.slice(i + 1);
1543
+ let hadRedirect = false;
1544
+ const positionals = [];
1545
+ for (let j = 0; j < args.length; j++) {
1546
+ const a = args[j];
1547
+ if (a === "-") return true;
1548
+ if (a.startsWith("<")) {
1549
+ hadRedirect = true;
1550
+ if (a === "<" || a === "<<") j++;
1551
+ continue;
1552
+ }
1553
+ if (a.startsWith("-")) {
1554
+ if (/^-(c|e|eval)$/i.test(a)) return true;
1555
+ continue;
1556
+ }
1557
+ positionals.push(a);
1558
+ }
1559
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
1560
+ return true;
1561
+ }
1562
+ }
1563
+ return false;
1564
+ }
1516
1565
  function resolvePinned(matches) {
1517
1566
  if (matches.length === 0) return void 0;
1518
1567
  const pinned = matches.filter((r) => r.pinned);
@@ -1539,7 +1588,7 @@ function isSqlTool(toolName, toolInspection) {
1539
1588
  const fieldName = toolInspection[matchingPattern];
1540
1589
  return fieldName === "sql" || fieldName === "query";
1541
1590
  }
1542
- function pipeChainVerdict(command, isTrustedHost2) {
1591
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
1543
1592
  const pipeAnalysis = analyzePipeChain(command);
1544
1593
  if (!pipeAnalysis.isPipeline) return null;
1545
1594
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -1570,7 +1619,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
1570
1619
  };
1571
1620
  }
1572
1621
  return {
1573
- decision: "review",
1622
+ decision: highAction,
1574
1623
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1575
1624
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1576
1625
  tier: 3
@@ -1596,7 +1645,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1596
1645
  if (wouldBeIgnored) return { decision: "allow" };
1597
1646
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1598
1647
  if (bashCommand !== null) {
1599
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1648
+ const pipeVerdict = pipeChainVerdict(
1649
+ bashCommand,
1650
+ isTrustedHost2,
1651
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1652
+ );
1600
1653
  if (pipeVerdict) return pipeVerdict;
1601
1654
  const fsVerdict = analyzeFsOperation(bashCommand);
1602
1655
  if (fsVerdict) {
@@ -1611,10 +1664,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1611
1664
  ruleDescription: fsVerdict.reason
1612
1665
  };
1613
1666
  }
1614
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
1667
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
1668
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
1615
1669
  if (sqlVerdict) {
1616
1670
  return {
1617
- decision: sqlVerdict.verdict,
1671
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
1672
+ decision: sqlAction === "block" ? "block" : "review",
1618
1673
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
1619
1674
  reason: sqlVerdict.reason,
1620
1675
  tier: 2,
@@ -1622,10 +1677,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1622
1677
  ruleDescription: sqlVerdict.description
1623
1678
  };
1624
1679
  }
1625
- const chmodVerdict = analyzeChmod777(bashCommand);
1680
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
1681
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
1626
1682
  if (chmodVerdict) {
1627
1683
  return {
1628
- decision: chmodVerdict.verdict,
1684
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
1685
+ decision: chmodAction === "block" ? "block" : "review",
1629
1686
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
1630
1687
  reason: chmodVerdict.reason,
1631
1688
  tier: 2,
@@ -1668,10 +1725,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1668
1725
  const analyzed = analyzeShellCommand(shellCommand);
1669
1726
  allTokens = analyzed.allTokens;
1670
1727
  pathTokens = analyzed.paths;
1671
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1672
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1728
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
1729
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
1673
1730
  return {
1674
- decision: "review",
1731
+ decision: inlineAction === "block" ? "block" : "review",
1675
1732
  blockedByLabel: "Node9 Standard (Inline Execution)",
1676
1733
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
1677
1734
  tier: 3
@@ -1689,14 +1746,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1689
1746
  }
1690
1747
  if (evalVerdict === "review") {
1691
1748
  return {
1692
- decision: "review",
1749
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
1750
+ // block but can never turn this off (eval-remote above is Class A —
1751
+ // no knob at all).
1752
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
1693
1753
  blockedByLabel: "Node9: Eval Dynamic Content",
1694
1754
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
1695
1755
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
1696
1756
  tier: 3
1697
1757
  };
1698
1758
  }
1699
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1759
+ const ptVerdict = pipeChainVerdict(
1760
+ shellCommand,
1761
+ isTrustedHost2,
1762
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1763
+ );
1700
1764
  if (ptVerdict) return ptVerdict;
1701
1765
  if (config.policy.egress?.enabled) {
1702
1766
  const dests = extractShellDestinations(shellCommand);
@@ -2378,7 +2442,7 @@ function* stringValues(obj, depth = 0) {
2378
2442
  }
2379
2443
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2380
2444
  }
2381
- var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2445
+ var import_safe_regex2, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto3, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, INLINE_INTERP, INLINE_SHELL, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2382
2446
  var init_dist = __esm({
2383
2447
  "packages/policy-engine/dist/index.mjs"() {
2384
2448
  "use strict";
@@ -3315,6 +3379,8 @@ var init_dist = __esm({
3315
3379
  REGEX_CACHE_MAX = 500;
3316
3380
  regexCache = /* @__PURE__ */ new Map();
3317
3381
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3382
+ INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
3383
+ INLINE_SHELL = /^(bash|sh|zsh)$/i;
3318
3384
  VERDICT_RANK = {
3319
3385
  allow: 0,
3320
3386
  review: 1,
@@ -4382,6 +4448,24 @@ function applyManagedDlp(local, managed, locked) {
4382
4448
  }
4383
4449
  return next;
4384
4450
  }
4451
+ function applyManagedCommandChecks(local, managed, locked) {
4452
+ const next = { ...local };
4453
+ for (const key of COMMAND_CHECK_KEYS) {
4454
+ const m = managed[key];
4455
+ if (typeof m !== "string") continue;
4456
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4457
+ const localValue = local[key];
4458
+ const resolved = localValue === void 0 ? m : resolveByOrder(
4459
+ COMMAND_CHECK_ORDER,
4460
+ localValue,
4461
+ m,
4462
+ locked.includes(lockKey)
4463
+ );
4464
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4465
+ next[key] = resolved;
4466
+ }
4467
+ return next;
4468
+ }
4385
4469
  function applyManagedApprovers(local, managed) {
4386
4470
  return {
4387
4471
  ...local,
@@ -4391,7 +4475,7 @@ function applyManagedApprovers(local, managed) {
4391
4475
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4392
4476
  };
4393
4477
  }
4394
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4478
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4395
4479
  var init_managed = __esm({
4396
4480
  "src/config/managed.ts"() {
4397
4481
  "use strict";
@@ -4399,6 +4483,15 @@ var init_managed = __esm({
4399
4483
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4400
4484
  DLP_PII_ORDER = ["off", "block"];
4401
4485
  DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4486
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4487
+ COMMAND_CHECK_KEYS = [
4488
+ "inlineExec",
4489
+ "rmAdvisory",
4490
+ "chmod",
4491
+ "sqlDdl",
4492
+ "evalDynamic",
4493
+ "pipeChainHigh"
4494
+ ];
4402
4495
  }
4403
4496
  });
4404
4497
 
@@ -4775,6 +4868,21 @@ function getConfig(cwd) {
4775
4868
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4776
4869
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4777
4870
  }
4871
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4872
+ const src = p.commandChecks;
4873
+ const cc2 = {
4874
+ ...mergedPolicy.commandChecks
4875
+ };
4876
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4877
+ const v = src[k];
4878
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4879
+ }
4880
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4881
+ const v = src[k];
4882
+ if (v === "review" || v === "block") cc2[k] = v;
4883
+ }
4884
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4885
+ }
4778
4886
  if (p.egress) {
4779
4887
  const e = p.egress;
4780
4888
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4876,6 +4984,13 @@ function getConfig(cwd) {
4876
4984
  locked
4877
4985
  );
4878
4986
  }
4987
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4988
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4989
+ mergedPolicy.commandChecks ?? {},
4990
+ mc.commandChecks,
4991
+ locked
4992
+ );
4993
+ }
4879
4994
  if (mc.approvers && typeof mc.approvers === "object") {
4880
4995
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4881
4996
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4985,8 +5100,17 @@ function getConfig(cwd) {
4985
5100
  }
4986
5101
  }
4987
5102
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5103
+ const cc = mergedPolicy.commandChecks ?? {};
5104
+ const advisoryKnob = (name) => {
5105
+ if (name === "review-rm") return cc.rmAdvisory;
5106
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5107
+ return void 0;
5108
+ };
4988
5109
  for (const rule of ADVISORY_SMART_RULES) {
4989
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5110
+ if (existingAdvisoryNames.has(rule.name)) continue;
5111
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5112
+ if (knob === "off") continue;
5113
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4990
5114
  }
4991
5115
  const envMode = process.env.NODE9_MODE;
4992
5116
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -18378,6 +18502,18 @@ function extractManagedConfig(body) {
18378
18502
  d.reviewAction = mc.dlp.reviewAction;
18379
18503
  if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18380
18504
  }
18505
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18506
+ const cc = {};
18507
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18508
+ const v = mc.commandChecks[k];
18509
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18510
+ }
18511
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18512
+ const v = mc.commandChecks[k];
18513
+ if (v === "review" || v === "block") cc[k] = v;
18514
+ }
18515
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18516
+ }
18381
18517
  if (mc.approvers && typeof mc.approvers === "object") {
18382
18518
  const a = {};
18383
18519
  for (const k of ["native", "browser", "cloud", "terminal"]) {
@@ -18440,7 +18576,7 @@ function extractManagedConfig(body) {
18440
18576
  }
18441
18577
  if (Object.keys(ap).length) out.appPermissions = ap;
18442
18578
  }
18443
- return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18579
+ return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.commandChecks !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18444
18580
  }
18445
18581
  function sweepStaleTmp(target) {
18446
18582
  try {
@@ -48808,6 +48944,7 @@ init_audit();
48808
48944
  init_config();
48809
48945
  init_daemon();
48810
48946
  init_dlp();
48947
+ init_hasher();
48811
48948
 
48812
48949
  // src/utils/cp-mv-parser.ts
48813
48950
  function parseCpMvOp(command) {
@@ -48869,6 +49006,20 @@ function atLeastConfidence(c, min) {
48869
49006
  function sanitize3(value) {
48870
49007
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48871
49008
  }
49009
+ function buildArgsField(rawInput) {
49010
+ let hashed = {};
49011
+ try {
49012
+ hashed = { argsHash: hashArgs(rawInput) };
49013
+ } catch {
49014
+ }
49015
+ try {
49016
+ const hit = scanArgs(rawInput);
49017
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49018
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49019
+ } catch {
49020
+ return hashed;
49021
+ }
49022
+ }
48872
49023
  function registerLogCommand(program2) {
48873
49024
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48874
49025
  "--agent <name>",
@@ -48907,7 +49058,7 @@ function registerLogCommand(program2) {
48907
49058
  const entry = {
48908
49059
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48909
49060
  tool,
48910
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49061
+ ...buildArgsField(rawInput),
48911
49062
  decision: "allowed",
48912
49063
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48913
49064
  };
package/dist/cli.mjs CHANGED
@@ -380,6 +380,16 @@ var init_config_schema = __esm({
380
380
  pii: z.enum(["off", "block"]).optional(),
381
381
  reviewAction: z.enum(["review", "block"]).optional()
382
382
  }).optional(),
383
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
384
+ // deliberately exclude 'off' — tighten-only.
385
+ commandChecks: z.object({
386
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
387
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
388
+ chmod: z.enum(["off", "review", "block"]).optional(),
389
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
390
+ evalDynamic: z.enum(["review", "block"]).optional(),
391
+ pipeChainHigh: z.enum(["review", "block"]).optional()
392
+ }).optional(),
383
393
  egress: z.object({
384
394
  enabled: z.boolean().optional(),
385
395
  mode: z.enum(["off", "review", "block"]).optional(),
@@ -1523,6 +1533,45 @@ function evaluateSmartConditions(args, rule) {
1523
1533
  });
1524
1534
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
1525
1535
  }
1536
+ function resolveCheck(v) {
1537
+ return v === "off" || v === "block" ? v : "review";
1538
+ }
1539
+ function resolveCheckTight(v) {
1540
+ return v === "block" ? "block" : "review";
1541
+ }
1542
+ function detectInlineExec(command) {
1543
+ const pipeFed = command.includes("|");
1544
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
1545
+ for (const rawSeg of segments) {
1546
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
1547
+ let i = 0;
1548
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
1549
+ if (i >= tokens.length) continue;
1550
+ const base = tokens[i].split("/").pop() ?? tokens[i];
1551
+ if (!INLINE_INTERP.test(base)) continue;
1552
+ const args = tokens.slice(i + 1);
1553
+ let hadRedirect = false;
1554
+ const positionals = [];
1555
+ for (let j = 0; j < args.length; j++) {
1556
+ const a = args[j];
1557
+ if (a === "-") return true;
1558
+ if (a.startsWith("<")) {
1559
+ hadRedirect = true;
1560
+ if (a === "<" || a === "<<") j++;
1561
+ continue;
1562
+ }
1563
+ if (a.startsWith("-")) {
1564
+ if (/^-(c|e|eval)$/i.test(a)) return true;
1565
+ continue;
1566
+ }
1567
+ positionals.push(a);
1568
+ }
1569
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
1570
+ return true;
1571
+ }
1572
+ }
1573
+ return false;
1574
+ }
1526
1575
  function resolvePinned(matches) {
1527
1576
  if (matches.length === 0) return void 0;
1528
1577
  const pinned = matches.filter((r) => r.pinned);
@@ -1549,7 +1598,7 @@ function isSqlTool(toolName, toolInspection) {
1549
1598
  const fieldName = toolInspection[matchingPattern];
1550
1599
  return fieldName === "sql" || fieldName === "query";
1551
1600
  }
1552
- function pipeChainVerdict(command, isTrustedHost2) {
1601
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
1553
1602
  const pipeAnalysis = analyzePipeChain(command);
1554
1603
  if (!pipeAnalysis.isPipeline) return null;
1555
1604
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -1580,7 +1629,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
1580
1629
  };
1581
1630
  }
1582
1631
  return {
1583
- decision: "review",
1632
+ decision: highAction,
1584
1633
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
1585
1634
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
1586
1635
  tier: 3
@@ -1606,7 +1655,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1606
1655
  if (wouldBeIgnored) return { decision: "allow" };
1607
1656
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
1608
1657
  if (bashCommand !== null) {
1609
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
1658
+ const pipeVerdict = pipeChainVerdict(
1659
+ bashCommand,
1660
+ isTrustedHost2,
1661
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1662
+ );
1610
1663
  if (pipeVerdict) return pipeVerdict;
1611
1664
  const fsVerdict = analyzeFsOperation(bashCommand);
1612
1665
  if (fsVerdict) {
@@ -1621,10 +1674,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1621
1674
  ruleDescription: fsVerdict.reason
1622
1675
  };
1623
1676
  }
1624
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
1677
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
1678
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
1625
1679
  if (sqlVerdict) {
1626
1680
  return {
1627
- decision: sqlVerdict.verdict,
1681
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
1682
+ decision: sqlAction === "block" ? "block" : "review",
1628
1683
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
1629
1684
  reason: sqlVerdict.reason,
1630
1685
  tier: 2,
@@ -1632,10 +1687,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1632
1687
  ruleDescription: sqlVerdict.description
1633
1688
  };
1634
1689
  }
1635
- const chmodVerdict = analyzeChmod777(bashCommand);
1690
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
1691
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
1636
1692
  if (chmodVerdict) {
1637
1693
  return {
1638
- decision: chmodVerdict.verdict,
1694
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
1695
+ decision: chmodAction === "block" ? "block" : "review",
1639
1696
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
1640
1697
  reason: chmodVerdict.reason,
1641
1698
  tier: 2,
@@ -1678,10 +1735,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1678
1735
  const analyzed = analyzeShellCommand(shellCommand);
1679
1736
  allTokens = analyzed.allTokens;
1680
1737
  pathTokens = analyzed.paths;
1681
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
1682
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
1738
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
1739
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
1683
1740
  return {
1684
- decision: "review",
1741
+ decision: inlineAction === "block" ? "block" : "review",
1685
1742
  blockedByLabel: "Node9 Standard (Inline Execution)",
1686
1743
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
1687
1744
  tier: 3
@@ -1699,14 +1756,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
1699
1756
  }
1700
1757
  if (evalVerdict === "review") {
1701
1758
  return {
1702
- decision: "review",
1759
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
1760
+ // block but can never turn this off (eval-remote above is Class A —
1761
+ // no knob at all).
1762
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
1703
1763
  blockedByLabel: "Node9: Eval Dynamic Content",
1704
1764
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
1705
1765
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
1706
1766
  tier: 3
1707
1767
  };
1708
1768
  }
1709
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
1769
+ const ptVerdict = pipeChainVerdict(
1770
+ shellCommand,
1771
+ isTrustedHost2,
1772
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
1773
+ );
1710
1774
  if (ptVerdict) return ptVerdict;
1711
1775
  if (config.policy.egress?.enabled) {
1712
1776
  const dests = extractShellDestinations(shellCommand);
@@ -2388,7 +2452,7 @@ function* stringValues(obj, depth = 0) {
2388
2452
  }
2389
2453
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
2390
2454
  }
2391
- var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2455
+ var MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, INLINE_INTERP, INLINE_SHELL, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, COST_PER_LOOP_ITER_USD, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
2392
2456
  var init_dist = __esm({
2393
2457
  "packages/policy-engine/dist/index.mjs"() {
2394
2458
  "use strict";
@@ -3319,6 +3383,8 @@ var init_dist = __esm({
3319
3383
  REGEX_CACHE_MAX = 500;
3320
3384
  regexCache = /* @__PURE__ */ new Map();
3321
3385
  FORBIDDEN_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3386
+ INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
3387
+ INLINE_SHELL = /^(bash|sh|zsh)$/i;
3322
3388
  VERDICT_RANK = {
3323
3389
  allow: 0,
3324
3390
  review: 1,
@@ -4386,6 +4452,24 @@ function applyManagedDlp(local, managed, locked) {
4386
4452
  }
4387
4453
  return next;
4388
4454
  }
4455
+ function applyManagedCommandChecks(local, managed, locked) {
4456
+ const next = { ...local };
4457
+ for (const key of COMMAND_CHECK_KEYS) {
4458
+ const m = managed[key];
4459
+ if (typeof m !== "string") continue;
4460
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
4461
+ const localValue = local[key];
4462
+ const resolved = localValue === void 0 ? m : resolveByOrder(
4463
+ COMMAND_CHECK_ORDER,
4464
+ localValue,
4465
+ m,
4466
+ locked.includes(lockKey)
4467
+ );
4468
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4469
+ next[key] = resolved;
4470
+ }
4471
+ return next;
4472
+ }
4389
4473
  function applyManagedApprovers(local, managed) {
4390
4474
  return {
4391
4475
  ...local,
@@ -4395,7 +4479,7 @@ function applyManagedApprovers(local, managed) {
4395
4479
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4396
4480
  };
4397
4481
  }
4398
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4482
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4399
4483
  var init_managed = __esm({
4400
4484
  "src/config/managed.ts"() {
4401
4485
  "use strict";
@@ -4403,6 +4487,15 @@ var init_managed = __esm({
4403
4487
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4404
4488
  DLP_PII_ORDER = ["off", "block"];
4405
4489
  DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4490
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4491
+ COMMAND_CHECK_KEYS = [
4492
+ "inlineExec",
4493
+ "rmAdvisory",
4494
+ "chmod",
4495
+ "sqlDdl",
4496
+ "evalDynamic",
4497
+ "pipeChainHigh"
4498
+ ];
4406
4499
  }
4407
4500
  });
4408
4501
 
@@ -4782,6 +4875,21 @@ function getConfig(cwd) {
4782
4875
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4783
4876
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4784
4877
  }
4878
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4879
+ const src = p.commandChecks;
4880
+ const cc2 = {
4881
+ ...mergedPolicy.commandChecks
4882
+ };
4883
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4884
+ const v = src[k];
4885
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4886
+ }
4887
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4888
+ const v = src[k];
4889
+ if (v === "review" || v === "block") cc2[k] = v;
4890
+ }
4891
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4892
+ }
4785
4893
  if (p.egress) {
4786
4894
  const e = p.egress;
4787
4895
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4883,6 +4991,13 @@ function getConfig(cwd) {
4883
4991
  locked
4884
4992
  );
4885
4993
  }
4994
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4995
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4996
+ mergedPolicy.commandChecks ?? {},
4997
+ mc.commandChecks,
4998
+ locked
4999
+ );
5000
+ }
4886
5001
  if (mc.approvers && typeof mc.approvers === "object") {
4887
5002
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4888
5003
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4992,8 +5107,17 @@ function getConfig(cwd) {
4992
5107
  }
4993
5108
  }
4994
5109
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5110
+ const cc = mergedPolicy.commandChecks ?? {};
5111
+ const advisoryKnob = (name) => {
5112
+ if (name === "review-rm") return cc.rmAdvisory;
5113
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5114
+ return void 0;
5115
+ };
4995
5116
  for (const rule of ADVISORY_SMART_RULES) {
4996
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5117
+ if (existingAdvisoryNames.has(rule.name)) continue;
5118
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5119
+ if (knob === "off") continue;
5120
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4997
5121
  }
4998
5122
  const envMode = process.env.NODE9_MODE;
4999
5123
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -18376,6 +18500,18 @@ function extractManagedConfig(body) {
18376
18500
  d.reviewAction = mc.dlp.reviewAction;
18377
18501
  if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18378
18502
  }
18503
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18504
+ const cc = {};
18505
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18506
+ const v = mc.commandChecks[k];
18507
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18508
+ }
18509
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18510
+ const v = mc.commandChecks[k];
18511
+ if (v === "review" || v === "block") cc[k] = v;
18512
+ }
18513
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18514
+ }
18379
18515
  if (mc.approvers && typeof mc.approvers === "object") {
18380
18516
  const a = {};
18381
18517
  for (const k of ["native", "browser", "cloud", "terminal"]) {
@@ -18438,7 +18574,7 @@ function extractManagedConfig(body) {
18438
18574
  }
18439
18575
  if (Object.keys(ap).length) out.appPermissions = ap;
18440
18576
  }
18441
- return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18577
+ return out.mode !== void 0 || out.egress !== void 0 || out.dlp !== void 0 || out.commandChecks !== void 0 || out.approvers !== void 0 || out.reviewChannel !== void 0 || out.approvalTimeoutMs !== void 0 || out.injectionScan !== void 0 || out.loopDetection !== void 0 || out.skillPinning !== void 0 || out.jailPaths !== void 0 || out.trustedHosts !== void 0 || out.appPermissions !== void 0 ? out : void 0;
18442
18578
  }
18443
18579
  function sweepStaleTmp(target) {
18444
18580
  try {
@@ -48801,6 +48937,7 @@ import path48 from "path";
48801
48937
  import os45 from "os";
48802
48938
  init_daemon();
48803
48939
  init_dlp();
48940
+ init_hasher();
48804
48941
 
48805
48942
  // src/utils/cp-mv-parser.ts
48806
48943
  function parseCpMvOp(command) {
@@ -48862,6 +48999,20 @@ function atLeastConfidence(c, min) {
48862
48999
  function sanitize3(value) {
48863
49000
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48864
49001
  }
49002
+ function buildArgsField(rawInput) {
49003
+ let hashed = {};
49004
+ try {
49005
+ hashed = { argsHash: hashArgs(rawInput) };
49006
+ } catch {
49007
+ }
49008
+ try {
49009
+ const hit = scanArgs(rawInput);
49010
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49011
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49012
+ } catch {
49013
+ return hashed;
49014
+ }
49015
+ }
48865
49016
  function registerLogCommand(program2) {
48866
49017
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48867
49018
  "--agent <name>",
@@ -48900,7 +49051,7 @@ function registerLogCommand(program2) {
48900
49051
  const entry = {
48901
49052
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48902
49053
  tool,
48903
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49054
+ ...buildArgsField(rawInput),
48904
49055
  decision: "allowed",
48905
49056
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48906
49057
  };
@@ -2336,6 +2336,16 @@ var init_config_schema = __esm({
2336
2336
  pii: z.enum(["off", "block"]).optional(),
2337
2337
  reviewAction: z.enum(["review", "block"]).optional()
2338
2338
  }).optional(),
2339
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
2340
+ // deliberately exclude 'off' — tighten-only.
2341
+ commandChecks: z.object({
2342
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
2343
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
2344
+ chmod: z.enum(["off", "review", "block"]).optional(),
2345
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
2346
+ evalDynamic: z.enum(["review", "block"]).optional(),
2347
+ pipeChainHigh: z.enum(["review", "block"]).optional()
2348
+ }).optional(),
2339
2349
  egress: z.object({
2340
2350
  enabled: z.boolean().optional(),
2341
2351
  mode: z.enum(["off", "review", "block"]).optional(),
package/dist/index.js CHANGED
@@ -358,6 +358,16 @@ var ConfigFileSchema = import_zod.z.object({
358
358
  pii: import_zod.z.enum(["off", "block"]).optional(),
359
359
  reviewAction: import_zod.z.enum(["review", "block"]).optional()
360
360
  }).optional(),
361
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
362
+ // deliberately exclude 'off' — tighten-only.
363
+ commandChecks: import_zod.z.object({
364
+ inlineExec: import_zod.z.enum(["off", "review", "block"]).optional(),
365
+ rmAdvisory: import_zod.z.enum(["off", "review", "block"]).optional(),
366
+ chmod: import_zod.z.enum(["off", "review", "block"]).optional(),
367
+ sqlDdl: import_zod.z.enum(["off", "review", "block"]).optional(),
368
+ evalDynamic: import_zod.z.enum(["review", "block"]).optional(),
369
+ pipeChainHigh: import_zod.z.enum(["review", "block"]).optional()
370
+ }).optional(),
361
371
  egress: import_zod.z.object({
362
372
  enabled: import_zod.z.boolean().optional(),
363
373
  mode: import_zod.z.enum(["off", "review", "block"]).optional(),
@@ -2368,6 +2378,47 @@ function evaluateSmartConditions(args, rule) {
2368
2378
  });
2369
2379
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2370
2380
  }
2381
+ function resolveCheck(v) {
2382
+ return v === "off" || v === "block" ? v : "review";
2383
+ }
2384
+ function resolveCheckTight(v) {
2385
+ return v === "block" ? "block" : "review";
2386
+ }
2387
+ var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2388
+ var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2389
+ function detectInlineExec(command) {
2390
+ const pipeFed = command.includes("|");
2391
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2392
+ for (const rawSeg of segments) {
2393
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2394
+ let i = 0;
2395
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2396
+ if (i >= tokens.length) continue;
2397
+ const base = tokens[i].split("/").pop() ?? tokens[i];
2398
+ if (!INLINE_INTERP.test(base)) continue;
2399
+ const args = tokens.slice(i + 1);
2400
+ let hadRedirect = false;
2401
+ const positionals = [];
2402
+ for (let j = 0; j < args.length; j++) {
2403
+ const a = args[j];
2404
+ if (a === "-") return true;
2405
+ if (a.startsWith("<")) {
2406
+ hadRedirect = true;
2407
+ if (a === "<" || a === "<<") j++;
2408
+ continue;
2409
+ }
2410
+ if (a.startsWith("-")) {
2411
+ if (/^-(c|e|eval)$/i.test(a)) return true;
2412
+ continue;
2413
+ }
2414
+ positionals.push(a);
2415
+ }
2416
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2417
+ return true;
2418
+ }
2419
+ }
2420
+ return false;
2421
+ }
2371
2422
  var VERDICT_RANK = {
2372
2423
  allow: 0,
2373
2424
  review: 1,
@@ -2400,7 +2451,7 @@ function isSqlTool(toolName, toolInspection) {
2400
2451
  return fieldName === "sql" || fieldName === "query";
2401
2452
  }
2402
2453
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2403
- function pipeChainVerdict(command, isTrustedHost2) {
2454
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2404
2455
  const pipeAnalysis = analyzePipeChain(command);
2405
2456
  if (!pipeAnalysis.isPipeline) return null;
2406
2457
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -2431,7 +2482,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
2431
2482
  };
2432
2483
  }
2433
2484
  return {
2434
- decision: "review",
2485
+ decision: highAction,
2435
2486
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
2436
2487
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
2437
2488
  tier: 3
@@ -2457,7 +2508,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2457
2508
  if (wouldBeIgnored) return { decision: "allow" };
2458
2509
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2459
2510
  if (bashCommand !== null) {
2460
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
2511
+ const pipeVerdict = pipeChainVerdict(
2512
+ bashCommand,
2513
+ isTrustedHost2,
2514
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2515
+ );
2461
2516
  if (pipeVerdict) return pipeVerdict;
2462
2517
  const fsVerdict = analyzeFsOperation(bashCommand);
2463
2518
  if (fsVerdict) {
@@ -2472,10 +2527,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2472
2527
  ruleDescription: fsVerdict.reason
2473
2528
  };
2474
2529
  }
2475
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
2530
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2531
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
2476
2532
  if (sqlVerdict) {
2477
2533
  return {
2478
- decision: sqlVerdict.verdict,
2534
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
2535
+ decision: sqlAction === "block" ? "block" : "review",
2479
2536
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
2480
2537
  reason: sqlVerdict.reason,
2481
2538
  tier: 2,
@@ -2483,10 +2540,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2483
2540
  ruleDescription: sqlVerdict.description
2484
2541
  };
2485
2542
  }
2486
- const chmodVerdict = analyzeChmod777(bashCommand);
2543
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
2544
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
2487
2545
  if (chmodVerdict) {
2488
2546
  return {
2489
- decision: chmodVerdict.verdict,
2547
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
2548
+ decision: chmodAction === "block" ? "block" : "review",
2490
2549
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2491
2550
  reason: chmodVerdict.reason,
2492
2551
  tier: 2,
@@ -2529,10 +2588,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2529
2588
  const analyzed = analyzeShellCommand(shellCommand);
2530
2589
  allTokens = analyzed.allTokens;
2531
2590
  pathTokens = analyzed.paths;
2532
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
2533
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
2591
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2592
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2534
2593
  return {
2535
- decision: "review",
2594
+ decision: inlineAction === "block" ? "block" : "review",
2536
2595
  blockedByLabel: "Node9 Standard (Inline Execution)",
2537
2596
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2538
2597
  tier: 3
@@ -2550,14 +2609,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2550
2609
  }
2551
2610
  if (evalVerdict === "review") {
2552
2611
  return {
2553
- decision: "review",
2612
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2613
+ // block but can never turn this off (eval-remote above is Class A —
2614
+ // no knob at all).
2615
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2554
2616
  blockedByLabel: "Node9: Eval Dynamic Content",
2555
2617
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2556
2618
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
2557
2619
  tier: 3
2558
2620
  };
2559
2621
  }
2560
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
2622
+ const ptVerdict = pipeChainVerdict(
2623
+ shellCommand,
2624
+ isTrustedHost2,
2625
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2626
+ );
2561
2627
  if (ptVerdict) return ptVerdict;
2562
2628
  if (config.policy.egress?.enabled) {
2563
2629
  const dests = extractShellDestinations(shellCommand);
@@ -3668,6 +3734,33 @@ function applyManagedDlp(local, managed, locked) {
3668
3734
  }
3669
3735
  return next;
3670
3736
  }
3737
+ var COMMAND_CHECK_ORDER = ["off", "review", "block"];
3738
+ var COMMAND_CHECK_KEYS = [
3739
+ "inlineExec",
3740
+ "rmAdvisory",
3741
+ "chmod",
3742
+ "sqlDdl",
3743
+ "evalDynamic",
3744
+ "pipeChainHigh"
3745
+ ];
3746
+ function applyManagedCommandChecks(local, managed, locked) {
3747
+ const next = { ...local };
3748
+ for (const key of COMMAND_CHECK_KEYS) {
3749
+ const m = managed[key];
3750
+ if (typeof m !== "string") continue;
3751
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3752
+ const localValue = local[key];
3753
+ const resolved = localValue === void 0 ? m : resolveByOrder(
3754
+ COMMAND_CHECK_ORDER,
3755
+ localValue,
3756
+ m,
3757
+ locked.includes(lockKey)
3758
+ );
3759
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3760
+ next[key] = resolved;
3761
+ }
3762
+ return next;
3763
+ }
3671
3764
  function applyManagedApprovers(local, managed) {
3672
3765
  return {
3673
3766
  ...local,
@@ -4238,6 +4331,21 @@ function getConfig(cwd) {
4238
4331
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4239
4332
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4240
4333
  }
4334
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4335
+ const src = p.commandChecks;
4336
+ const cc2 = {
4337
+ ...mergedPolicy.commandChecks
4338
+ };
4339
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4340
+ const v = src[k];
4341
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4342
+ }
4343
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4344
+ const v = src[k];
4345
+ if (v === "review" || v === "block") cc2[k] = v;
4346
+ }
4347
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4348
+ }
4241
4349
  if (p.egress) {
4242
4350
  const e = p.egress;
4243
4351
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4339,6 +4447,13 @@ function getConfig(cwd) {
4339
4447
  locked
4340
4448
  );
4341
4449
  }
4450
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4451
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4452
+ mergedPolicy.commandChecks ?? {},
4453
+ mc.commandChecks,
4454
+ locked
4455
+ );
4456
+ }
4342
4457
  if (mc.approvers && typeof mc.approvers === "object") {
4343
4458
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4344
4459
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4448,8 +4563,17 @@ function getConfig(cwd) {
4448
4563
  }
4449
4564
  }
4450
4565
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4566
+ const cc = mergedPolicy.commandChecks ?? {};
4567
+ const advisoryKnob = (name) => {
4568
+ if (name === "review-rm") return cc.rmAdvisory;
4569
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4570
+ return void 0;
4571
+ };
4451
4572
  for (const rule of ADVISORY_SMART_RULES) {
4452
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4573
+ if (existingAdvisoryNames.has(rule.name)) continue;
4574
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4575
+ if (knob === "off") continue;
4576
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4453
4577
  }
4454
4578
  const envMode = process.env.NODE9_MODE;
4455
4579
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
package/dist/index.mjs CHANGED
@@ -328,6 +328,16 @@ var ConfigFileSchema = z.object({
328
328
  pii: z.enum(["off", "block"]).optional(),
329
329
  reviewAction: z.enum(["review", "block"]).optional()
330
330
  }).optional(),
331
+ // Command-checks governance. Class-B keys (evalDynamic, pipeChainHigh)
332
+ // deliberately exclude 'off' — tighten-only.
333
+ commandChecks: z.object({
334
+ inlineExec: z.enum(["off", "review", "block"]).optional(),
335
+ rmAdvisory: z.enum(["off", "review", "block"]).optional(),
336
+ chmod: z.enum(["off", "review", "block"]).optional(),
337
+ sqlDdl: z.enum(["off", "review", "block"]).optional(),
338
+ evalDynamic: z.enum(["review", "block"]).optional(),
339
+ pipeChainHigh: z.enum(["review", "block"]).optional()
340
+ }).optional(),
331
341
  egress: z.object({
332
342
  enabled: z.boolean().optional(),
333
343
  mode: z.enum(["off", "review", "block"]).optional(),
@@ -2338,6 +2348,47 @@ function evaluateSmartConditions(args, rule) {
2338
2348
  });
2339
2349
  return mode === "any" ? results.some((r) => r) : results.every((r) => r);
2340
2350
  }
2351
+ function resolveCheck(v) {
2352
+ return v === "off" || v === "block" ? v : "review";
2353
+ }
2354
+ function resolveCheckTight(v) {
2355
+ return v === "block" ? "block" : "review";
2356
+ }
2357
+ var INLINE_INTERP = /^(python[\d.]*|bash|sh|zsh|perl|ruby|node|php|lua)$/i;
2358
+ var INLINE_SHELL = /^(bash|sh|zsh)$/i;
2359
+ function detectInlineExec(command) {
2360
+ const pipeFed = command.includes("|");
2361
+ const segments = command.split(/\|\||&&|;|\||\n|\$\(|`|\(/);
2362
+ for (const rawSeg of segments) {
2363
+ const tokens = rawSeg.trim().split(/\s+/).filter(Boolean);
2364
+ let i = 0;
2365
+ while (i < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[i])) i++;
2366
+ if (i >= tokens.length) continue;
2367
+ const base = tokens[i].split("/").pop() ?? tokens[i];
2368
+ if (!INLINE_INTERP.test(base)) continue;
2369
+ const args = tokens.slice(i + 1);
2370
+ let hadRedirect = false;
2371
+ const positionals = [];
2372
+ for (let j = 0; j < args.length; j++) {
2373
+ const a = args[j];
2374
+ if (a === "-") return true;
2375
+ if (a.startsWith("<")) {
2376
+ hadRedirect = true;
2377
+ if (a === "<" || a === "<<") j++;
2378
+ continue;
2379
+ }
2380
+ if (a.startsWith("-")) {
2381
+ if (/^-(c|e|eval)$/i.test(a)) return true;
2382
+ continue;
2383
+ }
2384
+ positionals.push(a);
2385
+ }
2386
+ if (positionals.length === 0 && (hadRedirect || pipeFed) && !INLINE_SHELL.test(base)) {
2387
+ return true;
2388
+ }
2389
+ }
2390
+ return false;
2391
+ }
2341
2392
  var VERDICT_RANK = {
2342
2393
  allow: 0,
2343
2394
  review: 1,
@@ -2370,7 +2421,7 @@ function isSqlTool(toolName, toolInspection) {
2370
2421
  return fieldName === "sql" || fieldName === "query";
2371
2422
  }
2372
2423
  var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
2373
- function pipeChainVerdict(command, isTrustedHost2) {
2424
+ function pipeChainVerdict(command, isTrustedHost2, highAction = "review") {
2374
2425
  const pipeAnalysis = analyzePipeChain(command);
2375
2426
  if (!pipeAnalysis.isPipeline) return null;
2376
2427
  if (pipeAnalysis.risk !== "critical" && pipeAnalysis.risk !== "high") return null;
@@ -2401,7 +2452,7 @@ function pipeChainVerdict(command, isTrustedHost2) {
2401
2452
  };
2402
2453
  }
2403
2454
  return {
2404
- decision: "review",
2455
+ decision: highAction,
2405
2456
  blockedByLabel: "Node9: Pipe-Chain Exfiltration (high)",
2406
2457
  reason: `Sensitive file piped to network sink: ${pipeAnalysis.sourceFiles.join(", ")} \u2192 ${sinks.join(", ")}`,
2407
2458
  tier: 3
@@ -2427,7 +2478,11 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2427
2478
  if (wouldBeIgnored) return { decision: "allow" };
2428
2479
  const bashCommand = agent !== "Terminal" && isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : null;
2429
2480
  if (bashCommand !== null) {
2430
- const pipeVerdict = pipeChainVerdict(bashCommand, isTrustedHost2);
2481
+ const pipeVerdict = pipeChainVerdict(
2482
+ bashCommand,
2483
+ isTrustedHost2,
2484
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2485
+ );
2431
2486
  if (pipeVerdict) return pipeVerdict;
2432
2487
  const fsVerdict = analyzeFsOperation(bashCommand);
2433
2488
  if (fsVerdict) {
@@ -2442,10 +2497,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2442
2497
  ruleDescription: fsVerdict.reason
2443
2498
  };
2444
2499
  }
2445
- const sqlVerdict = analyzeSqlDestructive(bashCommand);
2500
+ const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
2501
+ const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
2446
2502
  if (sqlVerdict) {
2447
2503
  return {
2448
- decision: sqlVerdict.verdict,
2504
+ // analyzeSqlDestructive is typed review-only, so the knob maps 1:1.
2505
+ decision: sqlAction === "block" ? "block" : "review",
2449
2506
  blockedByLabel: `Node9 (AST): ${sqlVerdict.ruleName}`,
2450
2507
  reason: sqlVerdict.reason,
2451
2508
  tier: 2,
@@ -2453,10 +2510,12 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2453
2510
  ruleDescription: sqlVerdict.description
2454
2511
  };
2455
2512
  }
2456
- const chmodVerdict = analyzeChmod777(bashCommand);
2513
+ const chmodAction = resolveCheck(config.policy.commandChecks?.chmod);
2514
+ const chmodVerdict = chmodAction === "off" ? null : analyzeChmod777(bashCommand);
2457
2515
  if (chmodVerdict) {
2458
2516
  return {
2459
- decision: chmodVerdict.verdict,
2517
+ // analyzeChmod777 is typed review-only, so the knob maps 1:1.
2518
+ decision: chmodAction === "block" ? "block" : "review",
2460
2519
  blockedByLabel: `project-jail (AST): ${chmodVerdict.ruleName}`,
2461
2520
  reason: chmodVerdict.reason,
2462
2521
  tier: 2,
@@ -2499,10 +2558,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2499
2558
  const analyzed = analyzeShellCommand(shellCommand);
2500
2559
  allTokens = analyzed.allTokens;
2501
2560
  pathTokens = analyzed.paths;
2502
- const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
2503
- if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
2561
+ const inlineAction = resolveCheck(config.policy.commandChecks?.inlineExec);
2562
+ if (inlineAction !== "off" && detectInlineExec(shellCommand)) {
2504
2563
  return {
2505
- decision: "review",
2564
+ decision: inlineAction === "block" ? "block" : "review",
2506
2565
  blockedByLabel: "Node9 Standard (Inline Execution)",
2507
2566
  ruleDescription: "The AI is running code directly from the command line. Review the full script below before allowing it to execute.",
2508
2567
  tier: 3
@@ -2520,14 +2579,21 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
2520
2579
  }
2521
2580
  if (evalVerdict === "review") {
2522
2581
  return {
2523
- decision: "review",
2582
+ // Class B tighten-only: commandChecks.evalDynamic may upgrade to
2583
+ // block but can never turn this off (eval-remote above is Class A —
2584
+ // no knob at all).
2585
+ decision: resolveCheckTight(config.policy.commandChecks?.evalDynamic),
2524
2586
  blockedByLabel: "Node9: Eval Dynamic Content",
2525
2587
  reason: "eval of dynamic content (variable or subshell expansion) requires approval",
2526
2588
  ruleDescription: "The AI is running a command that includes a variable or subshell expansion. The actual command executed at runtime may differ from what is shown here.",
2527
2589
  tier: 3
2528
2590
  };
2529
2591
  }
2530
- const ptVerdict = pipeChainVerdict(shellCommand, isTrustedHost2);
2592
+ const ptVerdict = pipeChainVerdict(
2593
+ shellCommand,
2594
+ isTrustedHost2,
2595
+ resolveCheckTight(config.policy.commandChecks?.pipeChainHigh)
2596
+ );
2531
2597
  if (ptVerdict) return ptVerdict;
2532
2598
  if (config.policy.egress?.enabled) {
2533
2599
  const dests = extractShellDestinations(shellCommand);
@@ -3638,6 +3704,33 @@ function applyManagedDlp(local, managed, locked) {
3638
3704
  }
3639
3705
  return next;
3640
3706
  }
3707
+ var COMMAND_CHECK_ORDER = ["off", "review", "block"];
3708
+ var COMMAND_CHECK_KEYS = [
3709
+ "inlineExec",
3710
+ "rmAdvisory",
3711
+ "chmod",
3712
+ "sqlDdl",
3713
+ "evalDynamic",
3714
+ "pipeChainHigh"
3715
+ ];
3716
+ function applyManagedCommandChecks(local, managed, locked) {
3717
+ const next = { ...local };
3718
+ for (const key of COMMAND_CHECK_KEYS) {
3719
+ const m = managed[key];
3720
+ if (typeof m !== "string") continue;
3721
+ const lockKey = `commandChecks${key[0].toUpperCase()}${key.slice(1)}`;
3722
+ const localValue = local[key];
3723
+ const resolved = localValue === void 0 ? m : resolveByOrder(
3724
+ COMMAND_CHECK_ORDER,
3725
+ localValue,
3726
+ m,
3727
+ locked.includes(lockKey)
3728
+ );
3729
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3730
+ next[key] = resolved;
3731
+ }
3732
+ return next;
3733
+ }
3641
3734
  function applyManagedApprovers(local, managed) {
3642
3735
  return {
3643
3736
  ...local,
@@ -4208,6 +4301,21 @@ function getConfig(cwd) {
4208
4301
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4209
4302
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4210
4303
  }
4304
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4305
+ const src = p.commandChecks;
4306
+ const cc2 = {
4307
+ ...mergedPolicy.commandChecks
4308
+ };
4309
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4310
+ const v = src[k];
4311
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4312
+ }
4313
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4314
+ const v = src[k];
4315
+ if (v === "review" || v === "block") cc2[k] = v;
4316
+ }
4317
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4318
+ }
4211
4319
  if (p.egress) {
4212
4320
  const e = p.egress;
4213
4321
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4309,6 +4417,13 @@ function getConfig(cwd) {
4309
4417
  locked
4310
4418
  );
4311
4419
  }
4420
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4421
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4422
+ mergedPolicy.commandChecks ?? {},
4423
+ mc.commandChecks,
4424
+ locked
4425
+ );
4426
+ }
4312
4427
  if (mc.approvers && typeof mc.approvers === "object") {
4313
4428
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4314
4429
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4418,8 +4533,17 @@ function getConfig(cwd) {
4418
4533
  }
4419
4534
  }
4420
4535
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4536
+ const cc = mergedPolicy.commandChecks ?? {};
4537
+ const advisoryKnob = (name) => {
4538
+ if (name === "review-rm") return cc.rmAdvisory;
4539
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4540
+ return void 0;
4541
+ };
4421
4542
  for (const rule of ADVISORY_SMART_RULES) {
4422
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4543
+ if (existingAdvisoryNames.has(rule.name)) continue;
4544
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4545
+ if (knob === "off") continue;
4546
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4423
4547
  }
4424
4548
  const envMode = process.env.NODE9_MODE;
4425
4549
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.66.0",
3
+ "version": "1.67.1",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",