claude-warden 2.3.2 → 2.4.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.
@@ -12481,7 +12481,7 @@ var require_log = __commonJS({
12481
12481
  if (logLevel === "debug")
12482
12482
  console.log(...messages);
12483
12483
  }
12484
- function warn(logLevel, warning) {
12484
+ function warn2(logLevel, warning) {
12485
12485
  if (logLevel === "debug" || logLevel === "warn") {
12486
12486
  if (typeof node_process.emitWarning === "function")
12487
12487
  node_process.emitWarning(warning);
@@ -12490,7 +12490,7 @@ var require_log = __commonJS({
12490
12490
  }
12491
12491
  }
12492
12492
  exports2.debug = debug;
12493
- exports2.warn = warn;
12493
+ exports2.warn = warn2;
12494
12494
  }
12495
12495
  });
12496
12496
 
@@ -18134,9 +18134,6 @@ var require_dist2 = __commonJS({
18134
18134
  var import_fs2 = require("fs");
18135
18135
  var import_path4 = require("path");
18136
18136
 
18137
- // src/evaluator.ts
18138
- var import_os = require("os");
18139
-
18140
18137
  // src/parser.ts
18141
18138
  var import_bash_parser = __toESM(require_src(), 1);
18142
18139
  var import_path = require("path");
@@ -18456,6 +18453,9 @@ function regexFallbackParse(input) {
18456
18453
  return { command, originalCommand, args: args2, envPrefixes, raw: trimmed };
18457
18454
  }
18458
18455
 
18456
+ // src/evaluator.ts
18457
+ var import_os2 = require("os");
18458
+
18459
18459
  // src/targets.ts
18460
18460
  var import_path2 = require("path");
18461
18461
 
@@ -18732,1029 +18732,152 @@ function evaluateTargetPolicies(cmd, cwd, config) {
18732
18732
  return results[0];
18733
18733
  }
18734
18734
 
18735
- // src/evaluator.ts
18736
- function safeRegexTest(pattern, input) {
18737
- try {
18738
- return new RegExp(pattern).test(input);
18739
- } catch {
18740
- process.stderr.write(`[warden] Warning: invalid regex pattern: ${pattern}
18741
- `);
18742
- return false;
18743
- }
18744
- }
18745
- function commandMatchesName(cmd, name) {
18746
- if (name.startsWith("/")) {
18747
- return cmd.originalCommand === name;
18748
- }
18749
- if (name.startsWith("~/")) {
18750
- return cmd.originalCommand === (0, import_os.homedir)() + name.slice(1);
18751
- }
18752
- return cmd.command === name;
18753
- }
18754
- var MAX_RECURSION_DEPTH = 10;
18755
- function evaluate(parsed, config, cwdOrDepth, maybeDepth) {
18756
- const cwd = typeof cwdOrDepth === "string" ? cwdOrDepth : void 0;
18757
- const depth = typeof cwdOrDepth === "number" ? cwdOrDepth : maybeDepth ?? 0;
18758
- if (depth > MAX_RECURSION_DEPTH) {
18759
- return { decision: "ask", reason: "Maximum recursion depth exceeded", details: [] };
18760
- }
18761
- if (parsed.parseError) {
18762
- return { decision: "ask", reason: "Could not parse command safely", details: [] };
18763
- }
18764
- if (parsed.commands.length === 0) {
18765
- return { decision: "allow", reason: "Empty command", details: [] };
18766
- }
18767
- if (parsed.hasSubshell && parsed.subshellCommands.length > 0) {
18768
- for (const subCmd of parsed.subshellCommands) {
18769
- const subParsed = parseCommand(subCmd);
18770
- const subResult = evaluate(subParsed, config, cwd, depth + 1);
18771
- if (subResult.decision === "deny") {
18772
- return { decision: "deny", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
18773
- }
18774
- if (subResult.decision === "ask") {
18775
- return { decision: "ask", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
18776
- }
18777
- }
18778
- } else if (parsed.hasSubshell && parsed.subshellCommands.length === 0 && config.askOnSubshell) {
18779
- return { decision: "ask", reason: "Command contains subshell/command substitution", details: [] };
18780
- }
18781
- const details = [];
18782
- for (const cmd of parsed.commands) {
18783
- details.push(evaluateCommand(cmd, config, cwd, depth));
18784
- }
18785
- const decisions = details.map((d) => d.decision);
18786
- if (decisions.includes("deny")) {
18787
- const denied = details.filter((d) => d.decision === "deny");
18788
- return {
18789
- decision: "deny",
18790
- reason: denied.map((d) => `${d.command}: ${d.reason}`).join("; "),
18791
- details
18792
- };
18793
- }
18794
- if (decisions.includes("ask")) {
18795
- const asked = details.filter((d) => d.decision === "ask");
18796
- return {
18797
- decision: "ask",
18798
- reason: asked.map((d) => `${d.command}: ${d.reason}`).join("; "),
18799
- details
18800
- };
18801
- }
18802
- return { decision: "allow", reason: "All commands are safe", details };
18803
- }
18804
- function evaluateCommand(cmd, config, cwd, depth = 0) {
18805
- const { command, args: args2 } = cmd;
18806
- for (const layer of config.layers) {
18807
- if (layer.alwaysDeny.some((name) => commandMatchesName(cmd, name))) {
18808
- return { command, args: args2, decision: "deny", reason: `"${command}" is blocked`, matchedRule: "alwaysDeny" };
18809
- }
18810
- if (layer.alwaysAllow.some((name) => commandMatchesName(cmd, name))) {
18811
- return { command, args: args2, decision: "allow", reason: `"${command}" is safe`, matchedRule: "alwaysAllow" };
18812
- }
18813
- }
18814
- if (cwd) {
18815
- const targetResult = evaluateTargetPolicies(cmd, cwd, config);
18816
- if (targetResult) return targetResult;
18817
- }
18818
- const remotes = config.trustedRemotes || [];
18819
- if (command === "ssh" || command === "scp" || command === "rsync") {
18820
- const sshTargets = remotes.filter((r) => r.context === "ssh");
18821
- if (sshTargets.length) {
18822
- const sshResult = evaluateSSHCommand(cmd, config, sshTargets, depth);
18823
- if (sshResult) return sshResult;
18824
- }
18825
- }
18826
- if (command === "docker") {
18827
- const dockerTargets = remotes.filter((r) => r.context === "docker");
18828
- if (dockerTargets.length) {
18829
- const dockerResult = evaluateDockerExec(cmd, config, dockerTargets, depth);
18830
- if (dockerResult) return dockerResult;
18831
- }
18832
- }
18833
- if (command === "kubectl") {
18834
- const kubectlTargets = remotes.filter((r) => r.context === "kubectl");
18835
- if (kubectlTargets.length) {
18836
- const kubectlResult = evaluateKubectlExec(cmd, config, kubectlTargets, depth);
18837
- if (kubectlResult) return kubectlResult;
18838
- }
18839
- }
18840
- if (command === "sprite") {
18841
- const spriteTargets = remotes.filter((r) => r.context === "sprite");
18842
- if (spriteTargets.length) {
18843
- const spriteResult = evaluateSpriteExec(cmd, config, spriteTargets, depth);
18844
- if (spriteResult) return spriteResult;
18845
- }
18846
- }
18847
- if (command === "fly" || command === "flyctl") {
18848
- const flyTargets = remotes.filter((r) => r.context === "fly");
18849
- if (flyTargets.length) {
18850
- const flyResult = evaluateFlyCommand(cmd, config, flyTargets, depth);
18851
- if (flyResult) return flyResult;
18852
- }
18853
- }
18854
- if (command === "xargs") {
18855
- return evaluateXargsCommand(cmd, config, cwd, depth);
18856
- }
18857
- if (command === "find") {
18858
- return evaluateFindCommand(cmd, config, cwd, depth);
18859
- }
18860
- const mergedRule = collectMergedRule(cmd, config);
18861
- if (mergedRule) {
18862
- return evaluateRule(cmd, mergedRule);
18863
- }
18864
- return { command, args: args2, decision: config.defaultDecision, reason: `No rule for "${command}"`, matchedRule: "default" };
18735
+ // src/rules.ts
18736
+ var import_fs = require("fs");
18737
+ var import_yaml = __toESM(require_dist2(), 1);
18738
+ var import_os = require("os");
18739
+ var import_path3 = require("path");
18740
+
18741
+ // src/defaults.ts
18742
+ var SAFE_DEV_TOOLS = [
18743
+ "jest",
18744
+ "vitest",
18745
+ "tsc",
18746
+ "eslint",
18747
+ "prettier",
18748
+ "mkdirp",
18749
+ "concurrently",
18750
+ "turbo",
18751
+ "next",
18752
+ "nuxt",
18753
+ "vite",
18754
+ "astro",
18755
+ "playwright",
18756
+ "cypress",
18757
+ "mocha",
18758
+ "nyc",
18759
+ "c8",
18760
+ "ts-jest",
18761
+ "tsup",
18762
+ "esbuild",
18763
+ "rollup",
18764
+ "webpack",
18765
+ "prisma",
18766
+ "drizzle-kit",
18767
+ "typeorm",
18768
+ "knex",
18769
+ "sequelize-cli",
18770
+ "tailwindcss",
18771
+ "postcss",
18772
+ "autoprefixer",
18773
+ "lint-staged",
18774
+ "husky",
18775
+ "changeset",
18776
+ "semantic-release",
18777
+ "lerna",
18778
+ "nx",
18779
+ "create-react-app",
18780
+ "create-next-app",
18781
+ "create-vite",
18782
+ "degit",
18783
+ "storybook",
18784
+ "wrangler",
18785
+ "netlify",
18786
+ "vercel",
18787
+ "json",
18788
+ "biome"
18789
+ ];
18790
+ var SCRIPT_RUNNERS = ["tsx", "ts-node", "nodemon"];
18791
+ var REGISTRY_OPS = ["publish", "unpublish", "deprecate", "owner", "access", "token", "adduser", "login", "logout"];
18792
+ var SAFE_PKG_MANAGER_CMDS = [
18793
+ "install",
18794
+ "add",
18795
+ "remove",
18796
+ "uninstall",
18797
+ "update",
18798
+ "upgrade",
18799
+ "outdated",
18800
+ "ls",
18801
+ "list",
18802
+ "run",
18803
+ "test",
18804
+ "start",
18805
+ "build",
18806
+ "init",
18807
+ "create",
18808
+ "info",
18809
+ "view",
18810
+ "show",
18811
+ "why",
18812
+ "pack",
18813
+ "cache",
18814
+ "config",
18815
+ "get",
18816
+ "set",
18817
+ "version",
18818
+ "help",
18819
+ "exec",
18820
+ "dedupe",
18821
+ "prune",
18822
+ "audit",
18823
+ "completion",
18824
+ "whoami"
18825
+ ];
18826
+ var VERSION_HELP_FLAGS = {
18827
+ match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] },
18828
+ decision: "allow",
18829
+ description: "Version/help flags"
18830
+ };
18831
+ function anyArgMatchesPattern(items) {
18832
+ return `^(${items.join("|")})$`;
18865
18833
  }
18866
- function collectMergedRule(cmd, config) {
18867
- const matchingRules = [];
18868
- for (const layer of config.layers) {
18869
- const rule = layer.rules.find((r) => commandMatchesName(cmd, r.command));
18870
- if (rule) {
18871
- matchingRules.push(rule);
18872
- if (rule.override) break;
18873
- }
18874
- }
18875
- if (matchingRules.length === 0) return null;
18876
- if (matchingRules.length === 1) return matchingRules[0];
18877
- const mergedPatterns = [];
18878
- for (const rule of matchingRules) {
18879
- if (rule.argPatterns) {
18880
- mergedPatterns.push(...rule.argPatterns);
18881
- }
18882
- }
18834
+ function safeDevToolsPattern() {
18883
18835
  return {
18884
- command: matchingRules[0].command,
18885
- default: matchingRules[0].default,
18886
- argPatterns: mergedPatterns
18836
+ match: { anyArgMatches: [anyArgMatchesPattern(SAFE_DEV_TOOLS)] },
18837
+ decision: "allow",
18838
+ description: "Well-known dev tools"
18887
18839
  };
18888
18840
  }
18889
- function evaluateRule(cmd, rule) {
18890
- const { command, args: args2 } = cmd;
18891
- const argsJoined = args2.join(" ");
18892
- for (const pattern of rule.argPatterns || []) {
18893
- const m = pattern.match;
18894
- let matched = true;
18895
- if (m.noArgs !== void 0) {
18896
- matched = matched && m.noArgs === (args2.length === 0);
18897
- }
18898
- if (m.argsMatch && matched) {
18899
- matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
18900
- }
18901
- if (m.anyArgMatches && matched) {
18902
- matched = args2.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
18903
- }
18904
- if (m.argCount && matched) {
18905
- if (m.argCount.min !== void 0) matched = matched && args2.length >= m.argCount.min;
18906
- if (m.argCount.max !== void 0) matched = matched && args2.length <= m.argCount.max;
18907
- }
18908
- if (m.not) matched = !matched;
18909
- if (matched) {
18910
- return {
18911
- command,
18912
- args: args2,
18913
- decision: pattern.decision,
18914
- reason: pattern.reason || pattern.description || `Matched pattern for "${command}"`,
18915
- matchedRule: `${command}:argPattern`
18916
- };
18917
- }
18918
- }
18841
+ function scriptRunnersPattern() {
18919
18842
  return {
18920
- command,
18921
- args: args2,
18922
- decision: rule.default,
18923
- reason: `Default for "${command}"`,
18924
- matchedRule: `${command}:default`
18843
+ match: { anyArgMatches: [anyArgMatchesPattern(SCRIPT_RUNNERS)] },
18844
+ decision: "ask",
18845
+ reason: "Script runners can execute arbitrary code"
18925
18846
  };
18926
18847
  }
18927
- var XARGS_SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["E", "I", "L", "n", "P", "s", "S", "d", "a"]);
18928
- var XARGS_SHORT_FLAGS_NO_VALUE = /* @__PURE__ */ new Set(["0", "e", "o", "p", "r", "t", "x"]);
18929
- var XARGS_LONG_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
18930
- "--eof",
18931
- "--replace",
18932
- "--max-lines",
18933
- "--max-args",
18934
- "--max-procs",
18935
- "--max-chars",
18936
- "--arg-file",
18937
- "--delimiter"
18938
- ]);
18939
- var XARGS_LONG_FLAGS_NO_VALUE = /* @__PURE__ */ new Set([
18940
- "--null",
18941
- "--exit",
18942
- "--open-tty",
18943
- "--interactive",
18944
- "--no-run-if-empty",
18945
- "--verbose",
18946
- "--show-limits"
18947
- ]);
18948
- function parseXargsSubcommand(args2) {
18949
- let i = 0;
18950
- while (i < args2.length) {
18951
- const arg = args2[i];
18952
- if (arg === "--") {
18953
- i++;
18954
- break;
18955
- }
18956
- if (!arg.startsWith("-") || arg === "-") {
18957
- break;
18958
- }
18959
- if (arg.startsWith("--")) {
18960
- const eqIndex = arg.indexOf("=");
18961
- const longFlag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
18962
- if (XARGS_LONG_FLAGS_WITH_VALUE.has(longFlag)) {
18963
- if (eqIndex !== -1) {
18964
- i++;
18965
- continue;
18966
- }
18967
- if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
18968
- i += 2;
18969
- continue;
18970
- }
18971
- if (XARGS_LONG_FLAGS_NO_VALUE.has(longFlag)) {
18972
- i++;
18973
- continue;
18974
- }
18975
- return { subcommand: null, unresolved: true };
18976
- }
18977
- const short = arg[1];
18978
- if (XARGS_SHORT_FLAGS_WITH_VALUE.has(short)) {
18979
- if (arg.length > 2) {
18980
- i++;
18981
- continue;
18982
- }
18983
- if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
18984
- i += 2;
18985
- continue;
18986
- }
18987
- const grouped = arg.slice(1).split("");
18988
- const allKnownNoValue = grouped.every((ch) => XARGS_SHORT_FLAGS_NO_VALUE.has(ch));
18989
- if (allKnownNoValue) {
18990
- i++;
18991
- continue;
18992
- }
18993
- return { subcommand: null, unresolved: true };
18994
- }
18995
- if (i >= args2.length) {
18996
- return {
18997
- unresolved: false,
18998
- subcommand: {
18999
- command: "echo",
19000
- originalCommand: "echo",
19001
- args: [],
19002
- envPrefixes: [],
19003
- raw: "echo"
19004
- }
19005
- };
19006
- }
19007
- const subcommand = args2[i];
19008
- const subArgs = args2.slice(i + 1);
18848
+ function registryOpsPattern() {
19009
18849
  return {
19010
- unresolved: false,
19011
- subcommand: {
19012
- command: subcommand,
19013
- originalCommand: subcommand,
19014
- args: subArgs,
19015
- envPrefixes: [],
19016
- raw: [subcommand, ...subArgs].join(" ")
19017
- }
18850
+ match: { anyArgMatches: [anyArgMatchesPattern(REGISTRY_OPS)] },
18851
+ decision: "ask",
18852
+ reason: "Registry modification"
19018
18853
  };
19019
18854
  }
19020
- function evaluateXargsCommand(cmd, config, cwd, depth = 0) {
19021
- const { command, args: args2 } = cmd;
19022
- const { subcommand, unresolved } = parseXargsSubcommand(args2);
19023
- if (unresolved || !subcommand) {
19024
- return {
19025
- command,
19026
- args: args2,
19027
- decision: "ask",
19028
- reason: "xargs subcommand could not be resolved safely",
19029
- matchedRule: "xargs:subcommand"
19030
- };
19031
- }
19032
- const isShellExec = (subcommand.command === "sh" || subcommand.command === "bash" || subcommand.command === "zsh") && subcommand.args.length >= 2 && subcommand.args[0] === "-c";
19033
- let parsed;
19034
- if (isShellExec) {
19035
- const innerResult = parseCommand(subcommand.args[1]);
19036
- if (innerResult.parseError) {
19037
- parsed = { commands: [subcommand], hasSubshell: false, subshellCommands: [], parseError: false };
19038
- } else {
19039
- parsed = innerResult;
19040
- }
19041
- } else {
19042
- parsed = { commands: [subcommand], hasSubshell: false, subshellCommands: [], parseError: false };
19043
- }
19044
- const result = evaluate(parsed, config, cwd, depth + 1);
18855
+ function pkgManagerRule(command, extraSafeCmds = []) {
18856
+ const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
19045
18857
  return {
19046
18858
  command,
19047
- args: args2,
19048
- decision: result.decision,
19049
- reason: `xargs subcommand "${subcommand.command}": ${result.reason}`,
19050
- matchedRule: "xargs:subcommand"
18859
+ default: "ask",
18860
+ argPatterns: [
18861
+ registryOpsPattern(),
18862
+ {
18863
+ match: { anyArgMatches: [anyArgMatchesPattern(safeCmds)] },
18864
+ decision: "allow",
18865
+ description: `Standard ${command} commands`
18866
+ },
18867
+ VERSION_HELP_FLAGS
18868
+ ]
19051
18869
  };
19052
18870
  }
19053
- function parseFindExecCommands(args2) {
19054
- const commands = [];
19055
- let i = 0;
19056
- while (i < args2.length) {
19057
- if (args2[i] === "-exec" || args2[i] === "-execdir") {
19058
- i++;
19059
- const cmdArgs = [];
19060
- while (i < args2.length && args2[i] !== ";" && args2[i] !== "+") {
19061
- if (args2[i] !== "{}") {
19062
- cmdArgs.push(args2[i]);
19063
- }
19064
- i++;
19065
- }
19066
- i++;
19067
- if (cmdArgs.length > 0) {
19068
- commands.push({
19069
- command: cmdArgs[0],
19070
- originalCommand: cmdArgs[0],
19071
- args: cmdArgs.slice(1),
19072
- envPrefixes: [],
19073
- raw: cmdArgs.join(" ")
19074
- });
19075
- }
19076
- } else {
19077
- i++;
19078
- }
19079
- }
19080
- return commands;
19081
- }
19082
- function evaluateFindCommand(cmd, config, cwd, depth = 0) {
19083
- const { command, args: args2 } = cmd;
19084
- if (args2.some((a) => a === "-delete")) {
19085
- return { command, args: args2, decision: "ask", reason: "find -delete can remove files", matchedRule: "find:delete" };
19086
- }
19087
- if (args2.some((a) => a === "-ok" || a === "-okdir")) {
19088
- return { command, args: args2, decision: "ask", reason: "find -ok/-okdir can execute commands interactively", matchedRule: "find:ok" };
19089
- }
19090
- const execCommands = parseFindExecCommands(args2);
19091
- if (execCommands.length === 0) {
19092
- return { command, args: args2, decision: "allow", reason: "find without dangerous flags", matchedRule: "find:safe" };
19093
- }
19094
- for (const execCmd of execCommands) {
19095
- const parsed = {
19096
- commands: [execCmd],
19097
- hasSubshell: false,
19098
- subshellCommands: [],
19099
- parseError: false
19100
- };
19101
- const result = evaluate(parsed, config, cwd, depth + 1);
19102
- if (result.decision === "deny") {
19103
- return { command, args: args2, decision: "deny", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
19104
- }
19105
- if (result.decision === "ask") {
19106
- return { command, args: args2, decision: "ask", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
19107
- }
19108
- }
19109
- return { command, args: args2, decision: "allow", reason: "find -exec commands are safe", matchedRule: "find:exec" };
19110
- }
19111
- var SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19112
- "-b",
19113
- "-c",
19114
- "-D",
19115
- "-E",
19116
- "-e",
19117
- "-F",
19118
- "-I",
19119
- "-i",
19120
- "-J",
19121
- "-L",
19122
- "-l",
19123
- "-m",
19124
- "-O",
19125
- "-o",
19126
- "-p",
19127
- "-Q",
19128
- "-R",
19129
- "-S",
19130
- "-W",
19131
- "-w"
19132
- ]);
19133
- function findMatchingTarget(value, targets) {
19134
- return targets.find((t) => globToRegex(t.name).test(value)) || null;
19135
- }
19136
- function parseSSHArgs(args2) {
19137
- let host = null;
19138
- const remoteArgs = [];
19139
- let i = 0;
19140
- while (i < args2.length) {
19141
- const arg = args2[i];
19142
- if (SSH_FLAGS_WITH_VALUE.has(arg)) {
19143
- i += 2;
19144
- continue;
19145
- }
19146
- if (arg.startsWith("-")) {
19147
- i++;
19148
- continue;
19149
- }
19150
- if (!host) {
19151
- host = arg.includes("@") ? arg.split("@").pop() : arg;
19152
- i++;
19153
- while (i < args2.length) {
19154
- remoteArgs.push(args2[i]);
19155
- i++;
19156
- }
19157
- break;
19158
- }
19159
- i++;
19160
- }
19161
- return {
19162
- host,
19163
- remoteCommand: remoteArgs.length > 0 ? remoteArgs.map(shellQuote).join(" ") : null
19164
- };
19165
- }
19166
- function extractHostFromRemotePath(args2) {
19167
- for (const arg of args2) {
19168
- const match = arg.match(/^(?:[^@]+@)?([^:]+):/);
19169
- if (match) return match[1];
19170
- }
19171
- return null;
19172
- }
19173
- function evaluateSSHCommand(cmd, config, targets, depth = 0) {
19174
- const { command, args: args2 } = cmd;
19175
- if (command === "scp" || command === "rsync") {
19176
- const host2 = extractHostFromRemotePath(args2);
19177
- if (!host2) return null;
19178
- const target2 = findMatchingTarget(host2, targets);
19179
- if (!target2) return null;
19180
- if (target2.allowAll || !target2.overrides) {
19181
- return {
19182
- command,
19183
- args: args2,
19184
- decision: "allow",
19185
- reason: `Trusted SSH host "${host2}"${target2.allowAll ? " (allowAll)" : ""}`,
19186
- matchedRule: "trustedSSHHosts"
19187
- };
19188
- }
19189
- if (target2.overrides.alwaysDeny.some((name) => name === command)) {
19190
- return {
19191
- command,
19192
- args: args2,
19193
- decision: "deny",
19194
- reason: `Trusted SSH host "${host2}": "${command}" blocked by overrides`,
19195
- matchedRule: "trustedSSHHosts"
19196
- };
19197
- }
19198
- return {
19199
- command,
19200
- args: args2,
19201
- decision: "allow",
19202
- reason: `Trusted SSH host "${host2}"`,
19203
- matchedRule: "trustedSSHHosts"
19204
- };
19205
- }
19206
- const { host, remoteCommand } = parseSSHArgs(args2);
19207
- if (!host) return null;
19208
- const target = findMatchingTarget(host, targets);
19209
- if (!target) return null;
19210
- if (!remoteCommand) {
19211
- return {
19212
- command,
19213
- args: args2,
19214
- decision: "allow",
19215
- reason: `Trusted SSH host "${host}" (interactive)`,
19216
- matchedRule: "trustedSSHHosts"
19217
- };
19218
- }
19219
- if (target.allowAll) {
19220
- return {
19221
- command,
19222
- args: args2,
19223
- decision: "allow",
19224
- reason: `Trusted SSH host "${host}" (allowAll)`,
19225
- matchedRule: "trustedSSHHosts"
19226
- };
19227
- }
19228
- const parsed = parseCommand(remoteCommand);
19229
- const result = evaluate(parsed, configWithContextOverrides(config, target), depth + 1);
19230
- return {
19231
- command,
19232
- args: args2,
19233
- decision: result.decision,
19234
- reason: `Trusted SSH host "${host}": ${result.reason}`,
19235
- matchedRule: "trustedSSHHosts"
19236
- };
19237
- }
19238
- var DOCKER_EXEC_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19239
- "-e",
19240
- "--env",
19241
- "--env-file",
19242
- "-u",
19243
- "--user",
19244
- "-w",
19245
- "--workdir",
19246
- "--detach-keys"
19247
- ]);
19248
- var INTERACTIVE_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh"]);
19249
- function shellQuote(arg) {
19250
- if (/[\s"'\\$`!#&|;()<>]/.test(arg)) {
19251
- return `'${arg.replace(/'/g, "'\\''")}'`;
19252
- }
19253
- return arg;
19254
- }
19255
- function configWithContextOverrides(config, target) {
19256
- const overrideLayers = [];
19257
- if (target?.overrides) overrideLayers.push(target.overrides);
19258
- if (config.trustedContextOverrides) overrideLayers.push(config.trustedContextOverrides);
19259
- if (overrideLayers.length === 0) return config;
19260
- return {
19261
- ...config,
19262
- layers: [...overrideLayers, ...config.layers]
19263
- };
19264
- }
19265
- function evaluateRemoteCommand(remoteArgs, config, target, depth = 0) {
19266
- if (target?.allowAll) {
19267
- return { decision: "allow", reason: "allowAll target", details: [] };
19268
- }
19269
- const overriddenConfig = configWithContextOverrides(config, target);
19270
- if (remoteArgs.length === 0) {
19271
- return { decision: "allow", reason: "interactive", details: [] };
19272
- }
19273
- const remoteCmd = remoteArgs[0];
19274
- if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs.length === 1) {
19275
- return { decision: "allow", reason: "interactive shell", details: [] };
19276
- }
19277
- if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs[1] === "-c" && remoteArgs.length >= 3) {
19278
- const innerCommand = remoteArgs.slice(2).join(" ");
19279
- const parsed2 = parseCommand(innerCommand);
19280
- return evaluate(parsed2, overriddenConfig, depth + 1);
19281
- }
19282
- const parsed = {
19283
- commands: [{ command: remoteCmd, originalCommand: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
19284
- hasSubshell: false,
19285
- subshellCommands: [],
19286
- parseError: false
19287
- };
19288
- return evaluate(parsed, overriddenConfig, depth + 1);
19289
- }
19290
- function parseDockerExecArgs(args2) {
19291
- let target = null;
19292
- const remoteArgs = [];
19293
- let i = 0;
19294
- while (i < args2.length) {
19295
- const arg = args2[i];
19296
- if (DOCKER_EXEC_FLAGS_WITH_VALUE.has(arg)) {
19297
- i += 2;
19298
- continue;
19299
- }
19300
- if (arg.startsWith("-")) {
19301
- i++;
19302
- continue;
19303
- }
19304
- if (!target) {
19305
- target = arg;
19306
- i++;
19307
- while (i < args2.length) {
19308
- remoteArgs.push(args2[i]);
19309
- i++;
19310
- }
19311
- break;
19312
- }
19313
- i++;
19314
- }
19315
- return { target, remoteArgs };
19316
- }
19317
- function evaluateDockerExec(cmd, config, targets, depth = 0) {
19318
- const { command, args: args2 } = cmd;
19319
- if (args2[0] !== "exec") return null;
19320
- const { target: containerName, remoteArgs } = parseDockerExecArgs(args2.slice(1));
19321
- if (!containerName) return null;
19322
- const matched = findMatchingTarget(containerName, targets);
19323
- if (!matched) return null;
19324
- const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
19325
- return {
19326
- command,
19327
- args: args2,
19328
- decision: result.decision,
19329
- reason: `Trusted Docker container "${containerName}" (${result.reason})`,
19330
- matchedRule: "trustedDockerContainers"
19331
- };
19332
- }
19333
- var KUBECTL_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19334
- "-n",
19335
- "--namespace",
19336
- "-c",
19337
- "--container",
19338
- "--context",
19339
- "--cluster",
19340
- "--kubeconfig",
19341
- "-s",
19342
- "--server",
19343
- "--token",
19344
- "--user",
19345
- "--as",
19346
- "--as-group",
19347
- "--certificate-authority",
19348
- "--client-certificate",
19349
- "--client-key",
19350
- "-l",
19351
- "--selector",
19352
- "-f",
19353
- "--filename",
19354
- "--cache-dir",
19355
- "--request-timeout",
19356
- "-o",
19357
- "--output"
19358
- ]);
19359
- function parseKubectlExecArgs(args2) {
19360
- let context = null;
19361
- let pod = null;
19362
- const remoteArgs = [];
19363
- let i = 0;
19364
- while (i < args2.length) {
19365
- const arg = args2[i];
19366
- if (arg === "--") {
19367
- i++;
19368
- while (i < args2.length) {
19369
- remoteArgs.push(args2[i]);
19370
- i++;
19371
- }
19372
- break;
19373
- }
19374
- if (arg.startsWith("--") && arg.includes("=")) {
19375
- if (arg.startsWith("--context=")) {
19376
- context = arg.split("=")[1];
19377
- }
19378
- i++;
19379
- continue;
19380
- }
19381
- if (KUBECTL_FLAGS_WITH_VALUE.has(arg)) {
19382
- if (arg === "--context") context = args2[i + 1] || null;
19383
- i += 2;
19384
- continue;
19385
- }
19386
- if (arg.startsWith("-")) {
19387
- i++;
19388
- continue;
19389
- }
19390
- if (!pod) {
19391
- pod = arg;
19392
- }
19393
- i++;
19394
- }
19395
- return { context, pod, remoteArgs };
19396
- }
19397
- function evaluateKubectlExec(cmd, config, targets, depth = 0) {
19398
- const { command, args: args2 } = cmd;
19399
- if (args2[0] !== "exec") return null;
19400
- const { context, pod, remoteArgs } = parseKubectlExecArgs(args2.slice(1));
19401
- if (!context) return null;
19402
- const matched = findMatchingTarget(context, targets);
19403
- if (!matched) return null;
19404
- const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
19405
- return {
19406
- command,
19407
- args: args2,
19408
- decision: result.decision,
19409
- reason: `Trusted kubectl context "${context}"${pod ? `, pod "${pod}"` : ""} (${result.reason})`,
19410
- matchedRule: "trustedKubectlContexts"
19411
- };
19412
- }
19413
- var SPRITE_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19414
- "-o",
19415
- "--org",
19416
- "-s",
19417
- "--sprite"
19418
- ]);
19419
- function parseSpriteExecArgs(args2) {
19420
- let spriteName = null;
19421
- const remoteArgs = [];
19422
- let foundExec = false;
19423
- let i = 0;
19424
- while (i < args2.length) {
19425
- const arg = args2[i];
19426
- if (arg.startsWith("--") && arg.includes("=")) {
19427
- if (arg.startsWith("--sprite=")) {
19428
- spriteName = arg.split("=")[1];
19429
- }
19430
- i++;
19431
- continue;
19432
- }
19433
- if (SPRITE_FLAGS_WITH_VALUE.has(arg)) {
19434
- if (arg === "-s" || arg === "--sprite") {
19435
- spriteName = args2[i + 1] || null;
19436
- }
19437
- i += 2;
19438
- continue;
19439
- }
19440
- if (arg === "--debug") {
19441
- i++;
19442
- continue;
19443
- }
19444
- if (arg.startsWith("-")) {
19445
- i++;
19446
- continue;
19447
- }
19448
- if (!foundExec) {
19449
- if (arg === "exec" || arg === "x" || arg === "console" || arg === "c") {
19450
- foundExec = true;
19451
- i++;
19452
- continue;
19453
- }
19454
- return { spriteName: null, remoteArgs: [] };
19455
- }
19456
- while (i < args2.length) {
19457
- remoteArgs.push(args2[i]);
19458
- i++;
19459
- }
19460
- break;
19461
- }
19462
- return { spriteName, remoteArgs };
19463
- }
19464
- function evaluateSpriteExec(cmd, config, targets, depth = 0) {
19465
- const { command, args: args2 } = cmd;
19466
- const { spriteName, remoteArgs } = parseSpriteExecArgs(args2);
19467
- if (!spriteName) return null;
19468
- const matched = findMatchingTarget(spriteName, targets);
19469
- if (!matched) return null;
19470
- const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
19471
- return {
19472
- command,
19473
- args: args2,
19474
- decision: result.decision,
19475
- reason: `Trusted sprite "${spriteName}" (${result.reason})`,
19476
- matchedRule: "trustedSprites"
19477
- };
19478
- }
19479
- var FLY_SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19480
- "-a",
19481
- "--app",
19482
- "-C",
19483
- "--command",
19484
- "-o",
19485
- "--org",
19486
- "-r",
19487
- "--region",
19488
- "-u",
19489
- "--user",
19490
- "--address"
19491
- ]);
19492
- function parseFlySSHArgs(args2) {
19493
- let app = null;
19494
- const remoteArgs = [];
19495
- let isSSH = false;
19496
- let foundConsole = false;
19497
- let i = 0;
19498
- while (i < args2.length) {
19499
- const arg = args2[i];
19500
- if (arg.startsWith("--app=")) {
19501
- app = arg.slice(6);
19502
- i++;
19503
- continue;
19504
- }
19505
- if (FLY_SSH_FLAGS_WITH_VALUE.has(arg)) {
19506
- if (arg === "-a" || arg === "--app") {
19507
- app = args2[i + 1] || null;
19508
- }
19509
- if ((arg === "-C" || arg === "--command") && foundConsole) {
19510
- const cmdValue = args2[i + 1];
19511
- if (cmdValue) {
19512
- const parsed = parseCommand(cmdValue);
19513
- if (!parsed.parseError && parsed.commands.length > 0) {
19514
- const cmd = parsed.commands[0];
19515
- remoteArgs.push(cmd.command, ...cmd.args);
19516
- }
19517
- }
19518
- i += 2;
19519
- continue;
19520
- }
19521
- i += 2;
19522
- continue;
19523
- }
19524
- if (arg === "--") {
19525
- i++;
19526
- while (i < args2.length) {
19527
- remoteArgs.push(args2[i]);
19528
- i++;
19529
- }
19530
- break;
19531
- }
19532
- if (arg.startsWith("-")) {
19533
- i++;
19534
- continue;
19535
- }
19536
- if (!isSSH && arg === "ssh") {
19537
- isSSH = true;
19538
- i++;
19539
- continue;
19540
- }
19541
- if (isSSH && !foundConsole && (arg === "console" || arg === "sftp")) {
19542
- foundConsole = true;
19543
- i++;
19544
- continue;
19545
- }
19546
- i++;
19547
- }
19548
- return { app, remoteArgs, isSSH: isSSH && foundConsole };
19549
- }
19550
- function evaluateFlyCommand(cmd, config, targets, depth = 0) {
19551
- const { command, args: args2 } = cmd;
19552
- const { app, remoteArgs, isSSH } = parseFlySSHArgs(args2);
19553
- if (!isSSH) return null;
19554
- if (!app) return null;
19555
- const matched = findMatchingTarget(app, targets);
19556
- if (!matched) return null;
19557
- const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
19558
- return {
19559
- command,
19560
- args: args2,
19561
- decision: result.decision,
19562
- reason: `Trusted Fly app "${app}" (${result.reason})`,
19563
- matchedRule: "trustedFlyApps"
19564
- };
19565
- }
19566
-
19567
- // src/codex.ts
19568
- function toCodexDecision(decision) {
19569
- if (decision === "allow") return "allow";
19570
- if (decision === "ask") return "prompt";
19571
- return "forbidden";
19572
- }
19573
- function quote(value) {
19574
- return JSON.stringify(value);
19575
- }
19576
- function collectCandidateCommands(config) {
19577
- const names = /* @__PURE__ */ new Set();
19578
- for (const layer of config.layers) {
19579
- for (const name of layer.alwaysAllow) names.add(name);
19580
- for (const name of layer.alwaysDeny) names.add(name);
19581
- for (const rule of layer.rules) names.add(rule.command);
19582
- }
19583
- return [...names].map((n) => n.trim()).filter((n) => n.length > 0 && !/\s/.test(n)).sort();
19584
- }
19585
- function buildCodexRuleRecords(config) {
19586
- const records = [];
19587
- for (const command of collectCandidateCommands(config)) {
19588
- const result = evaluate(parseCommand(command), config);
19589
- records.push({
19590
- command,
19591
- decision: result.decision,
19592
- reason: result.reason
19593
- });
19594
- }
19595
- return records;
19596
- }
19597
- function generateCodexRules(config) {
19598
- const lines = [
19599
- "# Generated by claude-warden.",
19600
- "# Regenerate with: pnpm codex:export-rules",
19601
- ""
19602
- ];
19603
- for (const record of buildCodexRuleRecords(config)) {
19604
- lines.push(
19605
- `prefix_rule(pattern = [${quote(record.command)}], decision = ${quote(toCodexDecision(record.decision))}, justification = ${quote(`Warden: ${record.reason}`)})`
19606
- );
19607
- }
19608
- lines.push("");
19609
- return lines.join("\n");
19610
- }
19611
-
19612
- // src/rules.ts
19613
- var import_fs = require("fs");
19614
- var import_yaml = __toESM(require_dist2(), 1);
19615
- var import_os2 = require("os");
19616
- var import_path3 = require("path");
19617
-
19618
- // src/defaults.ts
19619
- var SAFE_DEV_TOOLS = [
19620
- "jest",
19621
- "vitest",
19622
- "tsc",
19623
- "eslint",
19624
- "prettier",
19625
- "mkdirp",
19626
- "concurrently",
19627
- "turbo",
19628
- "next",
19629
- "nuxt",
19630
- "vite",
19631
- "astro",
19632
- "playwright",
19633
- "cypress",
19634
- "mocha",
19635
- "nyc",
19636
- "c8",
19637
- "ts-jest",
19638
- "tsup",
19639
- "esbuild",
19640
- "rollup",
19641
- "webpack",
19642
- "prisma",
19643
- "drizzle-kit",
19644
- "typeorm",
19645
- "knex",
19646
- "sequelize-cli",
19647
- "tailwindcss",
19648
- "postcss",
19649
- "autoprefixer",
19650
- "lint-staged",
19651
- "husky",
19652
- "changeset",
19653
- "semantic-release",
19654
- "lerna",
19655
- "nx",
19656
- "create-react-app",
19657
- "create-next-app",
19658
- "create-vite",
19659
- "degit",
19660
- "storybook",
19661
- "wrangler",
19662
- "netlify",
19663
- "vercel",
19664
- "json",
19665
- "biome"
19666
- ];
19667
- var SCRIPT_RUNNERS = ["tsx", "ts-node", "nodemon"];
19668
- var REGISTRY_OPS = ["publish", "unpublish", "deprecate", "owner", "access", "token", "adduser", "login", "logout"];
19669
- var SAFE_PKG_MANAGER_CMDS = [
19670
- "install",
19671
- "add",
19672
- "remove",
19673
- "uninstall",
19674
- "update",
19675
- "upgrade",
19676
- "outdated",
19677
- "ls",
19678
- "list",
19679
- "run",
19680
- "test",
19681
- "start",
19682
- "build",
19683
- "init",
19684
- "create",
19685
- "info",
19686
- "view",
19687
- "show",
19688
- "why",
19689
- "pack",
19690
- "cache",
19691
- "config",
19692
- "get",
19693
- "set",
19694
- "version",
19695
- "help",
19696
- "exec",
19697
- "dedupe",
19698
- "prune",
19699
- "audit",
19700
- "completion",
19701
- "whoami"
19702
- ];
19703
- var VERSION_HELP_FLAGS = {
19704
- match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] },
19705
- decision: "allow",
19706
- description: "Version/help flags"
19707
- };
19708
- function anyArgMatchesPattern(items) {
19709
- return `^(${items.join("|")})$`;
19710
- }
19711
- function safeDevToolsPattern() {
19712
- return {
19713
- match: { anyArgMatches: [anyArgMatchesPattern(SAFE_DEV_TOOLS)] },
19714
- decision: "allow",
19715
- description: "Well-known dev tools"
19716
- };
19717
- }
19718
- function scriptRunnersPattern() {
19719
- return {
19720
- match: { anyArgMatches: [anyArgMatchesPattern(SCRIPT_RUNNERS)] },
19721
- decision: "ask",
19722
- reason: "Script runners can execute arbitrary code"
19723
- };
19724
- }
19725
- function registryOpsPattern() {
19726
- return {
19727
- match: { anyArgMatches: [anyArgMatchesPattern(REGISTRY_OPS)] },
19728
- decision: "ask",
19729
- reason: "Registry modification"
19730
- };
19731
- }
19732
- function pkgManagerRule(command, extraSafeCmds = []) {
19733
- const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
19734
- return {
19735
- command,
19736
- default: "ask",
19737
- argPatterns: [
19738
- registryOpsPattern(),
19739
- {
19740
- match: { anyArgMatches: [anyArgMatchesPattern(safeCmds)] },
19741
- decision: "allow",
19742
- description: `Standard ${command} commands`
19743
- },
19744
- VERSION_HELP_FLAGS
19745
- ]
19746
- };
19747
- }
19748
- function pkgRunnerRule(command) {
19749
- return {
19750
- command,
19751
- default: "ask",
19752
- argPatterns: [
19753
- safeDevToolsPattern(),
19754
- scriptRunnersPattern(),
19755
- VERSION_HELP_FLAGS
19756
- ]
19757
- };
18871
+ function pkgRunnerRule(command) {
18872
+ return {
18873
+ command,
18874
+ default: "ask",
18875
+ argPatterns: [
18876
+ safeDevToolsPattern(),
18877
+ scriptRunnersPattern(),
18878
+ VERSION_HELP_FLAGS
18879
+ ]
18880
+ };
19758
18881
  }
19759
18882
  var DEFAULT_CONFIG = {
19760
18883
  defaultDecision: "ask",
@@ -20337,319 +19460,1181 @@ var DEFAULT_CONFIG = {
20337
19460
  }]
20338
19461
  };
20339
19462
 
20340
- // src/rules.ts
20341
- var VALID_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
20342
- function isValidDecision(value) {
20343
- return VALID_DECISIONS.has(value);
19463
+ // src/rules.ts
19464
+ var VALID_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
19465
+ function isValidDecision(value) {
19466
+ return VALID_DECISIONS.has(value);
19467
+ }
19468
+ var quiet = true;
19469
+ function setQuiet(value) {
19470
+ quiet = value;
19471
+ }
19472
+ function warn(message) {
19473
+ if (quiet) return;
19474
+ process.stderr.write(message);
19475
+ }
19476
+ var USER_CONFIG_PATHS = [
19477
+ (0, import_path3.join)((0, import_os.homedir)(), ".claude", "warden.yaml"),
19478
+ (0, import_path3.join)((0, import_os.homedir)(), ".claude", "warden.json")
19479
+ ];
19480
+ var PROJECT_CONFIG_NAMES = [
19481
+ ".claude/warden.yaml",
19482
+ ".claude/warden.json"
19483
+ ];
19484
+ function loadConfig(cwd) {
19485
+ const config = structuredClone(DEFAULT_CONFIG);
19486
+ const defaultLayer = config.layers[0];
19487
+ let userLayer = null;
19488
+ let userRaw = null;
19489
+ for (const configPath of USER_CONFIG_PATHS) {
19490
+ const result = tryLoadFile(configPath);
19491
+ if (result) {
19492
+ userLayer = extractLayer(result);
19493
+ userRaw = result;
19494
+ break;
19495
+ }
19496
+ }
19497
+ let workspaceLayer = null;
19498
+ let workspaceRaw = null;
19499
+ if (cwd) {
19500
+ for (const name of PROJECT_CONFIG_NAMES) {
19501
+ const result = tryLoadFile((0, import_path3.join)(cwd, name));
19502
+ if (result) {
19503
+ workspaceLayer = extractLayer(result);
19504
+ workspaceRaw = result;
19505
+ break;
19506
+ }
19507
+ }
19508
+ }
19509
+ config.layers = [
19510
+ ...workspaceLayer ? [workspaceLayer] : [],
19511
+ ...userLayer ? [userLayer] : [],
19512
+ defaultLayer
19513
+ ];
19514
+ if (userRaw) mergeNonLayerFields(config, userRaw);
19515
+ if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19516
+ return config;
19517
+ }
19518
+ function tryLoadFile(filePath) {
19519
+ if (!(0, import_fs.existsSync)(filePath)) return null;
19520
+ try {
19521
+ const raw = (0, import_fs.readFileSync)(filePath, "utf-8");
19522
+ const parsed = filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
19523
+ if (parsed && typeof parsed === "object") {
19524
+ return parsed;
19525
+ }
19526
+ } catch (err) {
19527
+ warn(`[warden] Warning: failed to parse config ${filePath}: ${err instanceof Error ? err.message : String(err)}
19528
+ `);
19529
+ }
19530
+ return null;
19531
+ }
19532
+ function extractLayer(raw) {
19533
+ const rules = Array.isArray(raw.rules) ? raw.rules : [];
19534
+ for (const rule of rules) {
19535
+ if (rule && typeof rule === "object") {
19536
+ if (rule.default && !isValidDecision(rule.default)) {
19537
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19538
+ `);
19539
+ rule.default = "ask";
19540
+ }
19541
+ if (Array.isArray(rule.argPatterns)) {
19542
+ for (const pattern of rule.argPatterns) {
19543
+ if (pattern?.decision && !isValidDecision(pattern.decision)) {
19544
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19545
+ `);
19546
+ pattern.decision = "ask";
19547
+ }
19548
+ }
19549
+ }
19550
+ }
19551
+ }
19552
+ return {
19553
+ alwaysAllow: Array.isArray(raw.alwaysAllow) ? raw.alwaysAllow : [],
19554
+ alwaysDeny: Array.isArray(raw.alwaysDeny) ? raw.alwaysDeny : [],
19555
+ rules
19556
+ };
19557
+ }
19558
+ function parseTrustedList(raw) {
19559
+ return raw.map((entry) => {
19560
+ if (typeof entry === "string") return { name: entry };
19561
+ if (entry && typeof entry === "object" && "name" in entry) {
19562
+ const obj = entry;
19563
+ const target = { name: String(obj.name) };
19564
+ if (obj.allowAll === true) target.allowAll = true;
19565
+ if (obj.overrides && typeof obj.overrides === "object") {
19566
+ target.overrides = extractLayer(obj.overrides);
19567
+ }
19568
+ return target;
19569
+ }
19570
+ return null;
19571
+ }).filter((t) => t !== null);
19572
+ }
19573
+ var VALID_REMOTE_CONTEXTS = /* @__PURE__ */ new Set(["ssh", "docker", "kubectl", "sprite", "fly"]);
19574
+ function parseTrustedRemotes(raw) {
19575
+ const results = [];
19576
+ for (const entry of raw) {
19577
+ if (!entry || typeof entry !== "object") continue;
19578
+ const obj = entry;
19579
+ const context = String(obj.context || "");
19580
+ if (!VALID_REMOTE_CONTEXTS.has(context)) {
19581
+ warn(`[warden] Warning: unknown remote context "${context}", skipping
19582
+ `);
19583
+ continue;
19584
+ }
19585
+ const name = String(obj.name || "");
19586
+ if (!name) continue;
19587
+ const remote = { name, context };
19588
+ if (obj.allowAll === true) remote.allowAll = true;
19589
+ if (obj.overrides && typeof obj.overrides === "object") {
19590
+ remote.overrides = extractLayer(obj.overrides);
19591
+ }
19592
+ results.push(remote);
19593
+ }
19594
+ return results;
19595
+ }
19596
+ function parseTargetPolicies(raw) {
19597
+ const results = [];
19598
+ for (const entry of raw) {
19599
+ if (!entry || typeof entry !== "object") continue;
19600
+ const obj = entry;
19601
+ const type = String(obj.type || "");
19602
+ const rawDecision = String(obj.decision || "allow");
19603
+ const decision = isValidDecision(rawDecision) ? rawDecision : "allow";
19604
+ const base = {
19605
+ commands: Array.isArray(obj.commands) ? obj.commands.map(String) : void 0,
19606
+ decision,
19607
+ reason: obj.reason ? String(obj.reason) : void 0,
19608
+ allowAll: obj.allowAll === true ? true : void 0
19609
+ };
19610
+ switch (type) {
19611
+ case "path": {
19612
+ const path = String(obj.path || "");
19613
+ if (!path) continue;
19614
+ const policy = { ...base, type: "path", path };
19615
+ if (obj.recursive === false) policy.recursive = false;
19616
+ results.push(policy);
19617
+ break;
19618
+ }
19619
+ case "database": {
19620
+ const host = String(obj.host || "");
19621
+ if (!host) continue;
19622
+ const policy = { ...base, type: "database", host };
19623
+ if (typeof obj.port === "number") policy.port = obj.port;
19624
+ if (obj.database) policy.database = String(obj.database);
19625
+ results.push(policy);
19626
+ break;
19627
+ }
19628
+ case "endpoint": {
19629
+ const pattern = String(obj.pattern || "");
19630
+ if (!pattern) continue;
19631
+ results.push({ ...base, type: "endpoint", pattern });
19632
+ break;
19633
+ }
19634
+ default:
19635
+ warn(`[warden] Warning: unknown target policy type "${type}", skipping
19636
+ `);
19637
+ }
19638
+ }
19639
+ return results;
19640
+ }
19641
+ function parseLegacyPaths(raw) {
19642
+ const results = [];
19643
+ for (const e of raw) {
19644
+ if (!e || typeof e !== "object") continue;
19645
+ const obj = e;
19646
+ const decision = String(obj.decision || "allow");
19647
+ if (!isValidDecision(decision)) continue;
19648
+ const path = String(obj.path || "");
19649
+ if (!path) continue;
19650
+ const tp = { type: "path", path, decision };
19651
+ if (obj.recursive === false) tp.recursive = false;
19652
+ if (Array.isArray(obj.commands)) tp.commands = obj.commands.map(String);
19653
+ if (obj.reason) tp.reason = String(obj.reason);
19654
+ results.push(tp);
19655
+ }
19656
+ return results;
19657
+ }
19658
+ function parseLegacyDatabases(raw) {
19659
+ const results = [];
19660
+ for (const e of raw) {
19661
+ if (!e || typeof e !== "object") continue;
19662
+ const obj = e;
19663
+ const decision = String(obj.decision || "allow");
19664
+ if (!isValidDecision(decision)) continue;
19665
+ const host = String(obj.host || "");
19666
+ if (!host) continue;
19667
+ const td = { type: "database", host, decision };
19668
+ if (typeof obj.port === "number") td.port = obj.port;
19669
+ if (obj.database) td.database = String(obj.database);
19670
+ if (Array.isArray(obj.commands)) td.commands = obj.commands.map(String);
19671
+ if (obj.reason) td.reason = String(obj.reason);
19672
+ results.push(td);
19673
+ }
19674
+ return results;
19675
+ }
19676
+ function parseLegacyEndpoints(raw) {
19677
+ const results = [];
19678
+ for (const e of raw) {
19679
+ if (!e || typeof e !== "object") continue;
19680
+ const obj = e;
19681
+ const decision = String(obj.decision || "allow");
19682
+ if (!isValidDecision(decision)) continue;
19683
+ const pattern = String(obj.pattern || "");
19684
+ if (!pattern) continue;
19685
+ const te = { type: "endpoint", pattern, decision };
19686
+ if (Array.isArray(obj.commands)) te.commands = obj.commands.map(String);
19687
+ if (obj.reason) te.reason = String(obj.reason);
19688
+ results.push(te);
19689
+ }
19690
+ return results;
19691
+ }
19692
+ var LEGACY_REMOTE_MAP = {
19693
+ trustedSSHHosts: "ssh",
19694
+ trustedDockerContainers: "docker",
19695
+ trustedKubectlContexts: "kubectl",
19696
+ trustedSprites: "sprite",
19697
+ trustedFlyApps: "fly"
19698
+ };
19699
+ function mergeNonLayerFields(config, raw) {
19700
+ if (Array.isArray(raw.trustedRemotes)) {
19701
+ config.trustedRemotes = [...config.trustedRemotes || [], ...parseTrustedRemotes(raw.trustedRemotes)];
19702
+ }
19703
+ if (Array.isArray(raw.targetPolicies)) {
19704
+ config.targetPolicies = [...config.targetPolicies || [], ...parseTargetPolicies(raw.targetPolicies)];
19705
+ }
19706
+ for (const [key, context] of Object.entries(LEGACY_REMOTE_MAP)) {
19707
+ if (Array.isArray(raw[key])) {
19708
+ const remotes = parseTrustedList(raw[key]).map((t) => ({ ...t, context }));
19709
+ config.trustedRemotes = [...config.trustedRemotes || [], ...remotes];
19710
+ }
19711
+ }
19712
+ if (Array.isArray(raw.trustedPaths)) {
19713
+ config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyPaths(raw.trustedPaths)];
19714
+ }
19715
+ if (Array.isArray(raw.trustedDatabases)) {
19716
+ config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyDatabases(raw.trustedDatabases)];
19717
+ }
19718
+ if (Array.isArray(raw.trustedEndpoints)) {
19719
+ config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyEndpoints(raw.trustedEndpoints)];
19720
+ }
19721
+ if (typeof raw.defaultDecision === "string") {
19722
+ if (isValidDecision(raw.defaultDecision)) {
19723
+ config.defaultDecision = raw.defaultDecision;
19724
+ } else {
19725
+ warn(`[warden] Warning: invalid defaultDecision "${raw.defaultDecision}", ignoring
19726
+ `);
19727
+ }
19728
+ }
19729
+ if (typeof raw.askOnSubshell === "boolean") {
19730
+ config.askOnSubshell = raw.askOnSubshell;
19731
+ }
19732
+ if (typeof raw.notifyOnAsk === "boolean") {
19733
+ config.notifyOnAsk = raw.notifyOnAsk;
19734
+ }
19735
+ if (typeof raw.notifyOnDeny === "boolean") {
19736
+ config.notifyOnDeny = raw.notifyOnDeny;
19737
+ }
19738
+ if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19739
+ const overrides = raw.trustedContextOverrides;
19740
+ const layer = extractLayer(overrides);
19741
+ if (config.trustedContextOverrides) {
19742
+ config.trustedContextOverrides = {
19743
+ alwaysAllow: [...layer.alwaysAllow, ...config.trustedContextOverrides.alwaysAllow],
19744
+ alwaysDeny: [...layer.alwaysDeny, ...config.trustedContextOverrides.alwaysDeny],
19745
+ rules: [...layer.rules, ...config.trustedContextOverrides.rules]
19746
+ };
19747
+ } else {
19748
+ config.trustedContextOverrides = layer;
19749
+ }
19750
+ }
19751
+ }
19752
+
19753
+ // src/evaluator.ts
19754
+ function safeRegexTest(pattern, input) {
19755
+ try {
19756
+ return new RegExp(pattern).test(input);
19757
+ } catch {
19758
+ warn(`[warden] Warning: invalid regex pattern: ${pattern}
19759
+ `);
19760
+ return false;
19761
+ }
19762
+ }
19763
+ function commandMatchesName(cmd, name) {
19764
+ if (name.startsWith("/")) {
19765
+ return cmd.originalCommand === name;
19766
+ }
19767
+ if (name.startsWith("~/")) {
19768
+ return cmd.originalCommand === (0, import_os2.homedir)() + name.slice(1);
19769
+ }
19770
+ return cmd.command === name;
19771
+ }
19772
+ var MAX_RECURSION_DEPTH = 10;
19773
+ function evaluate(parsed, config, cwdOrDepth, maybeDepth) {
19774
+ const cwd = typeof cwdOrDepth === "string" ? cwdOrDepth : void 0;
19775
+ const depth = typeof cwdOrDepth === "number" ? cwdOrDepth : maybeDepth ?? 0;
19776
+ if (depth > MAX_RECURSION_DEPTH) {
19777
+ return { decision: "ask", reason: "Maximum recursion depth exceeded", details: [] };
19778
+ }
19779
+ if (parsed.parseError) {
19780
+ return { decision: "ask", reason: "Could not parse command safely", details: [] };
19781
+ }
19782
+ if (parsed.commands.length === 0) {
19783
+ return { decision: "allow", reason: "Empty command", details: [] };
19784
+ }
19785
+ if (parsed.hasSubshell && parsed.subshellCommands.length > 0) {
19786
+ for (const subCmd of parsed.subshellCommands) {
19787
+ const subParsed = parseCommand(subCmd);
19788
+ const subResult = evaluate(subParsed, config, cwd, depth + 1);
19789
+ if (subResult.decision === "deny") {
19790
+ return { decision: "deny", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
19791
+ }
19792
+ if (subResult.decision === "ask") {
19793
+ return { decision: "ask", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
19794
+ }
19795
+ }
19796
+ } else if (parsed.hasSubshell && parsed.subshellCommands.length === 0 && config.askOnSubshell) {
19797
+ return { decision: "ask", reason: "Command contains subshell/command substitution", details: [] };
19798
+ }
19799
+ const details = [];
19800
+ for (const cmd of parsed.commands) {
19801
+ details.push(evaluateCommand(cmd, config, cwd, depth));
19802
+ }
19803
+ const decisions = details.map((d) => d.decision);
19804
+ if (decisions.includes("deny")) {
19805
+ const denied = details.filter((d) => d.decision === "deny");
19806
+ return {
19807
+ decision: "deny",
19808
+ reason: denied.map((d) => `${d.command}: ${d.reason}`).join("; "),
19809
+ details
19810
+ };
19811
+ }
19812
+ if (decisions.includes("ask")) {
19813
+ const asked = details.filter((d) => d.decision === "ask");
19814
+ return {
19815
+ decision: "ask",
19816
+ reason: asked.map((d) => `${d.command}: ${d.reason}`).join("; "),
19817
+ details
19818
+ };
19819
+ }
19820
+ return { decision: "allow", reason: "All commands are safe", details };
19821
+ }
19822
+ function evaluateCommand(cmd, config, cwd, depth = 0) {
19823
+ const { command, args: args2 } = cmd;
19824
+ for (const layer of config.layers) {
19825
+ if (layer.alwaysDeny.some((name) => commandMatchesName(cmd, name))) {
19826
+ return { command, args: args2, decision: "deny", reason: `"${command}" is blocked`, matchedRule: "alwaysDeny" };
19827
+ }
19828
+ if (layer.alwaysAllow.some((name) => commandMatchesName(cmd, name))) {
19829
+ return { command, args: args2, decision: "allow", reason: `"${command}" is safe`, matchedRule: "alwaysAllow" };
19830
+ }
19831
+ }
19832
+ if (cwd) {
19833
+ const targetResult = evaluateTargetPolicies(cmd, cwd, config);
19834
+ if (targetResult) return targetResult;
19835
+ }
19836
+ const remotes = config.trustedRemotes || [];
19837
+ if (command === "ssh" || command === "scp" || command === "rsync") {
19838
+ const sshTargets = remotes.filter((r) => r.context === "ssh");
19839
+ if (sshTargets.length) {
19840
+ const sshResult = evaluateSSHCommand(cmd, config, sshTargets, depth);
19841
+ if (sshResult) return sshResult;
19842
+ }
19843
+ }
19844
+ if (command === "docker") {
19845
+ const dockerTargets = remotes.filter((r) => r.context === "docker");
19846
+ if (dockerTargets.length) {
19847
+ const dockerResult = evaluateDockerExec(cmd, config, dockerTargets, depth);
19848
+ if (dockerResult) return dockerResult;
19849
+ }
19850
+ }
19851
+ if (command === "kubectl") {
19852
+ const kubectlTargets = remotes.filter((r) => r.context === "kubectl");
19853
+ if (kubectlTargets.length) {
19854
+ const kubectlResult = evaluateKubectlExec(cmd, config, kubectlTargets, depth);
19855
+ if (kubectlResult) return kubectlResult;
19856
+ }
19857
+ }
19858
+ if (command === "sprite") {
19859
+ const spriteTargets = remotes.filter((r) => r.context === "sprite");
19860
+ if (spriteTargets.length) {
19861
+ const spriteResult = evaluateSpriteExec(cmd, config, spriteTargets, depth);
19862
+ if (spriteResult) return spriteResult;
19863
+ }
19864
+ }
19865
+ if (command === "fly" || command === "flyctl") {
19866
+ const flyTargets = remotes.filter((r) => r.context === "fly");
19867
+ if (flyTargets.length) {
19868
+ const flyResult = evaluateFlyCommand(cmd, config, flyTargets, depth);
19869
+ if (flyResult) return flyResult;
19870
+ }
19871
+ }
19872
+ if (command === "xargs") {
19873
+ return evaluateXargsCommand(cmd, config, cwd, depth);
19874
+ }
19875
+ if (command === "find") {
19876
+ return evaluateFindCommand(cmd, config, cwd, depth);
19877
+ }
19878
+ const mergedRule = collectMergedRule(cmd, config);
19879
+ if (mergedRule) {
19880
+ return evaluateRule(cmd, mergedRule);
19881
+ }
19882
+ return { command, args: args2, decision: config.defaultDecision, reason: `No rule for "${command}"`, matchedRule: "default" };
19883
+ }
19884
+ function collectMergedRule(cmd, config) {
19885
+ const matchingRules = [];
19886
+ for (const layer of config.layers) {
19887
+ const rule = layer.rules.find((r) => commandMatchesName(cmd, r.command));
19888
+ if (rule) {
19889
+ matchingRules.push(rule);
19890
+ if (rule.override) break;
19891
+ }
19892
+ }
19893
+ if (matchingRules.length === 0) return null;
19894
+ if (matchingRules.length === 1) return matchingRules[0];
19895
+ const mergedPatterns = [];
19896
+ for (const rule of matchingRules) {
19897
+ if (rule.argPatterns) {
19898
+ mergedPatterns.push(...rule.argPatterns);
19899
+ }
19900
+ }
19901
+ return {
19902
+ command: matchingRules[0].command,
19903
+ default: matchingRules[0].default,
19904
+ argPatterns: mergedPatterns
19905
+ };
19906
+ }
19907
+ function evaluateRule(cmd, rule) {
19908
+ const { command, args: args2 } = cmd;
19909
+ const argsJoined = args2.join(" ");
19910
+ for (const pattern of rule.argPatterns || []) {
19911
+ const m = pattern.match;
19912
+ let matched = true;
19913
+ if (m.noArgs !== void 0) {
19914
+ matched = matched && m.noArgs === (args2.length === 0);
19915
+ }
19916
+ if (m.argsMatch && matched) {
19917
+ matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
19918
+ }
19919
+ if (m.anyArgMatches && matched) {
19920
+ matched = args2.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
19921
+ }
19922
+ if (m.argCount && matched) {
19923
+ if (m.argCount.min !== void 0) matched = matched && args2.length >= m.argCount.min;
19924
+ if (m.argCount.max !== void 0) matched = matched && args2.length <= m.argCount.max;
19925
+ }
19926
+ if (m.not) matched = !matched;
19927
+ if (matched) {
19928
+ return {
19929
+ command,
19930
+ args: args2,
19931
+ decision: pattern.decision,
19932
+ reason: pattern.reason || pattern.description || `Matched pattern for "${command}"`,
19933
+ matchedRule: `${command}:argPattern`
19934
+ };
19935
+ }
19936
+ }
19937
+ return {
19938
+ command,
19939
+ args: args2,
19940
+ decision: rule.default,
19941
+ reason: `Default for "${command}"`,
19942
+ matchedRule: `${command}:default`
19943
+ };
19944
+ }
19945
+ var XARGS_SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["E", "I", "L", "n", "P", "s", "S", "d", "a"]);
19946
+ var XARGS_SHORT_FLAGS_NO_VALUE = /* @__PURE__ */ new Set(["0", "e", "o", "p", "r", "t", "x"]);
19947
+ var XARGS_LONG_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
19948
+ "--eof",
19949
+ "--replace",
19950
+ "--max-lines",
19951
+ "--max-args",
19952
+ "--max-procs",
19953
+ "--max-chars",
19954
+ "--arg-file",
19955
+ "--delimiter"
19956
+ ]);
19957
+ var XARGS_LONG_FLAGS_NO_VALUE = /* @__PURE__ */ new Set([
19958
+ "--null",
19959
+ "--exit",
19960
+ "--open-tty",
19961
+ "--interactive",
19962
+ "--no-run-if-empty",
19963
+ "--verbose",
19964
+ "--show-limits"
19965
+ ]);
19966
+ function parseXargsSubcommand(args2) {
19967
+ let i = 0;
19968
+ while (i < args2.length) {
19969
+ const arg = args2[i];
19970
+ if (arg === "--") {
19971
+ i++;
19972
+ break;
19973
+ }
19974
+ if (!arg.startsWith("-") || arg === "-") {
19975
+ break;
19976
+ }
19977
+ if (arg.startsWith("--")) {
19978
+ const eqIndex = arg.indexOf("=");
19979
+ const longFlag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
19980
+ if (XARGS_LONG_FLAGS_WITH_VALUE.has(longFlag)) {
19981
+ if (eqIndex !== -1) {
19982
+ i++;
19983
+ continue;
19984
+ }
19985
+ if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
19986
+ i += 2;
19987
+ continue;
19988
+ }
19989
+ if (XARGS_LONG_FLAGS_NO_VALUE.has(longFlag)) {
19990
+ i++;
19991
+ continue;
19992
+ }
19993
+ return { subcommand: null, unresolved: true };
19994
+ }
19995
+ const short = arg[1];
19996
+ if (XARGS_SHORT_FLAGS_WITH_VALUE.has(short)) {
19997
+ if (arg.length > 2) {
19998
+ i++;
19999
+ continue;
20000
+ }
20001
+ if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
20002
+ i += 2;
20003
+ continue;
20004
+ }
20005
+ const grouped = arg.slice(1).split("");
20006
+ const allKnownNoValue = grouped.every((ch) => XARGS_SHORT_FLAGS_NO_VALUE.has(ch));
20007
+ if (allKnownNoValue) {
20008
+ i++;
20009
+ continue;
20010
+ }
20011
+ return { subcommand: null, unresolved: true };
20012
+ }
20013
+ if (i >= args2.length) {
20014
+ return {
20015
+ unresolved: false,
20016
+ subcommand: {
20017
+ command: "echo",
20018
+ originalCommand: "echo",
20019
+ args: [],
20020
+ envPrefixes: [],
20021
+ raw: "echo"
20022
+ }
20023
+ };
20024
+ }
20025
+ const subcommand = args2[i];
20026
+ const subArgs = args2.slice(i + 1);
20027
+ return {
20028
+ unresolved: false,
20029
+ subcommand: {
20030
+ command: subcommand,
20031
+ originalCommand: subcommand,
20032
+ args: subArgs,
20033
+ envPrefixes: [],
20034
+ raw: [subcommand, ...subArgs].join(" ")
20035
+ }
20036
+ };
20037
+ }
20038
+ function evaluateXargsCommand(cmd, config, cwd, depth = 0) {
20039
+ const { command, args: args2 } = cmd;
20040
+ const { subcommand, unresolved } = parseXargsSubcommand(args2);
20041
+ if (unresolved || !subcommand) {
20042
+ return {
20043
+ command,
20044
+ args: args2,
20045
+ decision: "ask",
20046
+ reason: "xargs subcommand could not be resolved safely",
20047
+ matchedRule: "xargs:subcommand"
20048
+ };
20049
+ }
20050
+ const isShellExec = (subcommand.command === "sh" || subcommand.command === "bash" || subcommand.command === "zsh") && subcommand.args.length >= 2 && subcommand.args[0] === "-c";
20051
+ let parsed;
20052
+ if (isShellExec) {
20053
+ const innerResult = parseCommand(subcommand.args[1]);
20054
+ if (innerResult.parseError) {
20055
+ parsed = { commands: [subcommand], hasSubshell: false, subshellCommands: [], parseError: false };
20056
+ } else {
20057
+ parsed = innerResult;
20058
+ }
20059
+ } else {
20060
+ parsed = { commands: [subcommand], hasSubshell: false, subshellCommands: [], parseError: false };
20061
+ }
20062
+ const result = evaluate(parsed, config, cwd, depth + 1);
20063
+ return {
20064
+ command,
20065
+ args: args2,
20066
+ decision: result.decision,
20067
+ reason: `xargs subcommand "${subcommand.command}": ${result.reason}`,
20068
+ matchedRule: "xargs:subcommand"
20069
+ };
20344
20070
  }
20345
- var USER_CONFIG_PATHS = [
20346
- (0, import_path3.join)((0, import_os2.homedir)(), ".claude", "warden.yaml"),
20347
- (0, import_path3.join)((0, import_os2.homedir)(), ".claude", "warden.json")
20348
- ];
20349
- var PROJECT_CONFIG_NAMES = [
20350
- ".claude/warden.yaml",
20351
- ".claude/warden.json"
20352
- ];
20353
- function loadConfig(cwd) {
20354
- const config = structuredClone(DEFAULT_CONFIG);
20355
- const defaultLayer = config.layers[0];
20356
- let userLayer = null;
20357
- let userRaw = null;
20358
- for (const configPath of USER_CONFIG_PATHS) {
20359
- const result = tryLoadFile(configPath);
20360
- if (result) {
20361
- userLayer = extractLayer(result);
20362
- userRaw = result;
20071
+ function parseFindExecCommands(args2) {
20072
+ const commands = [];
20073
+ let i = 0;
20074
+ while (i < args2.length) {
20075
+ if (args2[i] === "-exec" || args2[i] === "-execdir") {
20076
+ i++;
20077
+ const cmdArgs = [];
20078
+ while (i < args2.length && args2[i] !== ";" && args2[i] !== "+") {
20079
+ if (args2[i] !== "{}") {
20080
+ cmdArgs.push(args2[i]);
20081
+ }
20082
+ i++;
20083
+ }
20084
+ i++;
20085
+ if (cmdArgs.length > 0) {
20086
+ commands.push({
20087
+ command: cmdArgs[0],
20088
+ originalCommand: cmdArgs[0],
20089
+ args: cmdArgs.slice(1),
20090
+ envPrefixes: [],
20091
+ raw: cmdArgs.join(" ")
20092
+ });
20093
+ }
20094
+ } else {
20095
+ i++;
20096
+ }
20097
+ }
20098
+ return commands;
20099
+ }
20100
+ function evaluateFindCommand(cmd, config, cwd, depth = 0) {
20101
+ const { command, args: args2 } = cmd;
20102
+ if (args2.some((a) => a === "-delete")) {
20103
+ return { command, args: args2, decision: "ask", reason: "find -delete can remove files", matchedRule: "find:delete" };
20104
+ }
20105
+ if (args2.some((a) => a === "-ok" || a === "-okdir")) {
20106
+ return { command, args: args2, decision: "ask", reason: "find -ok/-okdir can execute commands interactively", matchedRule: "find:ok" };
20107
+ }
20108
+ const execCommands = parseFindExecCommands(args2);
20109
+ if (execCommands.length === 0) {
20110
+ return { command, args: args2, decision: "allow", reason: "find without dangerous flags", matchedRule: "find:safe" };
20111
+ }
20112
+ for (const execCmd of execCommands) {
20113
+ const parsed = {
20114
+ commands: [execCmd],
20115
+ hasSubshell: false,
20116
+ subshellCommands: [],
20117
+ parseError: false
20118
+ };
20119
+ const result = evaluate(parsed, config, cwd, depth + 1);
20120
+ if (result.decision === "deny") {
20121
+ return { command, args: args2, decision: "deny", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
20122
+ }
20123
+ if (result.decision === "ask") {
20124
+ return { command, args: args2, decision: "ask", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
20125
+ }
20126
+ }
20127
+ return { command, args: args2, decision: "allow", reason: "find -exec commands are safe", matchedRule: "find:exec" };
20128
+ }
20129
+ var SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
20130
+ "-b",
20131
+ "-c",
20132
+ "-D",
20133
+ "-E",
20134
+ "-e",
20135
+ "-F",
20136
+ "-I",
20137
+ "-i",
20138
+ "-J",
20139
+ "-L",
20140
+ "-l",
20141
+ "-m",
20142
+ "-O",
20143
+ "-o",
20144
+ "-p",
20145
+ "-Q",
20146
+ "-R",
20147
+ "-S",
20148
+ "-W",
20149
+ "-w"
20150
+ ]);
20151
+ function findMatchingTarget(value, targets) {
20152
+ return targets.find((t) => globToRegex(t.name).test(value)) || null;
20153
+ }
20154
+ function parseSSHArgs(args2) {
20155
+ let host = null;
20156
+ const remoteArgs = [];
20157
+ let i = 0;
20158
+ while (i < args2.length) {
20159
+ const arg = args2[i];
20160
+ if (SSH_FLAGS_WITH_VALUE.has(arg)) {
20161
+ i += 2;
20162
+ continue;
20163
+ }
20164
+ if (arg.startsWith("-")) {
20165
+ i++;
20166
+ continue;
20167
+ }
20168
+ if (!host) {
20169
+ host = arg.includes("@") ? arg.split("@").pop() : arg;
20170
+ i++;
20171
+ while (i < args2.length) {
20172
+ remoteArgs.push(args2[i]);
20173
+ i++;
20174
+ }
20363
20175
  break;
20364
20176
  }
20365
- }
20366
- let workspaceLayer = null;
20367
- let workspaceRaw = null;
20368
- if (cwd) {
20369
- for (const name of PROJECT_CONFIG_NAMES) {
20370
- const result = tryLoadFile((0, import_path3.join)(cwd, name));
20371
- if (result) {
20372
- workspaceLayer = extractLayer(result);
20373
- workspaceRaw = result;
20374
- break;
20177
+ i++;
20178
+ }
20179
+ return {
20180
+ host,
20181
+ remoteCommand: remoteArgs.length > 0 ? remoteArgs.map(shellQuote).join(" ") : null
20182
+ };
20183
+ }
20184
+ function extractHostFromRemotePath(args2) {
20185
+ for (const arg of args2) {
20186
+ const match = arg.match(/^(?:[^@]+@)?([^:]+):/);
20187
+ if (match) return match[1];
20188
+ }
20189
+ return null;
20190
+ }
20191
+ function evaluateSSHCommand(cmd, config, targets, depth = 0) {
20192
+ const { command, args: args2 } = cmd;
20193
+ if (command === "scp" || command === "rsync") {
20194
+ const host2 = extractHostFromRemotePath(args2);
20195
+ if (!host2) return null;
20196
+ const target2 = findMatchingTarget(host2, targets);
20197
+ if (!target2) return null;
20198
+ if (target2.allowAll || !target2.overrides) {
20199
+ return {
20200
+ command,
20201
+ args: args2,
20202
+ decision: "allow",
20203
+ reason: `Trusted SSH host "${host2}"${target2.allowAll ? " (allowAll)" : ""}`,
20204
+ matchedRule: "trustedSSHHosts"
20205
+ };
20206
+ }
20207
+ if (target2.overrides.alwaysDeny.some((name) => name === command)) {
20208
+ return {
20209
+ command,
20210
+ args: args2,
20211
+ decision: "deny",
20212
+ reason: `Trusted SSH host "${host2}": "${command}" blocked by overrides`,
20213
+ matchedRule: "trustedSSHHosts"
20214
+ };
20215
+ }
20216
+ return {
20217
+ command,
20218
+ args: args2,
20219
+ decision: "allow",
20220
+ reason: `Trusted SSH host "${host2}"`,
20221
+ matchedRule: "trustedSSHHosts"
20222
+ };
20223
+ }
20224
+ const { host, remoteCommand } = parseSSHArgs(args2);
20225
+ if (!host) return null;
20226
+ const target = findMatchingTarget(host, targets);
20227
+ if (!target) return null;
20228
+ if (!remoteCommand) {
20229
+ return {
20230
+ command,
20231
+ args: args2,
20232
+ decision: "allow",
20233
+ reason: `Trusted SSH host "${host}" (interactive)`,
20234
+ matchedRule: "trustedSSHHosts"
20235
+ };
20236
+ }
20237
+ if (target.allowAll) {
20238
+ return {
20239
+ command,
20240
+ args: args2,
20241
+ decision: "allow",
20242
+ reason: `Trusted SSH host "${host}" (allowAll)`,
20243
+ matchedRule: "trustedSSHHosts"
20244
+ };
20245
+ }
20246
+ const parsed = parseCommand(remoteCommand);
20247
+ const result = evaluate(parsed, configWithContextOverrides(config, target), depth + 1);
20248
+ return {
20249
+ command,
20250
+ args: args2,
20251
+ decision: result.decision,
20252
+ reason: `Trusted SSH host "${host}": ${result.reason}`,
20253
+ matchedRule: "trustedSSHHosts"
20254
+ };
20255
+ }
20256
+ var DOCKER_EXEC_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
20257
+ "-e",
20258
+ "--env",
20259
+ "--env-file",
20260
+ "-u",
20261
+ "--user",
20262
+ "-w",
20263
+ "--workdir",
20264
+ "--detach-keys"
20265
+ ]);
20266
+ var INTERACTIVE_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh"]);
20267
+ function shellQuote(arg) {
20268
+ if (/[\s"'\\$`!#&|;()<>]/.test(arg)) {
20269
+ return `'${arg.replace(/'/g, "'\\''")}'`;
20270
+ }
20271
+ return arg;
20272
+ }
20273
+ function configWithContextOverrides(config, target) {
20274
+ const overrideLayers = [];
20275
+ if (target?.overrides) overrideLayers.push(target.overrides);
20276
+ if (config.trustedContextOverrides) overrideLayers.push(config.trustedContextOverrides);
20277
+ if (overrideLayers.length === 0) return config;
20278
+ return {
20279
+ ...config,
20280
+ layers: [...overrideLayers, ...config.layers]
20281
+ };
20282
+ }
20283
+ function evaluateRemoteCommand(remoteArgs, config, target, depth = 0) {
20284
+ if (target?.allowAll) {
20285
+ return { decision: "allow", reason: "allowAll target", details: [] };
20286
+ }
20287
+ const overriddenConfig = configWithContextOverrides(config, target);
20288
+ if (remoteArgs.length === 0) {
20289
+ return { decision: "allow", reason: "interactive", details: [] };
20290
+ }
20291
+ const remoteCmd = remoteArgs[0];
20292
+ if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs.length === 1) {
20293
+ return { decision: "allow", reason: "interactive shell", details: [] };
20294
+ }
20295
+ if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs[1] === "-c" && remoteArgs.length >= 3) {
20296
+ const innerCommand = remoteArgs.slice(2).join(" ");
20297
+ const parsed2 = parseCommand(innerCommand);
20298
+ return evaluate(parsed2, overriddenConfig, depth + 1);
20299
+ }
20300
+ const parsed = {
20301
+ commands: [{ command: remoteCmd, originalCommand: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
20302
+ hasSubshell: false,
20303
+ subshellCommands: [],
20304
+ parseError: false
20305
+ };
20306
+ return evaluate(parsed, overriddenConfig, depth + 1);
20307
+ }
20308
+ function parseDockerExecArgs(args2) {
20309
+ let target = null;
20310
+ const remoteArgs = [];
20311
+ let i = 0;
20312
+ while (i < args2.length) {
20313
+ const arg = args2[i];
20314
+ if (DOCKER_EXEC_FLAGS_WITH_VALUE.has(arg)) {
20315
+ i += 2;
20316
+ continue;
20317
+ }
20318
+ if (arg.startsWith("-")) {
20319
+ i++;
20320
+ continue;
20321
+ }
20322
+ if (!target) {
20323
+ target = arg;
20324
+ i++;
20325
+ while (i < args2.length) {
20326
+ remoteArgs.push(args2[i]);
20327
+ i++;
20375
20328
  }
20329
+ break;
20376
20330
  }
20331
+ i++;
20377
20332
  }
20378
- config.layers = [
20379
- ...workspaceLayer ? [workspaceLayer] : [],
20380
- ...userLayer ? [userLayer] : [],
20381
- defaultLayer
20382
- ];
20383
- if (userRaw) mergeNonLayerFields(config, userRaw);
20384
- if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
20385
- return config;
20333
+ return { target, remoteArgs };
20386
20334
  }
20387
- function tryLoadFile(filePath) {
20388
- if (!(0, import_fs.existsSync)(filePath)) return null;
20389
- try {
20390
- const raw = (0, import_fs.readFileSync)(filePath, "utf-8");
20391
- const parsed = filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? (0, import_yaml.parse)(raw) : JSON.parse(raw);
20392
- if (parsed && typeof parsed === "object") {
20393
- return parsed;
20335
+ function evaluateDockerExec(cmd, config, targets, depth = 0) {
20336
+ const { command, args: args2 } = cmd;
20337
+ if (args2[0] !== "exec") return null;
20338
+ const { target: containerName, remoteArgs } = parseDockerExecArgs(args2.slice(1));
20339
+ if (!containerName) return null;
20340
+ const matched = findMatchingTarget(containerName, targets);
20341
+ if (!matched) return null;
20342
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
20343
+ return {
20344
+ command,
20345
+ args: args2,
20346
+ decision: result.decision,
20347
+ reason: `Trusted Docker container "${containerName}" (${result.reason})`,
20348
+ matchedRule: "trustedDockerContainers"
20349
+ };
20350
+ }
20351
+ var KUBECTL_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
20352
+ "-n",
20353
+ "--namespace",
20354
+ "-c",
20355
+ "--container",
20356
+ "--context",
20357
+ "--cluster",
20358
+ "--kubeconfig",
20359
+ "-s",
20360
+ "--server",
20361
+ "--token",
20362
+ "--user",
20363
+ "--as",
20364
+ "--as-group",
20365
+ "--certificate-authority",
20366
+ "--client-certificate",
20367
+ "--client-key",
20368
+ "-l",
20369
+ "--selector",
20370
+ "-f",
20371
+ "--filename",
20372
+ "--cache-dir",
20373
+ "--request-timeout",
20374
+ "-o",
20375
+ "--output"
20376
+ ]);
20377
+ function parseKubectlExecArgs(args2) {
20378
+ let context = null;
20379
+ let pod = null;
20380
+ const remoteArgs = [];
20381
+ let i = 0;
20382
+ while (i < args2.length) {
20383
+ const arg = args2[i];
20384
+ if (arg === "--") {
20385
+ i++;
20386
+ while (i < args2.length) {
20387
+ remoteArgs.push(args2[i]);
20388
+ i++;
20389
+ }
20390
+ break;
20394
20391
  }
20395
- } catch (err) {
20396
- process.stderr.write(`[warden] Warning: failed to parse config ${filePath}: ${err instanceof Error ? err.message : String(err)}
20397
- `);
20392
+ if (arg.startsWith("--") && arg.includes("=")) {
20393
+ if (arg.startsWith("--context=")) {
20394
+ context = arg.split("=")[1];
20395
+ }
20396
+ i++;
20397
+ continue;
20398
+ }
20399
+ if (KUBECTL_FLAGS_WITH_VALUE.has(arg)) {
20400
+ if (arg === "--context") context = args2[i + 1] || null;
20401
+ i += 2;
20402
+ continue;
20403
+ }
20404
+ if (arg.startsWith("-")) {
20405
+ i++;
20406
+ continue;
20407
+ }
20408
+ if (!pod) {
20409
+ pod = arg;
20410
+ }
20411
+ i++;
20398
20412
  }
20399
- return null;
20413
+ return { context, pod, remoteArgs };
20414
+ }
20415
+ function evaluateKubectlExec(cmd, config, targets, depth = 0) {
20416
+ const { command, args: args2 } = cmd;
20417
+ if (args2[0] !== "exec") return null;
20418
+ const { context, pod, remoteArgs } = parseKubectlExecArgs(args2.slice(1));
20419
+ if (!context) return null;
20420
+ const matched = findMatchingTarget(context, targets);
20421
+ if (!matched) return null;
20422
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
20423
+ return {
20424
+ command,
20425
+ args: args2,
20426
+ decision: result.decision,
20427
+ reason: `Trusted kubectl context "${context}"${pod ? `, pod "${pod}"` : ""} (${result.reason})`,
20428
+ matchedRule: "trustedKubectlContexts"
20429
+ };
20400
20430
  }
20401
- var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
20402
- ...DEFAULT_CONFIG.layers[0].alwaysAllow,
20403
- ...DEFAULT_CONFIG.layers[0].alwaysDeny,
20404
- ...DEFAULT_CONFIG.layers[0].rules.map((r) => r.command)
20431
+ var SPRITE_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
20432
+ "-o",
20433
+ "--org",
20434
+ "-s",
20435
+ "--sprite"
20405
20436
  ]);
20406
- function warnArgPatternCommandMismatch(rule) {
20407
- if (!Array.isArray(rule.argPatterns)) return;
20408
- for (const pattern of rule.argPatterns) {
20409
- const matchers = [
20410
- ...pattern.match?.anyArgMatches || [],
20411
- ...pattern.match?.argsMatch || []
20412
- ];
20413
- for (const m of matchers) {
20414
- const literals = extractLiteralsFromPattern(m);
20415
- for (const lit of literals) {
20416
- if (lit !== rule.command && KNOWN_COMMANDS.has(lit)) {
20417
- process.stderr.write(
20418
- `[warden] Warning: rule for "${rule.command}" has argPattern matching "${lit}" \u2014 this won't work as expected. Rules are matched by the command being run, not its arguments. If you want to control "${lit}", add a separate rule with command: "${lit}".
20419
- `
20420
- );
20421
- }
20437
+ function parseSpriteExecArgs(args2) {
20438
+ let spriteName = null;
20439
+ const remoteArgs = [];
20440
+ let foundExec = false;
20441
+ let i = 0;
20442
+ while (i < args2.length) {
20443
+ const arg = args2[i];
20444
+ if (arg.startsWith("--") && arg.includes("=")) {
20445
+ if (arg.startsWith("--sprite=")) {
20446
+ spriteName = arg.split("=")[1];
20447
+ }
20448
+ i++;
20449
+ continue;
20450
+ }
20451
+ if (SPRITE_FLAGS_WITH_VALUE.has(arg)) {
20452
+ if (arg === "-s" || arg === "--sprite") {
20453
+ spriteName = args2[i + 1] || null;
20454
+ }
20455
+ i += 2;
20456
+ continue;
20457
+ }
20458
+ if (arg === "--debug") {
20459
+ i++;
20460
+ continue;
20461
+ }
20462
+ if (arg.startsWith("-")) {
20463
+ i++;
20464
+ continue;
20465
+ }
20466
+ if (!foundExec) {
20467
+ if (arg === "exec" || arg === "x" || arg === "console" || arg === "c") {
20468
+ foundExec = true;
20469
+ i++;
20470
+ continue;
20422
20471
  }
20472
+ return { spriteName: null, remoteArgs: [] };
20423
20473
  }
20474
+ while (i < args2.length) {
20475
+ remoteArgs.push(args2[i]);
20476
+ i++;
20477
+ }
20478
+ break;
20424
20479
  }
20480
+ return { spriteName, remoteArgs };
20425
20481
  }
20426
- function extractLiteralsFromPattern(pattern) {
20427
- let cleaned = pattern.replace(/^\^?\(?(.*?)\)?\$?$/, "$1");
20428
- return cleaned.split("|").map((s) => s.trim()).filter((s) => /^[a-z][a-z0-9_-]*$/i.test(s));
20482
+ function evaluateSpriteExec(cmd, config, targets, depth = 0) {
20483
+ const { command, args: args2 } = cmd;
20484
+ const { spriteName, remoteArgs } = parseSpriteExecArgs(args2);
20485
+ if (!spriteName) return null;
20486
+ const matched = findMatchingTarget(spriteName, targets);
20487
+ if (!matched) return null;
20488
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
20489
+ return {
20490
+ command,
20491
+ args: args2,
20492
+ decision: result.decision,
20493
+ reason: `Trusted sprite "${spriteName}" (${result.reason})`,
20494
+ matchedRule: "trustedSprites"
20495
+ };
20429
20496
  }
20430
- function extractLayer(raw) {
20431
- const rules = Array.isArray(raw.rules) ? raw.rules : [];
20432
- for (const rule of rules) {
20433
- if (rule && typeof rule === "object") {
20434
- if (rule.default && !isValidDecision(rule.default)) {
20435
- process.stderr.write(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
20436
- `);
20437
- rule.default = "ask";
20497
+ var FLY_SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
20498
+ "-a",
20499
+ "--app",
20500
+ "-C",
20501
+ "--command",
20502
+ "-o",
20503
+ "--org",
20504
+ "-r",
20505
+ "--region",
20506
+ "-u",
20507
+ "--user",
20508
+ "--address"
20509
+ ]);
20510
+ function parseFlySSHArgs(args2) {
20511
+ let app = null;
20512
+ const remoteArgs = [];
20513
+ let isSSH = false;
20514
+ let foundConsole = false;
20515
+ let i = 0;
20516
+ while (i < args2.length) {
20517
+ const arg = args2[i];
20518
+ if (arg.startsWith("--app=")) {
20519
+ app = arg.slice(6);
20520
+ i++;
20521
+ continue;
20522
+ }
20523
+ if (FLY_SSH_FLAGS_WITH_VALUE.has(arg)) {
20524
+ if (arg === "-a" || arg === "--app") {
20525
+ app = args2[i + 1] || null;
20438
20526
  }
20439
- if (Array.isArray(rule.argPatterns)) {
20440
- for (const pattern of rule.argPatterns) {
20441
- if (pattern?.decision && !isValidDecision(pattern.decision)) {
20442
- process.stderr.write(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
20443
- `);
20444
- pattern.decision = "ask";
20527
+ if ((arg === "-C" || arg === "--command") && foundConsole) {
20528
+ const cmdValue = args2[i + 1];
20529
+ if (cmdValue) {
20530
+ const parsed = parseCommand(cmdValue);
20531
+ if (!parsed.parseError && parsed.commands.length > 0) {
20532
+ const cmd = parsed.commands[0];
20533
+ remoteArgs.push(cmd.command, ...cmd.args);
20445
20534
  }
20446
20535
  }
20536
+ i += 2;
20537
+ continue;
20447
20538
  }
20448
- warnArgPatternCommandMismatch(rule);
20539
+ i += 2;
20540
+ continue;
20449
20541
  }
20450
- }
20451
- return {
20452
- alwaysAllow: Array.isArray(raw.alwaysAllow) ? raw.alwaysAllow : [],
20453
- alwaysDeny: Array.isArray(raw.alwaysDeny) ? raw.alwaysDeny : [],
20454
- rules
20455
- };
20456
- }
20457
- function parseTrustedList(raw) {
20458
- return raw.map((entry) => {
20459
- if (typeof entry === "string") return { name: entry };
20460
- if (entry && typeof entry === "object" && "name" in entry) {
20461
- const obj = entry;
20462
- const target = { name: String(obj.name) };
20463
- if (obj.allowAll === true) target.allowAll = true;
20464
- if (obj.overrides && typeof obj.overrides === "object") {
20465
- target.overrides = extractLayer(obj.overrides);
20542
+ if (arg === "--") {
20543
+ i++;
20544
+ while (i < args2.length) {
20545
+ remoteArgs.push(args2[i]);
20546
+ i++;
20466
20547
  }
20467
- return target;
20548
+ break;
20468
20549
  }
20469
- return null;
20470
- }).filter((t) => t !== null);
20471
- }
20472
- var VALID_REMOTE_CONTEXTS = /* @__PURE__ */ new Set(["ssh", "docker", "kubectl", "sprite", "fly"]);
20473
- function parseTrustedRemotes(raw) {
20474
- const results = [];
20475
- for (const entry of raw) {
20476
- if (!entry || typeof entry !== "object") continue;
20477
- const obj = entry;
20478
- const context = String(obj.context || "");
20479
- if (!VALID_REMOTE_CONTEXTS.has(context)) {
20480
- process.stderr.write(`[warden] Warning: unknown remote context "${context}", skipping
20481
- `);
20550
+ if (arg.startsWith("-")) {
20551
+ i++;
20482
20552
  continue;
20483
20553
  }
20484
- const name = String(obj.name || "");
20485
- if (!name) continue;
20486
- const remote = { name, context };
20487
- if (obj.allowAll === true) remote.allowAll = true;
20488
- if (obj.overrides && typeof obj.overrides === "object") {
20489
- remote.overrides = extractLayer(obj.overrides);
20554
+ if (!isSSH && arg === "ssh") {
20555
+ isSSH = true;
20556
+ i++;
20557
+ continue;
20490
20558
  }
20491
- results.push(remote);
20492
- }
20493
- return results;
20494
- }
20495
- function parseTargetPolicies(raw) {
20496
- const results = [];
20497
- for (const entry of raw) {
20498
- if (!entry || typeof entry !== "object") continue;
20499
- const obj = entry;
20500
- const type = String(obj.type || "");
20501
- const rawDecision = String(obj.decision || "allow");
20502
- const decision = isValidDecision(rawDecision) ? rawDecision : "allow";
20503
- const base = {
20504
- commands: Array.isArray(obj.commands) ? obj.commands.map(String) : void 0,
20505
- decision,
20506
- reason: obj.reason ? String(obj.reason) : void 0,
20507
- allowAll: obj.allowAll === true ? true : void 0
20508
- };
20509
- switch (type) {
20510
- case "path": {
20511
- const path = String(obj.path || "");
20512
- if (!path) continue;
20513
- const policy = { ...base, type: "path", path };
20514
- if (obj.recursive === false) policy.recursive = false;
20515
- results.push(policy);
20516
- break;
20517
- }
20518
- case "database": {
20519
- const host = String(obj.host || "");
20520
- if (!host) continue;
20521
- const policy = { ...base, type: "database", host };
20522
- if (typeof obj.port === "number") policy.port = obj.port;
20523
- if (obj.database) policy.database = String(obj.database);
20524
- results.push(policy);
20525
- break;
20526
- }
20527
- case "endpoint": {
20528
- const pattern = String(obj.pattern || "");
20529
- if (!pattern) continue;
20530
- results.push({ ...base, type: "endpoint", pattern });
20531
- break;
20532
- }
20533
- default:
20534
- process.stderr.write(`[warden] Warning: unknown target policy type "${type}", skipping
20535
- `);
20559
+ if (isSSH && !foundConsole && (arg === "console" || arg === "sftp")) {
20560
+ foundConsole = true;
20561
+ i++;
20562
+ continue;
20536
20563
  }
20564
+ i++;
20537
20565
  }
20538
- return results;
20566
+ return { app, remoteArgs, isSSH: isSSH && foundConsole };
20539
20567
  }
20540
- function parseLegacyPaths(raw) {
20541
- const results = [];
20542
- for (const e of raw) {
20543
- if (!e || typeof e !== "object") continue;
20544
- const obj = e;
20545
- const decision = String(obj.decision || "allow");
20546
- if (!isValidDecision(decision)) continue;
20547
- const path = String(obj.path || "");
20548
- if (!path) continue;
20549
- const tp = { type: "path", path, decision };
20550
- if (obj.recursive === false) tp.recursive = false;
20551
- if (Array.isArray(obj.commands)) tp.commands = obj.commands.map(String);
20552
- if (obj.reason) tp.reason = String(obj.reason);
20553
- results.push(tp);
20554
- }
20555
- return results;
20568
+ function evaluateFlyCommand(cmd, config, targets, depth = 0) {
20569
+ const { command, args: args2 } = cmd;
20570
+ const { app, remoteArgs, isSSH } = parseFlySSHArgs(args2);
20571
+ if (!isSSH) return null;
20572
+ if (!app) return null;
20573
+ const matched = findMatchingTarget(app, targets);
20574
+ if (!matched) return null;
20575
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
20576
+ return {
20577
+ command,
20578
+ args: args2,
20579
+ decision: result.decision,
20580
+ reason: `Trusted Fly app "${app}" (${result.reason})`,
20581
+ matchedRule: "trustedFlyApps"
20582
+ };
20556
20583
  }
20557
- function parseLegacyDatabases(raw) {
20558
- const results = [];
20559
- for (const e of raw) {
20560
- if (!e || typeof e !== "object") continue;
20561
- const obj = e;
20562
- const decision = String(obj.decision || "allow");
20563
- if (!isValidDecision(decision)) continue;
20564
- const host = String(obj.host || "");
20565
- if (!host) continue;
20566
- const td = { type: "database", host, decision };
20567
- if (typeof obj.port === "number") td.port = obj.port;
20568
- if (obj.database) td.database = String(obj.database);
20569
- if (Array.isArray(obj.commands)) td.commands = obj.commands.map(String);
20570
- if (obj.reason) td.reason = String(obj.reason);
20571
- results.push(td);
20572
- }
20573
- return results;
20584
+
20585
+ // src/core.ts
20586
+ function wardenEvalWithConfig(command, config, cwd) {
20587
+ const parsed = parseCommand(command);
20588
+ return evaluate(parsed, config, cwd);
20574
20589
  }
20575
- function parseLegacyEndpoints(raw) {
20576
- const results = [];
20577
- for (const e of raw) {
20578
- if (!e || typeof e !== "object") continue;
20579
- const obj = e;
20580
- const decision = String(obj.decision || "allow");
20581
- if (!isValidDecision(decision)) continue;
20582
- const pattern = String(obj.pattern || "");
20583
- if (!pattern) continue;
20584
- const te = { type: "endpoint", pattern, decision };
20585
- if (Array.isArray(obj.commands)) te.commands = obj.commands.map(String);
20586
- if (obj.reason) te.reason = String(obj.reason);
20587
- results.push(te);
20588
- }
20589
- return results;
20590
+
20591
+ // src/codex.ts
20592
+ function toCodexDecision(decision) {
20593
+ if (decision === "allow") return "allow";
20594
+ if (decision === "ask") return "prompt";
20595
+ return "forbidden";
20590
20596
  }
20591
- var LEGACY_REMOTE_MAP = {
20592
- trustedSSHHosts: "ssh",
20593
- trustedDockerContainers: "docker",
20594
- trustedKubectlContexts: "kubectl",
20595
- trustedSprites: "sprite",
20596
- trustedFlyApps: "fly"
20597
- };
20598
- function mergeNonLayerFields(config, raw) {
20599
- if (Array.isArray(raw.trustedRemotes)) {
20600
- config.trustedRemotes = [...config.trustedRemotes || [], ...parseTrustedRemotes(raw.trustedRemotes)];
20601
- }
20602
- if (Array.isArray(raw.targetPolicies)) {
20603
- config.targetPolicies = [...config.targetPolicies || [], ...parseTargetPolicies(raw.targetPolicies)];
20604
- }
20605
- for (const [key, context] of Object.entries(LEGACY_REMOTE_MAP)) {
20606
- if (Array.isArray(raw[key])) {
20607
- const remotes = parseTrustedList(raw[key]).map((t) => ({ ...t, context }));
20608
- config.trustedRemotes = [...config.trustedRemotes || [], ...remotes];
20609
- }
20610
- }
20611
- if (Array.isArray(raw.trustedPaths)) {
20612
- config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyPaths(raw.trustedPaths)];
20613
- }
20614
- if (Array.isArray(raw.trustedDatabases)) {
20615
- config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyDatabases(raw.trustedDatabases)];
20616
- }
20617
- if (Array.isArray(raw.trustedEndpoints)) {
20618
- config.targetPolicies = [...config.targetPolicies || [], ...parseLegacyEndpoints(raw.trustedEndpoints)];
20619
- }
20620
- if (typeof raw.defaultDecision === "string") {
20621
- if (isValidDecision(raw.defaultDecision)) {
20622
- config.defaultDecision = raw.defaultDecision;
20623
- } else {
20624
- process.stderr.write(`[warden] Warning: invalid defaultDecision "${raw.defaultDecision}", ignoring
20625
- `);
20626
- }
20627
- }
20628
- if (typeof raw.askOnSubshell === "boolean") {
20629
- config.askOnSubshell = raw.askOnSubshell;
20630
- }
20631
- if (typeof raw.notifyOnAsk === "boolean") {
20632
- config.notifyOnAsk = raw.notifyOnAsk;
20597
+ function quote(value) {
20598
+ return JSON.stringify(value);
20599
+ }
20600
+ function collectCandidateCommands(config) {
20601
+ const names = /* @__PURE__ */ new Set();
20602
+ for (const layer of config.layers) {
20603
+ for (const name of layer.alwaysAllow) names.add(name);
20604
+ for (const name of layer.alwaysDeny) names.add(name);
20605
+ for (const rule of layer.rules) names.add(rule.command);
20633
20606
  }
20634
- if (typeof raw.notifyOnDeny === "boolean") {
20635
- config.notifyOnDeny = raw.notifyOnDeny;
20607
+ return [...names].map((n) => n.trim()).filter((n) => n.length > 0 && !/\s/.test(n)).sort();
20608
+ }
20609
+ function buildCodexRuleRecords(config) {
20610
+ const records = [];
20611
+ for (const command of collectCandidateCommands(config)) {
20612
+ const result = wardenEvalWithConfig(command, config);
20613
+ records.push({
20614
+ command,
20615
+ decision: result.decision,
20616
+ reason: result.reason
20617
+ });
20636
20618
  }
20637
- if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
20638
- const overrides = raw.trustedContextOverrides;
20639
- const layer = extractLayer(overrides);
20640
- if (config.trustedContextOverrides) {
20641
- config.trustedContextOverrides = {
20642
- alwaysAllow: [...layer.alwaysAllow, ...config.trustedContextOverrides.alwaysAllow],
20643
- alwaysDeny: [...layer.alwaysDeny, ...config.trustedContextOverrides.alwaysDeny],
20644
- rules: [...layer.rules, ...config.trustedContextOverrides.rules]
20645
- };
20646
- } else {
20647
- config.trustedContextOverrides = layer;
20648
- }
20619
+ return records;
20620
+ }
20621
+ function generateCodexRules(config) {
20622
+ const lines = [
20623
+ "# Generated by claude-warden.",
20624
+ "# Regenerate with: pnpm codex:export-rules",
20625
+ ""
20626
+ ];
20627
+ for (const record of buildCodexRuleRecords(config)) {
20628
+ lines.push(
20629
+ `prefix_rule(pattern = [${quote(record.command)}], decision = ${quote(toCodexDecision(record.decision))}, justification = ${quote(`Warden: ${record.reason}`)})`
20630
+ );
20649
20631
  }
20632
+ lines.push("");
20633
+ return lines.join("\n");
20650
20634
  }
20651
20635
 
20652
20636
  // src/codex-export.ts
20637
+ setQuiet(false);
20653
20638
  function parseArgs(argv) {
20654
20639
  let cwd = process.cwd();
20655
20640
  let outPath = null;