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