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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/agent-exec-guard.js +2 -0
- package/dist/approval/cliApproval.js +44 -0
- package/dist/approval/cliApproval.js.map +1 -0
- package/dist/approval/db.js +49 -0
- package/dist/approval/db.js.map +1 -0
- package/dist/approval/store.js +132 -0
- package/dist/approval/store.js.map +1 -0
- package/dist/approval/token.js +57 -0
- package/dist/approval/token.js.map +1 -0
- package/dist/classify/checksum.js +26 -0
- package/dist/classify/checksum.js.map +1 -0
- package/dist/classify/classify.js +89 -0
- package/dist/classify/classify.js.map +1 -0
- package/dist/classify/rules/blocked.js +162 -0
- package/dist/classify/rules/blocked.js.map +1 -0
- package/dist/classify/rules/safe.js +117 -0
- package/dist/classify/rules/safe.js.map +1 -0
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -0
- package/dist/exec/run.js +69 -0
- package/dist/exec/run.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/normalize.js +0 -0
- package/dist/parser/normalize.js.map +1 -0
- package/dist/parser/shellParse.js +184 -0
- package/dist/parser/shellParse.js.map +1 -0
- package/dist/parser/types.js +6 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/pipeline.js +22 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/security/audit.js +50 -0
- package/dist/security/audit.js.map +1 -0
- package/dist/security/redact.js +22 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/security/secret.js +13 -0
- package/dist/security/secret.js.map +1 -0
- package/dist/server.js +35 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/auditLog.js +14 -0
- package/dist/tools/auditLog.js.map +1 -0
- package/dist/tools/check.js +26 -0
- package/dist/tools/check.js.map +1 -0
- package/dist/tools/checkApprovalStatus.js +22 -0
- package/dist/tools/checkApprovalStatus.js.map +1 -0
- package/dist/tools/index.js +38 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/requestApproval.js +35 -0
- package/dist/tools/requestApproval.js.map +1 -0
- package/dist/tools/run.js +93 -0
- package/dist/tools/run.js.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/parser/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeCommand } from "./parser/normalize.js";
|
|
2
|
+
import { parseShellCommand } from "./parser/shellParse.js";
|
|
3
|
+
import { classify } from "./classify/classify.js";
|
|
4
|
+
/**
|
|
5
|
+
* The single shared normalize -> parse -> classify pipeline. Both
|
|
6
|
+
* exec_guard_check and exec_guard_run call exactly this function --
|
|
7
|
+
* exec_guard_run NEVER trusts a classification/checksum handed to it by a
|
|
8
|
+
* caller, it always re-derives its own by calling this again (see
|
|
9
|
+
* docs/security-model.md, "Re-verification, not trust-on-first-check").
|
|
10
|
+
*/
|
|
11
|
+
export function runPipeline(rawCommand) {
|
|
12
|
+
const normalized = normalizeCommand(rawCommand);
|
|
13
|
+
if (!normalized.ok) {
|
|
14
|
+
const parseResult = { ok: false, reason: `normalization_rejected: ${normalized.reason}`, raw: rawCommand };
|
|
15
|
+
const result = classify(parseResult);
|
|
16
|
+
return { classification: result.classification, reason: result.reason, commandChecksum: result.commandChecksum, parsedCommand: parseResult };
|
|
17
|
+
}
|
|
18
|
+
const parsed = parseShellCommand(normalized.normalized);
|
|
19
|
+
const result = classify(parsed);
|
|
20
|
+
return { classification: result.classification, reason: result.reason, commandChecksum: result.commandChecksum, parsedCommand: parsed };
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAuB,MAAM,wBAAwB,CAAC;AAUvE;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,MAAM,UAAU,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAChD,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;QACnB,MAAM,WAAW,GAAgB,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,2BAA2B,UAAU,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;QACxH,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;QACrC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;IAC/I,CAAC;IAED,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;AAC1I,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
const GENESIS_HASH = "0".repeat(64);
|
|
6
|
+
const auditPath = path.join(config.stateDir, "audit.log");
|
|
7
|
+
function computeEntryHash(prevHash, partial) {
|
|
8
|
+
return createHash("sha256").update(JSON.stringify({ ...partial, prevHash })).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function readRawLines() {
|
|
11
|
+
if (!fs.existsSync(auditPath))
|
|
12
|
+
return [];
|
|
13
|
+
return fs
|
|
14
|
+
.readFileSync(auditPath, "utf8")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((l) => JSON.parse(l));
|
|
18
|
+
}
|
|
19
|
+
export function appendAudit(event) {
|
|
20
|
+
fs.mkdirSync(config.stateDir, { recursive: true });
|
|
21
|
+
const existing = readRawLines();
|
|
22
|
+
const prev = existing[existing.length - 1];
|
|
23
|
+
const prevHash = prev ? prev.entryHash : GENESIS_HASH;
|
|
24
|
+
const seq = prev ? prev.seq + 1 : 1;
|
|
25
|
+
const base = { seq, ts: new Date().toISOString(), prevHash, ...event };
|
|
26
|
+
const entryHash = computeEntryHash(prevHash, base);
|
|
27
|
+
const full = { ...base, entryHash };
|
|
28
|
+
fs.appendFileSync(auditPath, JSON.stringify(full) + "\n", "utf8");
|
|
29
|
+
return full;
|
|
30
|
+
}
|
|
31
|
+
export function readAudit(limit = 50) {
|
|
32
|
+
const all = readRawLines();
|
|
33
|
+
return all.slice(Math.max(0, all.length - limit));
|
|
34
|
+
}
|
|
35
|
+
/** Recomputes the hash chain over the whole log; used by tests and
|
|
36
|
+
* available for operators who want to verify the log hasn't been altered. */
|
|
37
|
+
export function verifyAuditChain() {
|
|
38
|
+
const all = readRawLines();
|
|
39
|
+
let prevHash = GENESIS_HASH;
|
|
40
|
+
for (const entry of all) {
|
|
41
|
+
const { entryHash, ...rest } = entry;
|
|
42
|
+
const expected = computeEntryHash(prevHash, rest);
|
|
43
|
+
if (expected !== entryHash || entry.prevHash !== prevHash) {
|
|
44
|
+
return { ok: false, brokenAtSeq: entry.seq };
|
|
45
|
+
}
|
|
46
|
+
prevHash = entryHash;
|
|
47
|
+
}
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/security/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAyBtC,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACpC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAE1D,SAAS,gBAAgB,CAAC,QAAgB,EAAE,OAAsC;IAChF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7F,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,OAAO,EAAE;SACN,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC;SAC/B,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAe,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAsB;IAChD,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;IACtD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,MAAM,IAAI,GAAkC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAC;IACtG,MAAM,SAAS,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnD,MAAM,IAAI,GAAe,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC;IAChD,EAAE,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAClE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAK,GAAG,EAAE;IAClC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC;AACpD,CAAC;AAOD;6EAC6E;AAC7E,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,IAAI,QAAQ,GAAG,YAAY,CAAC;IAC5B,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;QACrC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAClD,IAAI,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC1D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;QAC/C,CAAC;QACD,QAAQ,GAAG,SAAS,CAAC;IACvB,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Deep-redacts any object by key name before it's returned to the LLM,
|
|
2
|
+
* as defense-in-depth against a tool accidentally including something
|
|
3
|
+
* credential-shaped in its response. */
|
|
4
|
+
const SENSITIVE_KEY_RE = /^(.*_?(secret|password|api_key|apikey|access_key|private_key|credential)s?.*)$/i;
|
|
5
|
+
// "approvalToken" is the one legitimate token this server is *supposed* to
|
|
6
|
+
// hand back to the caller (it's the human-approval proof itself, minted by
|
|
7
|
+
// exec_guard_check_approval_status) -- excluded from the generic
|
|
8
|
+
// token-shaped-key redaction below so it isn't scrubbed away by accident.
|
|
9
|
+
// The server secret used to *sign* tokens never flows through a tool
|
|
10
|
+
// response at all (see security/secret.ts), so there is no ambiguity here.
|
|
11
|
+
const ALLOWED_TOKEN_KEYS = new Set(["approvaltoken"]);
|
|
12
|
+
const TOKEN_KEY_RE = /token/i;
|
|
13
|
+
export function redactByKeyName(value) {
|
|
14
|
+
return JSON.parse(JSON.stringify(value, (key, val) => {
|
|
15
|
+
if (TOKEN_KEY_RE.test(key) && !ALLOWED_TOKEN_KEYS.has(key.toLowerCase()))
|
|
16
|
+
return "[REDACTED]";
|
|
17
|
+
if (SENSITIVE_KEY_RE.test(key))
|
|
18
|
+
return "[REDACTED]";
|
|
19
|
+
return val;
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/security/redact.ts"],"names":[],"mappings":"AAAA;;wCAEwC;AACxC,MAAM,gBAAgB,GAAG,iFAAiF,CAAC;AAE3G,2EAA2E;AAC3E,2EAA2E;AAC3E,iEAAiE;AACjE,0EAA0E;AAC1E,qEAAqE;AACrE,2EAA2E;AAC3E,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;AACtD,MAAM,YAAY,GAAG,QAAQ,CAAC;AAE9B,MAAM,UAAU,eAAe,CAAI,KAAQ;IACzC,OAAO,IAAI,CAAC,KAAK,CACf,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACjC,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;YAAE,OAAO,YAAY,CAAC;QAC9F,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,YAAY,CAAC;QACpD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { registerRedactedValue } from "../utils/logger.js";
|
|
3
|
+
// Single choke point for the server secret. Nothing outside
|
|
4
|
+
// approval/token.ts should call getServerSecret(). It is registered with
|
|
5
|
+
// the logger so any accidental log line containing it gets scrubbed, but
|
|
6
|
+
// the real invariant is enforced by never handing it to execFile env or a
|
|
7
|
+
// tool response -- see exec/run.ts (env is always explicitly allow-listed)
|
|
8
|
+
// and security/redact.ts.
|
|
9
|
+
registerRedactedValue(config.serverSecret);
|
|
10
|
+
export function getServerSecret() {
|
|
11
|
+
return config.serverSecret;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/security/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,4DAA4D;AAC5D,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,2EAA2E;AAC3E,0BAA0B;AAC1B,qBAAqB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;AAE3C,MAAM,UAAU,eAAe;IAC7B,OAAO,MAAM,CAAC,YAAY,CAAC;AAC7B,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { tools } from "./tools/index.js";
|
|
5
|
+
import { redactByKeyName } from "./security/redact.js";
|
|
6
|
+
import { logger } from "./utils/logger.js";
|
|
7
|
+
export async function buildServer() {
|
|
8
|
+
const server = new McpServer({ name: "agent-exec-guard", version: "0.1.0" });
|
|
9
|
+
for (const tool of tools) {
|
|
10
|
+
const shape = tool.schema instanceof z.ZodObject ? tool.schema.shape : { input: tool.schema };
|
|
11
|
+
server.tool(tool.name, tool.description, shape, async (args) => {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = tool.schema.parse(args);
|
|
14
|
+
const result = await tool.handler(parsed);
|
|
15
|
+
const redacted = redactByKeyName(result);
|
|
16
|
+
return { content: [{ type: "text", text: JSON.stringify(redacted, null, 2) }] };
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
logger.error(`tool ${tool.name} failed`, { error: err instanceof Error ? err.message : String(err) });
|
|
20
|
+
return {
|
|
21
|
+
isError: true,
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify({ error: "tool_failed", message: err instanceof Error ? err.message : String(err) }) }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return server;
|
|
28
|
+
}
|
|
29
|
+
export async function main() {
|
|
30
|
+
const server = await buildServer();
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
logger.info("agent-exec-guard MCP server started");
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAE7E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9F,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,IAAa,EAAE,EAAE;YACtE,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACvC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;gBAC1C,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;gBACzC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YAC3F,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACtG,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;iBAChJ,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,MAAM,GAAG,MAAM,WAAW,EAAE,CAAC;IACnC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readAudit, verifyAuditChain } from "../security/audit.js";
|
|
3
|
+
export const auditLogSchema = z.object({
|
|
4
|
+
limit: z.number().int().positive().max(1000).optional(),
|
|
5
|
+
});
|
|
6
|
+
/** Read-only: returns recent classification+execution decisions from the
|
|
7
|
+
* local hash-chained audit log, and whether the chain still verifies
|
|
8
|
+
* (false would mean the log file was tampered with or corrupted). */
|
|
9
|
+
export async function exec_guard_audit_log(input) {
|
|
10
|
+
const events = readAudit(input.limit ?? 50);
|
|
11
|
+
const chain = verifyAuditChain();
|
|
12
|
+
return { events, chainVerified: chain.ok, brokenAtSeq: chain.ok ? undefined : chain.brokenAtSeq };
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=auditLog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auditLog.js","sourceRoot":"","sources":["../../src/tools/auditLog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;CACxD,CAAC,CAAC;AAEH;;qEAEqE;AACrE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAAqC;IAC9E,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAC;IACjC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;AACpG,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runPipeline } from "../pipeline.js";
|
|
3
|
+
import { appendAudit } from "../security/audit.js";
|
|
4
|
+
export const checkSchema = z.object({
|
|
5
|
+
command: z.string().min(1),
|
|
6
|
+
cwd: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
/** Read-only: parses and classifies a candidate command without executing
|
|
9
|
+
* anything. This is what an agent (or its host harness) calls first to
|
|
10
|
+
* decide what to do next. */
|
|
11
|
+
export async function exec_guard_check(input) {
|
|
12
|
+
const result = runPipeline(input.command);
|
|
13
|
+
appendAudit({
|
|
14
|
+
type: "check",
|
|
15
|
+
command: input.command,
|
|
16
|
+
classification: result.classification,
|
|
17
|
+
commandChecksum: result.commandChecksum ?? undefined,
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
classification: result.classification,
|
|
21
|
+
reason: result.reason,
|
|
22
|
+
parsedCommand: result.parsedCommand,
|
|
23
|
+
commandChecksum: result.commandChecksum,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/tools/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC3B,CAAC,CAAC;AAEH;;6BAE6B;AAC7B,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAkC;IACvE,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC1C,WAAW,CAAC;QACV,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,SAAS;KACrD,CAAC,CAAC;IACH,OAAO;QACL,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,eAAe,EAAE,MAAM,CAAC,eAAe;KACxC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getApproval } from "../approval/store.js";
|
|
3
|
+
export const checkApprovalStatusSchema = z.object({
|
|
4
|
+
approvalId: z.string().uuid(),
|
|
5
|
+
});
|
|
6
|
+
/** Polled by the agent after exec_guard_request_approval. Returns the
|
|
7
|
+
* signed single-use approvalToken only once a human has approved -- the
|
|
8
|
+
* agent can never obtain a token any other way. */
|
|
9
|
+
export async function exec_guard_check_approval_status(input) {
|
|
10
|
+
const approval = await getApproval(input.approvalId);
|
|
11
|
+
if (!approval)
|
|
12
|
+
return { error: "not_found" };
|
|
13
|
+
const base = { approvalId: approval.id, status: approval.status, expiresAt: approval.expiresAt };
|
|
14
|
+
if (approval.status === "approved") {
|
|
15
|
+
return { ...base, approvalToken: approval.approvalToken };
|
|
16
|
+
}
|
|
17
|
+
if (approval.status === "denied") {
|
|
18
|
+
return { ...base, denyReason: approval.denyReason };
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=checkApprovalStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkApprovalStatus.js","sourceRoot":"","sources":["../../src/tools/checkApprovalStatus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;CAC9B,CAAC,CAAC;AAEH;;mDAEmD;AACnD,MAAM,CAAC,KAAK,UAAU,gCAAgC,CAAC,KAAgD;IACrG,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAE7C,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC;IACjG,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,EAAE,GAAG,IAAI,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC;IAC5D,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC;IACtD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { exec_guard_check, checkSchema } from "./check.js";
|
|
2
|
+
import { exec_guard_run, runSchema } from "./run.js";
|
|
3
|
+
import { exec_guard_request_approval, requestApprovalSchema } from "./requestApproval.js";
|
|
4
|
+
import { exec_guard_check_approval_status, checkApprovalStatusSchema } from "./checkApprovalStatus.js";
|
|
5
|
+
import { exec_guard_audit_log, auditLogSchema } from "./auditLog.js";
|
|
6
|
+
export const tools = [
|
|
7
|
+
{
|
|
8
|
+
name: "exec_guard_check",
|
|
9
|
+
description: "Fully parses (real shell grammar, not string-prefix matching) and classifies a candidate shell command as SAFE, BLOCKED, or UNCERTAIN. Read-only -- never executes anything. Call this first to decide what to do with a command.",
|
|
10
|
+
schema: checkSchema,
|
|
11
|
+
handler: exec_guard_check,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "exec_guard_run",
|
|
15
|
+
description: "The only tool that actually executes a command. Always re-parses and re-classifies internally (never trusts a classification passed in). Executes immediately if SAFE; refuses if BLOCKED; requires a verified approvalId+approvalToken (from exec_guard_request_approval / exec_guard_check_approval_status) if UNCERTAIN.",
|
|
16
|
+
schema: runSchema,
|
|
17
|
+
handler: exec_guard_run,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "exec_guard_request_approval",
|
|
21
|
+
description: "For an UNCERTAIN command, creates a pending approval request and prompts a human at the server's terminal for a y/n decision. Returns an approvalId to poll with exec_guard_check_approval_status.",
|
|
22
|
+
schema: requestApprovalSchema,
|
|
23
|
+
handler: exec_guard_request_approval,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "exec_guard_check_approval_status",
|
|
27
|
+
description: "Polls the status of a pending approval request. Returns a signed single-use approvalToken once a human approves.",
|
|
28
|
+
schema: checkApprovalStatusSchema,
|
|
29
|
+
handler: exec_guard_check_approval_status,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "exec_guard_audit_log",
|
|
33
|
+
description: "Read-only: returns recent classification/execution decisions from the local tamper-evident (hash-chained) audit log.",
|
|
34
|
+
schema: auditLogSchema,
|
|
35
|
+
handler: exec_guard_audit_log,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACvG,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AASrE,MAAM,CAAC,MAAM,KAAK,GAAqB;IACrC;QACE,IAAI,EAAE,kBAAkB;QACxB,WAAW,EACT,mOAAmO;QACrO,MAAM,EAAE,WAAW;QACnB,OAAO,EAAE,gBAAgB;KAC1B;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,WAAW,EACT,6TAA6T;QAC/T,MAAM,EAAE,SAAS;QACjB,OAAO,EAAE,cAAc;KACxB;IACD;QACE,IAAI,EAAE,6BAA6B;QACnC,WAAW,EACT,oMAAoM;QACtM,MAAM,EAAE,qBAAqB;QAC7B,OAAO,EAAE,2BAA2B;KACrC;IACD;QACE,IAAI,EAAE,kCAAkC;QACxC,WAAW,EAAE,kHAAkH;QAC/H,MAAM,EAAE,yBAAyB;QACjC,OAAO,EAAE,gCAAgC;KAC1C;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,WAAW,EAAE,sHAAsH;QACnI,MAAM,EAAE,cAAc;QACtB,OAAO,EAAE,oBAAoB;KAC9B;CACF,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runPipeline } from "../pipeline.js";
|
|
3
|
+
import { createApproval } from "../approval/store.js";
|
|
4
|
+
import { promptForApproval } from "../approval/cliApproval.js";
|
|
5
|
+
export const requestApprovalSchema = z.object({
|
|
6
|
+
command: z.string().min(1),
|
|
7
|
+
cwd: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
/** For an UNCERTAIN command, creates a pending approval request and
|
|
10
|
+
* (v0.1) prompts the human running the server via the CLI. Returns an
|
|
11
|
+
* approvalId the agent should poll with exec_guard_check_approval_status.
|
|
12
|
+
* Refuses outright for BLOCKED commands (no point asking a human to
|
|
13
|
+
* approve something that will never be allowed to run) and for already-SAFE
|
|
14
|
+
* commands (no approval needed -- call exec_guard_run directly). */
|
|
15
|
+
export async function exec_guard_request_approval(input) {
|
|
16
|
+
const result = runPipeline(input.command);
|
|
17
|
+
if (result.classification === "BLOCKED") {
|
|
18
|
+
return { error: "blocked", message: `This command is BLOCKED and cannot be approved: ${result.reason}` };
|
|
19
|
+
}
|
|
20
|
+
if (result.classification === "SAFE") {
|
|
21
|
+
return { error: "already_safe", message: "This command already classifies SAFE -- call exec_guard_run directly, no approval needed." };
|
|
22
|
+
}
|
|
23
|
+
if (!result.commandChecksum) {
|
|
24
|
+
return { error: "internal_error", message: "no checksum computed" };
|
|
25
|
+
}
|
|
26
|
+
const approval = await createApproval(input.command, result.commandChecksum, input.cwd);
|
|
27
|
+
promptForApproval(approval);
|
|
28
|
+
return {
|
|
29
|
+
approvalId: approval.id,
|
|
30
|
+
status: approval.status,
|
|
31
|
+
expiresAt: approval.expiresAt,
|
|
32
|
+
message: "Approval requested. A human must respond at the server's terminal. Poll exec_guard_check_approval_status with this approvalId.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=requestApproval.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestApproval.js","sourceRoot":"","sources":["../../src/tools/requestApproval.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC3B,CAAC,CAAC;AAEH;;;;;oEAKoE;AACpE,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAAC,KAA4C;IAC5F,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE1C,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACxC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,mDAAmD,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;IAC3G,CAAC;IACD,IAAI,MAAM,CAAC,cAAc,KAAK,MAAM,EAAE,CAAC;QACrC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,2FAA2F,EAAE,CAAC;IACzI,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;QAC5B,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;IACtE,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACxF,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAE5B,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,EAAE;QACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,OAAO,EAAE,gIAAgI;KAC1I,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runPipeline } from "../pipeline.js";
|
|
3
|
+
import { executeParsedCommand } from "../exec/run.js";
|
|
4
|
+
import { consumeToken } from "../approval/store.js";
|
|
5
|
+
import { appendAudit } from "../security/audit.js";
|
|
6
|
+
export const runSchema = z.object({
|
|
7
|
+
command: z.string().min(1),
|
|
8
|
+
cwd: z.string().optional(),
|
|
9
|
+
approvalId: z.string().optional(),
|
|
10
|
+
approvalToken: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* The only tool that actually executes a command. Deliberately re-runs the
|
|
14
|
+
* exact same normalize -> parse -> classify pipeline as exec_guard_check
|
|
15
|
+
* internally -- it never trusts a classification, checksum, or "this was
|
|
16
|
+
* already checked" claim passed in from outside (see docs/security-model.md,
|
|
17
|
+
* "Re-verification, not trust-on-first-check").
|
|
18
|
+
*
|
|
19
|
+
* - SAFE: executes immediately.
|
|
20
|
+
* - BLOCKED: refuses with a structured reason, never executes.
|
|
21
|
+
* - UNCERTAIN: requires approvalId + approvalToken; verifies signature,
|
|
22
|
+
* expiry, single-use, and that the token's checksum matches the freshly
|
|
23
|
+
* re-parsed command -- only executes on success.
|
|
24
|
+
*/
|
|
25
|
+
export async function exec_guard_run(input) {
|
|
26
|
+
const cwd = input.cwd ?? process.cwd();
|
|
27
|
+
const result = runPipeline(input.command);
|
|
28
|
+
if (result.classification === "BLOCKED") {
|
|
29
|
+
appendAudit({ type: "run_blocked", command: input.command, classification: "BLOCKED", commandChecksum: result.commandChecksum ?? undefined, detail: { reason: result.reason } });
|
|
30
|
+
return { executed: false, classification: "BLOCKED", reason: result.reason };
|
|
31
|
+
}
|
|
32
|
+
if (result.classification === "UNCERTAIN") {
|
|
33
|
+
if (!input.approvalId || !input.approvalToken) {
|
|
34
|
+
appendAudit({ type: "run_uncertain_no_token", command: input.command, classification: "UNCERTAIN", commandChecksum: result.commandChecksum ?? undefined });
|
|
35
|
+
return {
|
|
36
|
+
executed: false,
|
|
37
|
+
classification: "UNCERTAIN",
|
|
38
|
+
reason: `${result.reason}. Call exec_guard_request_approval, then retry exec_guard_run with the resulting approvalId and approvalToken.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (!result.commandChecksum) {
|
|
42
|
+
return { executed: false, classification: "UNCERTAIN", reason: "internal error: no checksum computed for an UNCERTAIN command" };
|
|
43
|
+
}
|
|
44
|
+
const consumed = await consumeToken(input.approvalId, input.approvalToken, result.commandChecksum);
|
|
45
|
+
if (!consumed.ok) {
|
|
46
|
+
appendAudit({ type: "run_rejected", command: input.command, classification: "UNCERTAIN", commandChecksum: result.commandChecksum, approvalId: input.approvalId, detail: { reason: consumed.reason } });
|
|
47
|
+
return { executed: false, classification: "UNCERTAIN", reason: describeRejection(consumed.reason) };
|
|
48
|
+
}
|
|
49
|
+
// Falls through to execution below -- token verified, single-use
|
|
50
|
+
// consumed atomically.
|
|
51
|
+
}
|
|
52
|
+
if (!result.parsedCommand.ok) {
|
|
53
|
+
// Cannot happen for SAFE/consumed-UNCERTAIN (classify() only returns
|
|
54
|
+
// those for a successful parse), but keeps the type checker honest.
|
|
55
|
+
return { executed: false, classification: result.classification, reason: "internal error: no parsed command available to execute" };
|
|
56
|
+
}
|
|
57
|
+
const outcome = await executeParsedCommand(result.parsedCommand, cwd);
|
|
58
|
+
appendAudit({
|
|
59
|
+
type: "run_executed",
|
|
60
|
+
command: input.command,
|
|
61
|
+
classification: result.classification,
|
|
62
|
+
commandChecksum: result.commandChecksum ?? undefined,
|
|
63
|
+
approvalId: input.approvalId,
|
|
64
|
+
detail: { exitCode: outcome.exitCode, timedOut: outcome.timedOut },
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
executed: true,
|
|
68
|
+
classification: result.classification,
|
|
69
|
+
exitCode: outcome.exitCode,
|
|
70
|
+
timedOut: outcome.timedOut,
|
|
71
|
+
stdout: outcome.stdout,
|
|
72
|
+
stderr: outcome.stderr,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function describeRejection(reason) {
|
|
76
|
+
switch (reason) {
|
|
77
|
+
case "not_found":
|
|
78
|
+
return "No such approval request.";
|
|
79
|
+
case "not_approved":
|
|
80
|
+
return "This request has not been approved yet (or was denied/expired). Poll exec_guard_check_approval_status.";
|
|
81
|
+
case "already_consumed":
|
|
82
|
+
return "This approval token has already been used. Tokens are single-use; call exec_guard_request_approval again to retry.";
|
|
83
|
+
case "expired":
|
|
84
|
+
return "This approval token has expired.";
|
|
85
|
+
case "checksum_mismatch":
|
|
86
|
+
return "The command differs from what was approved (checksum mismatch) -- approving one command never approves a different one. Request a fresh approval for the exact command you want to run.";
|
|
87
|
+
case "bad_signature":
|
|
88
|
+
return "Token signature is invalid.";
|
|
89
|
+
default:
|
|
90
|
+
return "Approval could not be verified.";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/tools/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAgC;IACnE,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE1C,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACxC,WAAW,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjL,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;IAC/E,CAAC;IAED,IAAI,MAAM,CAAC,cAAc,KAAK,WAAW,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;YAC9C,WAAW,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,SAAS,EAAE,CAAC,CAAC;YAC3J,OAAO;gBACL,QAAQ,EAAE,KAAK;gBACf,cAAc,EAAE,WAAW;gBAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,gHAAgH;aACzI,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC5B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,EAAE,+DAA+D,EAAE,CAAC;QACnI,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,aAAa,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;QACnG,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,WAAW,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACvM,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACtG,CAAC;QACD,iEAAiE;QACjE,uBAAuB;IACzB,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC;QAC7B,qEAAqE;QACrE,oEAAoE;QACpE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,wDAAwD,EAAE,CAAC;IACtI,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IACtE,WAAW,CAAC;QACV,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,SAAS;QACpD,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE;KACnE,CAAC,CAAC;IAEH,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,MAA0B;IACnD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,WAAW;YACd,OAAO,2BAA2B,CAAC;QACrC,KAAK,cAAc;YACjB,OAAO,wGAAwG,CAAC;QAClH,KAAK,kBAAkB;YACrB,OAAO,oHAAoH,CAAC;QAC9H,KAAK,SAAS;YACZ,OAAO,kCAAkC,CAAC;QAC5C,KAAK,mBAAmB;YACtB,OAAO,yLAAyL,CAAC;QACnM,KAAK,eAAe;YAClB,OAAO,6BAA6B,CAAC;QACvC;YACE,OAAO,iCAAiC,CAAC;IAC7C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [];
|
|
2
|
+
// Registered lazily by security/secret.ts so the logger module doesn't need
|
|
3
|
+
// to import the secret directly (keeps the secret's blast radius to one
|
|
4
|
+
// file).
|
|
5
|
+
export function registerRedactedValue(value) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return;
|
|
8
|
+
SECRET_PATTERNS.push(new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"));
|
|
9
|
+
}
|
|
10
|
+
function redact(msg) {
|
|
11
|
+
let out = msg;
|
|
12
|
+
for (const re of SECRET_PATTERNS)
|
|
13
|
+
out = out.replace(re, "[REDACTED]");
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function line(level, msg, meta) {
|
|
17
|
+
const entry = {
|
|
18
|
+
ts: new Date().toISOString(),
|
|
19
|
+
level,
|
|
20
|
+
msg: redact(msg),
|
|
21
|
+
...(meta ? { meta: JSON.parse(redact(JSON.stringify(meta))) } : {}),
|
|
22
|
+
};
|
|
23
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
24
|
+
}
|
|
25
|
+
export const logger = {
|
|
26
|
+
info: (msg, meta) => line("info", msg, meta),
|
|
27
|
+
warn: (msg, meta) => line("warn", msg, meta),
|
|
28
|
+
error: (msg, meta) => line("error", msg, meta),
|
|
29
|
+
debug: (msg, meta) => {
|
|
30
|
+
if (process.env.AGENT_EXEC_GUARD_DEBUG)
|
|
31
|
+
line("debug", msg, meta);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAa,EAAE,CAAC;AAErC,4EAA4E;AAC5E,wEAAwE;AACxE,SAAS;AACT,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,eAAe,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,MAAM,CAAC,GAAW;IACzB,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,KAAK,MAAM,EAAE,IAAI,eAAe;QAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;IACtE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,GAAW,EAAE,IAA8B;IACtE,MAAM,KAAK,GAAG;QACZ,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,KAAK;QACL,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;QAChB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpE,CAAC;IACF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,CAAC,GAAW,EAAE,IAA8B,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC9E,IAAI,EAAE,CAAC,GAAW,EAAE,IAA8B,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC9E,KAAK,EAAE,CAAC,GAAW,EAAE,IAA8B,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;IAChF,KAAK,EAAE,CAAC,GAAW,EAAE,IAA8B,EAAE,EAAE;QACrD,IAAI,OAAO,CAAC,GAAG,CAAC,sBAAsB;YAAE,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACnE,CAAC;CACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-exec-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server that fully parses and classifies shell commands (SAFE/BLOCKED/UNCERTAIN) before AI coding agents execute them, requiring a human-signed single-use token for anything uncertain.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-exec-guard": "bin/agent-exec-guard.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "eslint src",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"security",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"shell",
|
|
30
|
+
"command-validation",
|
|
31
|
+
"sandbox",
|
|
32
|
+
"approval",
|
|
33
|
+
"claude",
|
|
34
|
+
"cursor"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"author": "Sourabh Yogi",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/SORABH13/agent-exec-guard.git"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
47
|
+
"mvdan-sh": "^0.10.1",
|
|
48
|
+
"sql.js": "^1.14.2",
|
|
49
|
+
"zod": "^3.23.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.14.0",
|
|
53
|
+
"@types/sql.js": "^1.4.11",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
56
|
+
"eslint": "^9.9.0",
|
|
57
|
+
"tsx": "^4.16.2",
|
|
58
|
+
"typescript": "^5.5.4",
|
|
59
|
+
"vitest": "^2.0.5"
|
|
60
|
+
}
|
|
61
|
+
}
|