@node9/proxy 1.66.0 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,23 @@ 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 resolved = resolveByOrder(
4458
+ COMMAND_CHECK_ORDER,
4459
+ local[key] ?? "review",
4460
+ m,
4461
+ locked.includes(lockKey)
4462
+ );
4463
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4464
+ next[key] = resolved;
4465
+ }
4466
+ return next;
4467
+ }
4385
4468
  function applyManagedApprovers(local, managed) {
4386
4469
  return {
4387
4470
  ...local,
@@ -4391,7 +4474,7 @@ function applyManagedApprovers(local, managed) {
4391
4474
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4392
4475
  };
4393
4476
  }
4394
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4477
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4395
4478
  var init_managed = __esm({
4396
4479
  "src/config/managed.ts"() {
4397
4480
  "use strict";
@@ -4399,6 +4482,15 @@ var init_managed = __esm({
4399
4482
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4400
4483
  DLP_PII_ORDER = ["off", "block"];
4401
4484
  DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4485
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4486
+ COMMAND_CHECK_KEYS = [
4487
+ "inlineExec",
4488
+ "rmAdvisory",
4489
+ "chmod",
4490
+ "sqlDdl",
4491
+ "evalDynamic",
4492
+ "pipeChainHigh"
4493
+ ];
4402
4494
  }
4403
4495
  });
4404
4496
 
@@ -4775,6 +4867,21 @@ function getConfig(cwd) {
4775
4867
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4776
4868
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4777
4869
  }
4870
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4871
+ const src = p.commandChecks;
4872
+ const cc2 = {
4873
+ ...mergedPolicy.commandChecks
4874
+ };
4875
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4876
+ const v = src[k];
4877
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4878
+ }
4879
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4880
+ const v = src[k];
4881
+ if (v === "review" || v === "block") cc2[k] = v;
4882
+ }
4883
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4884
+ }
4778
4885
  if (p.egress) {
4779
4886
  const e = p.egress;
4780
4887
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4876,6 +4983,13 @@ function getConfig(cwd) {
4876
4983
  locked
4877
4984
  );
4878
4985
  }
4986
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4987
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4988
+ mergedPolicy.commandChecks ?? {},
4989
+ mc.commandChecks,
4990
+ locked
4991
+ );
4992
+ }
4879
4993
  if (mc.approvers && typeof mc.approvers === "object") {
4880
4994
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4881
4995
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4985,8 +5099,17 @@ function getConfig(cwd) {
4985
5099
  }
4986
5100
  }
4987
5101
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5102
+ const cc = mergedPolicy.commandChecks ?? {};
5103
+ const advisoryKnob = (name) => {
5104
+ if (name === "review-rm") return cc.rmAdvisory;
5105
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5106
+ return void 0;
5107
+ };
4988
5108
  for (const rule of ADVISORY_SMART_RULES) {
4989
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5109
+ if (existingAdvisoryNames.has(rule.name)) continue;
5110
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5111
+ if (knob === "off") continue;
5112
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4990
5113
  }
4991
5114
  const envMode = process.env.NODE9_MODE;
4992
5115
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -18378,6 +18501,18 @@ function extractManagedConfig(body) {
18378
18501
  d.reviewAction = mc.dlp.reviewAction;
18379
18502
  if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18380
18503
  }
18504
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18505
+ const cc = {};
18506
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18507
+ const v = mc.commandChecks[k];
18508
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18509
+ }
18510
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18511
+ const v = mc.commandChecks[k];
18512
+ if (v === "review" || v === "block") cc[k] = v;
18513
+ }
18514
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18515
+ }
18381
18516
  if (mc.approvers && typeof mc.approvers === "object") {
18382
18517
  const a = {};
18383
18518
  for (const k of ["native", "browser", "cloud", "terminal"]) {
@@ -18440,7 +18575,7 @@ function extractManagedConfig(body) {
18440
18575
  }
18441
18576
  if (Object.keys(ap).length) out.appPermissions = ap;
18442
18577
  }
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;
18578
+ 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
18579
  }
18445
18580
  function sweepStaleTmp(target) {
18446
18581
  try {
@@ -48808,6 +48943,7 @@ init_audit();
48808
48943
  init_config();
48809
48944
  init_daemon();
48810
48945
  init_dlp();
48946
+ init_hasher();
48811
48947
 
48812
48948
  // src/utils/cp-mv-parser.ts
48813
48949
  function parseCpMvOp(command) {
@@ -48869,6 +49005,20 @@ function atLeastConfidence(c, min) {
48869
49005
  function sanitize3(value) {
48870
49006
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48871
49007
  }
49008
+ function buildArgsField(rawInput) {
49009
+ let hashed = {};
49010
+ try {
49011
+ hashed = { argsHash: hashArgs(rawInput) };
49012
+ } catch {
49013
+ }
49014
+ try {
49015
+ const hit = scanArgs(rawInput);
49016
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49017
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49018
+ } catch {
49019
+ return hashed;
49020
+ }
49021
+ }
48872
49022
  function registerLogCommand(program2) {
48873
49023
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48874
49024
  "--agent <name>",
@@ -48907,7 +49057,7 @@ function registerLogCommand(program2) {
48907
49057
  const entry = {
48908
49058
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48909
49059
  tool,
48910
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49060
+ ...buildArgsField(rawInput),
48911
49061
  decision: "allowed",
48912
49062
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48913
49063
  };
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,23 @@ 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 resolved = resolveByOrder(
4462
+ COMMAND_CHECK_ORDER,
4463
+ local[key] ?? "review",
4464
+ m,
4465
+ locked.includes(lockKey)
4466
+ );
4467
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
4468
+ next[key] = resolved;
4469
+ }
4470
+ return next;
4471
+ }
4389
4472
  function applyManagedApprovers(local, managed) {
4390
4473
  return {
4391
4474
  ...local,
@@ -4395,7 +4478,7 @@ function applyManagedApprovers(local, managed) {
4395
4478
  terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
4396
4479
  };
4397
4480
  }
4398
- var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
4481
+ var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER, COMMAND_CHECK_ORDER, COMMAND_CHECK_KEYS;
4399
4482
  var init_managed = __esm({
4400
4483
  "src/config/managed.ts"() {
4401
4484
  "use strict";
@@ -4403,6 +4486,15 @@ var init_managed = __esm({
4403
4486
  EGRESS_MODE_ORDER = ["off", "review", "block"];
4404
4487
  DLP_PII_ORDER = ["off", "block"];
4405
4488
  DLP_REVIEW_ACTION_ORDER = ["review", "block"];
4489
+ COMMAND_CHECK_ORDER = ["off", "review", "block"];
4490
+ COMMAND_CHECK_KEYS = [
4491
+ "inlineExec",
4492
+ "rmAdvisory",
4493
+ "chmod",
4494
+ "sqlDdl",
4495
+ "evalDynamic",
4496
+ "pipeChainHigh"
4497
+ ];
4406
4498
  }
4407
4499
  });
4408
4500
 
@@ -4782,6 +4874,21 @@ function getConfig(cwd) {
4782
4874
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4783
4875
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4784
4876
  }
4877
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4878
+ const src = p.commandChecks;
4879
+ const cc2 = {
4880
+ ...mergedPolicy.commandChecks
4881
+ };
4882
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4883
+ const v = src[k];
4884
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4885
+ }
4886
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4887
+ const v = src[k];
4888
+ if (v === "review" || v === "block") cc2[k] = v;
4889
+ }
4890
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4891
+ }
4785
4892
  if (p.egress) {
4786
4893
  const e = p.egress;
4787
4894
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4883,6 +4990,13 @@ function getConfig(cwd) {
4883
4990
  locked
4884
4991
  );
4885
4992
  }
4993
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4994
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4995
+ mergedPolicy.commandChecks ?? {},
4996
+ mc.commandChecks,
4997
+ locked
4998
+ );
4999
+ }
4886
5000
  if (mc.approvers && typeof mc.approvers === "object") {
4887
5001
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4888
5002
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4992,8 +5106,17 @@ function getConfig(cwd) {
4992
5106
  }
4993
5107
  }
4994
5108
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
5109
+ const cc = mergedPolicy.commandChecks ?? {};
5110
+ const advisoryKnob = (name) => {
5111
+ if (name === "review-rm") return cc.rmAdvisory;
5112
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
5113
+ return void 0;
5114
+ };
4995
5115
  for (const rule of ADVISORY_SMART_RULES) {
4996
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
5116
+ if (existingAdvisoryNames.has(rule.name)) continue;
5117
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
5118
+ if (knob === "off") continue;
5119
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4997
5120
  }
4998
5121
  const envMode = process.env.NODE9_MODE;
4999
5122
  if (envMode && !modeCloudControlled && ["observe", "audit", "standard", "strict"].includes(envMode)) {
@@ -18376,6 +18499,18 @@ function extractManagedConfig(body) {
18376
18499
  d.reviewAction = mc.dlp.reviewAction;
18377
18500
  if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
18378
18501
  }
18502
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
18503
+ const cc = {};
18504
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
18505
+ const v = mc.commandChecks[k];
18506
+ if (v === "off" || v === "review" || v === "block") cc[k] = v;
18507
+ }
18508
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
18509
+ const v = mc.commandChecks[k];
18510
+ if (v === "review" || v === "block") cc[k] = v;
18511
+ }
18512
+ if (Object.keys(cc).length > 0) out.commandChecks = cc;
18513
+ }
18379
18514
  if (mc.approvers && typeof mc.approvers === "object") {
18380
18515
  const a = {};
18381
18516
  for (const k of ["native", "browser", "cloud", "terminal"]) {
@@ -18438,7 +18573,7 @@ function extractManagedConfig(body) {
18438
18573
  }
18439
18574
  if (Object.keys(ap).length) out.appPermissions = ap;
18440
18575
  }
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;
18576
+ 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
18577
  }
18443
18578
  function sweepStaleTmp(target) {
18444
18579
  try {
@@ -48801,6 +48936,7 @@ import path48 from "path";
48801
48936
  import os45 from "os";
48802
48937
  init_daemon();
48803
48938
  init_dlp();
48939
+ init_hasher();
48804
48940
 
48805
48941
  // src/utils/cp-mv-parser.ts
48806
48942
  function parseCpMvOp(command) {
@@ -48862,6 +48998,20 @@ function atLeastConfidence(c, min) {
48862
48998
  function sanitize3(value) {
48863
48999
  return value.replace(/[\x00-\x1F\x7F]/g, "");
48864
49000
  }
49001
+ function buildArgsField(rawInput) {
49002
+ let hashed = {};
49003
+ try {
49004
+ hashed = { argsHash: hashArgs(rawInput) };
49005
+ } catch {
49006
+ }
49007
+ try {
49008
+ const hit = scanArgs(rawInput);
49009
+ if (hit) return { ...hashed, dlpPattern: hit.patternName, dlpSample: hit.redactedSample };
49010
+ return { args: JSON.parse(redactSecrets(JSON.stringify(rawInput))) };
49011
+ } catch {
49012
+ return hashed;
49013
+ }
49014
+ }
48865
49015
  function registerLogCommand(program2) {
48866
49016
  program2.command("log", { hidden: true }).description("PostToolUse hook \u2014 records executed tool calls").argument("[data]", "JSON string of the tool call").option(
48867
49017
  "--agent <name>",
@@ -48900,7 +49050,7 @@ function registerLogCommand(program2) {
48900
49050
  const entry = {
48901
49051
  ts: (/* @__PURE__ */ new Date()).toISOString(),
48902
49052
  tool,
48903
- args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
49053
+ ...buildArgsField(rawInput),
48904
49054
  decision: "allowed",
48905
49055
  source: reviewApproved ? "inline-review-approved" : "post-hook"
48906
49056
  };
@@ -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,32 @@ 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 resolved = resolveByOrder(
3753
+ COMMAND_CHECK_ORDER,
3754
+ local[key] ?? "review",
3755
+ m,
3756
+ locked.includes(lockKey)
3757
+ );
3758
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3759
+ next[key] = resolved;
3760
+ }
3761
+ return next;
3762
+ }
3671
3763
  function applyManagedApprovers(local, managed) {
3672
3764
  return {
3673
3765
  ...local,
@@ -4238,6 +4330,21 @@ function getConfig(cwd) {
4238
4330
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4239
4331
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4240
4332
  }
4333
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4334
+ const src = p.commandChecks;
4335
+ const cc2 = {
4336
+ ...mergedPolicy.commandChecks
4337
+ };
4338
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4339
+ const v = src[k];
4340
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4341
+ }
4342
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4343
+ const v = src[k];
4344
+ if (v === "review" || v === "block") cc2[k] = v;
4345
+ }
4346
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4347
+ }
4241
4348
  if (p.egress) {
4242
4349
  const e = p.egress;
4243
4350
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4339,6 +4446,13 @@ function getConfig(cwd) {
4339
4446
  locked
4340
4447
  );
4341
4448
  }
4449
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4450
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4451
+ mergedPolicy.commandChecks ?? {},
4452
+ mc.commandChecks,
4453
+ locked
4454
+ );
4455
+ }
4342
4456
  if (mc.approvers && typeof mc.approvers === "object") {
4343
4457
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4344
4458
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4448,8 +4562,17 @@ function getConfig(cwd) {
4448
4562
  }
4449
4563
  }
4450
4564
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4565
+ const cc = mergedPolicy.commandChecks ?? {};
4566
+ const advisoryKnob = (name) => {
4567
+ if (name === "review-rm") return cc.rmAdvisory;
4568
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4569
+ return void 0;
4570
+ };
4451
4571
  for (const rule of ADVISORY_SMART_RULES) {
4452
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4572
+ if (existingAdvisoryNames.has(rule.name)) continue;
4573
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4574
+ if (knob === "off") continue;
4575
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4453
4576
  }
4454
4577
  const envMode = process.env.NODE9_MODE;
4455
4578
  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,32 @@ 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 resolved = resolveByOrder(
3723
+ COMMAND_CHECK_ORDER,
3724
+ local[key] ?? "review",
3725
+ m,
3726
+ locked.includes(lockKey)
3727
+ );
3728
+ if ((key === "evalDynamic" || key === "pipeChainHigh") && resolved === "off") continue;
3729
+ next[key] = resolved;
3730
+ }
3731
+ return next;
3732
+ }
3641
3733
  function applyManagedApprovers(local, managed) {
3642
3734
  return {
3643
3735
  ...local,
@@ -4208,6 +4300,21 @@ function getConfig(cwd) {
4208
4300
  if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
4209
4301
  if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
4210
4302
  }
4303
+ if (p.commandChecks && typeof p.commandChecks === "object") {
4304
+ const src = p.commandChecks;
4305
+ const cc2 = {
4306
+ ...mergedPolicy.commandChecks
4307
+ };
4308
+ for (const k of ["inlineExec", "rmAdvisory", "chmod", "sqlDdl"]) {
4309
+ const v = src[k];
4310
+ if (v === "off" || v === "review" || v === "block") cc2[k] = v;
4311
+ }
4312
+ for (const k of ["evalDynamic", "pipeChainHigh"]) {
4313
+ const v = src[k];
4314
+ if (v === "review" || v === "block") cc2[k] = v;
4315
+ }
4316
+ if (Object.keys(cc2).length > 0) mergedPolicy.commandChecks = cc2;
4317
+ }
4211
4318
  if (p.egress) {
4212
4319
  const e = p.egress;
4213
4320
  if (e.enabled !== void 0) mergedPolicy.egress.enabled = e.enabled;
@@ -4309,6 +4416,13 @@ function getConfig(cwd) {
4309
4416
  locked
4310
4417
  );
4311
4418
  }
4419
+ if (mc.commandChecks && typeof mc.commandChecks === "object") {
4420
+ mergedPolicy.commandChecks = applyManagedCommandChecks(
4421
+ mergedPolicy.commandChecks ?? {},
4422
+ mc.commandChecks,
4423
+ locked
4424
+ );
4425
+ }
4312
4426
  if (mc.approvers && typeof mc.approvers === "object") {
4313
4427
  const bool = (v) => typeof v === "boolean" ? v : void 0;
4314
4428
  mergedSettings.approvers = applyManagedApprovers(mergedSettings.approvers, {
@@ -4418,8 +4532,17 @@ function getConfig(cwd) {
4418
4532
  }
4419
4533
  }
4420
4534
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
4535
+ const cc = mergedPolicy.commandChecks ?? {};
4536
+ const advisoryKnob = (name) => {
4537
+ if (name === "review-rm") return cc.rmAdvisory;
4538
+ if (name?.endsWith("-sql")) return cc.sqlDdl;
4539
+ return void 0;
4540
+ };
4421
4541
  for (const rule of ADVISORY_SMART_RULES) {
4422
- if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
4542
+ if (existingAdvisoryNames.has(rule.name)) continue;
4543
+ const knob = rule.verdict === "review" ? advisoryKnob(rule.name) : void 0;
4544
+ if (knob === "off") continue;
4545
+ mergedPolicy.smartRules.push(knob === "block" ? { ...rule, verdict: "block" } : rule);
4423
4546
  }
4424
4547
  const envMode = process.env.NODE9_MODE;
4425
4548
  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.0",
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",