fix: allow chained exec allowlists
Co-authored-by: Lucas Czekaj <1464539+czekaj@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||||
|
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
||||||
- Agents: avoid sanitizing tool call IDs for OpenAI responses to preserve Pi pairing.
|
- Agents: avoid sanitizing tool call IDs for OpenAI responses to preserve Pi pairing.
|
||||||
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
|
||||||
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ that can run in allowlist mode **without** explicit allowlist entries. Safe bins
|
|||||||
positional file args and path-like tokens, so they can only operate on the incoming stream.
|
positional file args and path-like tokens, so they can only operate on the incoming stream.
|
||||||
Shell chaining and redirections are not auto-allowed in allowlist mode.
|
Shell chaining and redirections are not auto-allowed in allowlist mode.
|
||||||
|
|
||||||
|
Shell chaining (`&&`, `||`, `;`) is allowed when every top-level segment satisfies the allowlist
|
||||||
|
(including safe bins or skill auto-allow). Redirections remain unsupported in allowlist mode.
|
||||||
|
|
||||||
Default safe bins: `jq`, `grep`, `cut`, `sort`, `uniq`, `head`, `tail`, `tr`, `wc`.
|
Default safe bins: `jq`, `grep`, `cut`, `sort`, `uniq`, `head`, `tail`, `tr`, `wc`.
|
||||||
|
|
||||||
## Control UI editing
|
## Control UI editing
|
||||||
|
|||||||
@@ -16,11 +16,15 @@ vi.mock("./tools/nodes-utils.js", () => ({
|
|||||||
|
|
||||||
describe("exec approvals", () => {
|
describe("exec approvals", () => {
|
||||||
let previousHome: string | undefined;
|
let previousHome: string | undefined;
|
||||||
|
let previousUserProfile: string | undefined;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
previousHome = process.env.HOME;
|
previousHome = process.env.HOME;
|
||||||
|
previousUserProfile = process.env.USERPROFILE;
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-"));
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-"));
|
||||||
process.env.HOME = tempDir;
|
process.env.HOME = tempDir;
|
||||||
|
// Windows uses USERPROFILE for os.homedir()
|
||||||
|
process.env.USERPROFILE = tempDir;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -30,6 +34,11 @@ describe("exec approvals", () => {
|
|||||||
} else {
|
} else {
|
||||||
process.env.HOME = previousHome;
|
process.env.HOME = previousHome;
|
||||||
}
|
}
|
||||||
|
if (previousUserProfile === undefined) {
|
||||||
|
delete process.env.USERPROFILE;
|
||||||
|
} else {
|
||||||
|
process.env.USERPROFILE = previousUserProfile;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reuses approval id as the node runId", async () => {
|
it("reuses approval id as the node runId", async () => {
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
type ExecSecurity,
|
type ExecSecurity,
|
||||||
type ExecApprovalsFile,
|
type ExecApprovalsFile,
|
||||||
addAllowlistEntry,
|
addAllowlistEntry,
|
||||||
analyzeShellCommand,
|
evaluateShellAllowlist,
|
||||||
evaluateExecAllowlist,
|
|
||||||
maxAsk,
|
maxAsk,
|
||||||
minSecurity,
|
minSecurity,
|
||||||
requiresExecApproval,
|
requiresExecApproval,
|
||||||
@@ -871,9 +870,16 @@ export function createExecTool(
|
|||||||
if (nodeEnv) {
|
if (nodeEnv) {
|
||||||
applyPathPrepend(nodeEnv, defaultPathPrepend, { requireExisting: true });
|
applyPathPrepend(nodeEnv, defaultPathPrepend, { requireExisting: true });
|
||||||
}
|
}
|
||||||
const analysis = analyzeShellCommand({ command: params.command, cwd: workdir, env });
|
const baseAllowlistEval = evaluateShellAllowlist({
|
||||||
|
command: params.command,
|
||||||
|
allowlist: [],
|
||||||
|
safeBins: new Set(),
|
||||||
|
cwd: workdir,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
let analysisOk = baseAllowlistEval.analysisOk;
|
||||||
let allowlistSatisfied = false;
|
let allowlistSatisfied = false;
|
||||||
if (hostAsk === "on-miss" && hostSecurity === "allowlist") {
|
if (hostAsk === "on-miss" && hostSecurity === "allowlist" && analysisOk) {
|
||||||
try {
|
try {
|
||||||
const approvalsSnapshot = (await callGatewayTool(
|
const approvalsSnapshot = (await callGatewayTool(
|
||||||
"exec.approvals.node.get",
|
"exec.approvals.node.get",
|
||||||
@@ -891,12 +897,15 @@ export function createExecTool(
|
|||||||
overrides: { security: "allowlist" },
|
overrides: { security: "allowlist" },
|
||||||
});
|
});
|
||||||
// Allowlist-only precheck; safe bins are node-local and may diverge.
|
// Allowlist-only precheck; safe bins are node-local and may diverge.
|
||||||
allowlistSatisfied = evaluateExecAllowlist({
|
const allowlistEval = evaluateShellAllowlist({
|
||||||
analysis,
|
command: params.command,
|
||||||
allowlist: resolved.allowlist,
|
allowlist: resolved.allowlist,
|
||||||
safeBins: new Set(),
|
safeBins: new Set(),
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
}).allowlistSatisfied;
|
env,
|
||||||
|
});
|
||||||
|
allowlistSatisfied = allowlistEval.allowlistSatisfied;
|
||||||
|
analysisOk = allowlistEval.analysisOk;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to requiring approval if node approvals cannot be fetched.
|
// Fall back to requiring approval if node approvals cannot be fetched.
|
||||||
@@ -905,7 +914,7 @@ export function createExecTool(
|
|||||||
const requiresAsk = requiresExecApproval({
|
const requiresAsk = requiresExecApproval({
|
||||||
ask: hostAsk,
|
ask: hostAsk,
|
||||||
security: hostSecurity,
|
security: hostSecurity,
|
||||||
analysisOk: analysis.ok,
|
analysisOk,
|
||||||
allowlistSatisfied,
|
allowlistSatisfied,
|
||||||
});
|
});
|
||||||
const commandText = params.command;
|
const commandText = params.command;
|
||||||
@@ -1095,20 +1104,21 @@ export function createExecTool(
|
|||||||
if (hostSecurity === "deny") {
|
if (hostSecurity === "deny") {
|
||||||
throw new Error("exec denied: host=gateway security=deny");
|
throw new Error("exec denied: host=gateway security=deny");
|
||||||
}
|
}
|
||||||
const analysis = analyzeShellCommand({ command: params.command, cwd: workdir, env });
|
const allowlistEval = evaluateShellAllowlist({
|
||||||
const allowlistEval = evaluateExecAllowlist({
|
command: params.command,
|
||||||
analysis,
|
|
||||||
allowlist: approvals.allowlist,
|
allowlist: approvals.allowlist,
|
||||||
safeBins,
|
safeBins,
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
|
env,
|
||||||
});
|
});
|
||||||
const allowlistMatches = allowlistEval.allowlistMatches;
|
const allowlistMatches = allowlistEval.allowlistMatches;
|
||||||
|
const analysisOk = allowlistEval.analysisOk;
|
||||||
const allowlistSatisfied =
|
const allowlistSatisfied =
|
||||||
hostSecurity === "allowlist" && analysis.ok ? allowlistEval.allowlistSatisfied : false;
|
hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
|
||||||
const requiresAsk = requiresExecApproval({
|
const requiresAsk = requiresExecApproval({
|
||||||
ask: hostAsk,
|
ask: hostAsk,
|
||||||
security: hostSecurity,
|
security: hostSecurity,
|
||||||
analysisOk: analysis.ok,
|
analysisOk,
|
||||||
allowlistSatisfied,
|
allowlistSatisfied,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1117,7 +1127,7 @@ export function createExecTool(
|
|||||||
const approvalSlug = createApprovalSlug(approvalId);
|
const approvalSlug = createApprovalSlug(approvalId);
|
||||||
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
||||||
const contextKey = `exec:${approvalId}`;
|
const contextKey = `exec:${approvalId}`;
|
||||||
const resolvedPath = analysis.segments[0]?.resolution?.resolvedPath;
|
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
|
||||||
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
||||||
const commandText = params.command;
|
const commandText = params.command;
|
||||||
const effectiveTimeout =
|
const effectiveTimeout =
|
||||||
@@ -1164,7 +1174,7 @@ export function createExecTool(
|
|||||||
if (askFallback === "full") {
|
if (askFallback === "full") {
|
||||||
approvedByAsk = true;
|
approvedByAsk = true;
|
||||||
} else if (askFallback === "allowlist") {
|
} else if (askFallback === "allowlist") {
|
||||||
if (!analysis.ok || !allowlistSatisfied) {
|
if (!analysisOk || !allowlistSatisfied) {
|
||||||
deniedReason = "approval-timeout (allowlist-miss)";
|
deniedReason = "approval-timeout (allowlist-miss)";
|
||||||
} else {
|
} else {
|
||||||
approvedByAsk = true;
|
approvedByAsk = true;
|
||||||
@@ -1177,7 +1187,7 @@ export function createExecTool(
|
|||||||
} else if (decision === "allow-always") {
|
} else if (decision === "allow-always") {
|
||||||
approvedByAsk = true;
|
approvedByAsk = true;
|
||||||
if (hostSecurity === "allowlist") {
|
if (hostSecurity === "allowlist") {
|
||||||
for (const segment of analysis.segments) {
|
for (const segment of allowlistEval.segments) {
|
||||||
const pattern = segment.resolution?.resolvedPath ?? "";
|
const pattern = segment.resolution?.resolvedPath ?? "";
|
||||||
if (pattern) {
|
if (pattern) {
|
||||||
addAllowlistEntry(approvals.file, agentId, pattern);
|
addAllowlistEntry(approvals.file, agentId, pattern);
|
||||||
@@ -1188,7 +1198,7 @@ export function createExecTool(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
hostSecurity === "allowlist" &&
|
hostSecurity === "allowlist" &&
|
||||||
(!analysis.ok || !allowlistSatisfied) &&
|
(!analysisOk || !allowlistSatisfied) &&
|
||||||
!approvedByAsk
|
!approvedByAsk
|
||||||
) {
|
) {
|
||||||
deniedReason = deniedReason ?? "allowlist-miss";
|
deniedReason = deniedReason ?? "allowlist-miss";
|
||||||
@@ -1288,7 +1298,7 @@ export function createExecTool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostSecurity === "allowlist" && (!analysis.ok || !allowlistSatisfied)) {
|
if (hostSecurity === "allowlist" && (!analysisOk || !allowlistSatisfied)) {
|
||||||
throw new Error("exec denied: allowlist miss");
|
throw new Error("exec denied: allowlist miss");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,7 +1312,7 @@ export function createExecTool(
|
|||||||
agentId,
|
agentId,
|
||||||
match,
|
match,
|
||||||
params.command,
|
params.command,
|
||||||
analysis.segments[0]?.resolution?.resolvedPath,
|
allowlistEval.segments[0]?.resolution?.resolvedPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
analyzeArgvCommand,
|
analyzeArgvCommand,
|
||||||
analyzeShellCommand,
|
analyzeShellCommand,
|
||||||
evaluateExecAllowlist,
|
evaluateExecAllowlist,
|
||||||
|
evaluateShellAllowlist,
|
||||||
isSafeBinUsage,
|
isSafeBinUsage,
|
||||||
matchAllowlist,
|
matchAllowlist,
|
||||||
maxAsk,
|
maxAsk,
|
||||||
@@ -121,9 +122,10 @@ describe("exec approvals shell parsing", () => {
|
|||||||
expect(res.segments.map((seg) => seg.argv[0])).toEqual(["echo", "jq"]);
|
expect(res.segments.map((seg) => seg.argv[0])).toEqual(["echo", "jq"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects chained commands", () => {
|
it("parses chained commands", () => {
|
||||||
const res = analyzeShellCommand({ command: "ls && rm -rf /" });
|
const res = analyzeShellCommand({ command: "ls && rm -rf /" });
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(true);
|
||||||
|
expect(res.chains?.map((chain) => chain[0]?.argv[0])).toEqual(["ls", "rm"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses argv commands", () => {
|
it("parses argv commands", () => {
|
||||||
@@ -133,6 +135,60 @@ describe("exec approvals shell parsing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("exec approvals shell allowlist (chained commands)", () => {
|
||||||
|
it("allows chained commands when all parts are allowlisted", () => {
|
||||||
|
const allowlist: ExecAllowlistEntry[] = [
|
||||||
|
{ pattern: "/usr/bin/obsidian-cli" },
|
||||||
|
{ pattern: "/usr/bin/head" },
|
||||||
|
];
|
||||||
|
const result = evaluateShellAllowlist({
|
||||||
|
command:
|
||||||
|
"/usr/bin/obsidian-cli print-default && /usr/bin/obsidian-cli search foo | /usr/bin/head",
|
||||||
|
allowlist,
|
||||||
|
safeBins: new Set(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
expect(result.analysisOk).toBe(true);
|
||||||
|
expect(result.allowlistSatisfied).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects chained commands when any part is not allowlisted", () => {
|
||||||
|
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/obsidian-cli" }];
|
||||||
|
const result = evaluateShellAllowlist({
|
||||||
|
command: "/usr/bin/obsidian-cli print-default && /usr/bin/rm -rf /",
|
||||||
|
allowlist,
|
||||||
|
safeBins: new Set(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
expect(result.analysisOk).toBe(true);
|
||||||
|
expect(result.allowlistSatisfied).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns analysisOk=false for malformed chains", () => {
|
||||||
|
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }];
|
||||||
|
const result = evaluateShellAllowlist({
|
||||||
|
command: "/usr/bin/echo ok &&",
|
||||||
|
allowlist,
|
||||||
|
safeBins: new Set(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
expect(result.analysisOk).toBe(false);
|
||||||
|
expect(result.allowlistSatisfied).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects quotes when splitting chains", () => {
|
||||||
|
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }];
|
||||||
|
const result = evaluateShellAllowlist({
|
||||||
|
command: '/usr/bin/echo "foo && bar"',
|
||||||
|
allowlist,
|
||||||
|
safeBins: new Set(),
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
expect(result.analysisOk).toBe(true);
|
||||||
|
expect(result.allowlistSatisfied).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("exec approvals safe bins", () => {
|
describe("exec approvals safe bins", () => {
|
||||||
it("allows safe bins with non-path args", () => {
|
it("allows safe bins with non-path args", () => {
|
||||||
const dir = makeTempDir();
|
const dir = makeTempDir();
|
||||||
|
|||||||
@@ -506,29 +506,44 @@ export type ExecCommandAnalysis = {
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
segments: ExecCommandSegment[];
|
segments: ExecCommandSegment[];
|
||||||
|
chains?: ExecCommandSegment[][]; // Segments grouped by chain operator (&&, ||, ;)
|
||||||
};
|
};
|
||||||
|
|
||||||
const DISALLOWED_TOKENS = new Set([";", "&", ">", "<", "`", "\n", "\r", "(", ")"]);
|
const DISALLOWED_PIPELINE_TOKENS = new Set([">", "<", "`", "\n", "\r", "(", ")"]);
|
||||||
|
|
||||||
function splitShellPipeline(command: string): { ok: boolean; reason?: string; segments: string[] } {
|
type IteratorAction = "split" | "skip" | "include" | { reject: string };
|
||||||
const segments: string[] = [];
|
|
||||||
|
/**
|
||||||
|
* Iterates through a command string while respecting shell quoting rules.
|
||||||
|
* The callback receives each character and the next character, and returns an action:
|
||||||
|
* - "split": push current buffer as a segment and start a new one
|
||||||
|
* - "skip": skip this character (and optionally the next via skip count)
|
||||||
|
* - "include": add this character to the buffer
|
||||||
|
* - { reject: reason }: abort with an error
|
||||||
|
*/
|
||||||
|
function iterateQuoteAware(
|
||||||
|
command: string,
|
||||||
|
onChar: (ch: string, next: string | undefined, index: number) => IteratorAction,
|
||||||
|
): { ok: true; parts: string[]; hasSplit: boolean } | { ok: false; reason: string } {
|
||||||
|
const parts: string[] = [];
|
||||||
let buf = "";
|
let buf = "";
|
||||||
let inSingle = false;
|
let inSingle = false;
|
||||||
let inDouble = false;
|
let inDouble = false;
|
||||||
let escaped = false;
|
let escaped = false;
|
||||||
|
let hasSplit = false;
|
||||||
|
|
||||||
const pushSegment = () => {
|
const pushPart = () => {
|
||||||
const trimmed = buf.trim();
|
const trimmed = buf.trim();
|
||||||
if (!trimmed) {
|
if (trimmed) {
|
||||||
return false;
|
parts.push(trimmed);
|
||||||
}
|
}
|
||||||
segments.push(trimmed);
|
|
||||||
buf = "";
|
buf = "";
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < command.length; i += 1) {
|
for (let i = 0; i < command.length; i += 1) {
|
||||||
const ch = command[i];
|
const ch = command[i];
|
||||||
|
const next = command[i + 1];
|
||||||
|
|
||||||
if (escaped) {
|
if (escaped) {
|
||||||
buf += ch;
|
buf += ch;
|
||||||
escaped = false;
|
escaped = false;
|
||||||
@@ -559,34 +574,66 @@ function splitShellPipeline(command: string): { ok: boolean; reason?: string; se
|
|||||||
buf += ch;
|
buf += ch;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch === "|" && command[i + 1] === "|") {
|
|
||||||
return { ok: false, reason: "unsupported shell token: ||", segments: [] };
|
const action = onChar(ch, next, i);
|
||||||
}
|
if (typeof action === "object" && "reject" in action) {
|
||||||
if (ch === "|" && command[i + 1] === "&") {
|
return { ok: false, reason: action.reject };
|
||||||
return { ok: false, reason: "unsupported shell token: |&", segments: [] };
|
|
||||||
}
|
|
||||||
if (ch === "|") {
|
|
||||||
if (!pushSegment()) {
|
|
||||||
return { ok: false, reason: "empty pipeline segment", segments: [] };
|
|
||||||
}
|
}
|
||||||
|
if (action === "split") {
|
||||||
|
pushPart();
|
||||||
|
hasSplit = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (DISALLOWED_TOKENS.has(ch)) {
|
if (action === "skip") {
|
||||||
return { ok: false, reason: `unsupported shell token: ${ch}`, segments: [] };
|
continue;
|
||||||
}
|
|
||||||
if (ch === "$" && command[i + 1] === "(") {
|
|
||||||
return { ok: false, reason: "unsupported shell token: $()", segments: [] };
|
|
||||||
}
|
}
|
||||||
buf += ch;
|
buf += ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (escaped || inSingle || inDouble) {
|
if (escaped || inSingle || inDouble) {
|
||||||
return { ok: false, reason: "unterminated shell quote/escape", segments: [] };
|
return { ok: false, reason: "unterminated shell quote/escape" };
|
||||||
}
|
}
|
||||||
if (!pushSegment()) {
|
pushPart();
|
||||||
return { ok: false, reason: "empty command", segments: [] };
|
return { ok: true, parts, hasSplit };
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitShellPipeline(command: string): { ok: boolean; reason?: string; segments: string[] } {
|
||||||
|
let emptySegment = false;
|
||||||
|
const result = iterateQuoteAware(command, (ch, next) => {
|
||||||
|
if (ch === "|" && next === "|") {
|
||||||
|
return { reject: "unsupported shell token: ||" };
|
||||||
}
|
}
|
||||||
return { ok: true, segments };
|
if (ch === "|" && next === "&") {
|
||||||
|
return { reject: "unsupported shell token: |&" };
|
||||||
|
}
|
||||||
|
if (ch === "|") {
|
||||||
|
emptySegment = true;
|
||||||
|
return "split";
|
||||||
|
}
|
||||||
|
if (ch === "&" || ch === ";") {
|
||||||
|
return { reject: `unsupported shell token: ${ch}` };
|
||||||
|
}
|
||||||
|
if (DISALLOWED_PIPELINE_TOKENS.has(ch)) {
|
||||||
|
return { reject: `unsupported shell token: ${ch}` };
|
||||||
|
}
|
||||||
|
if (ch === "$" && next === "(") {
|
||||||
|
return { reject: "unsupported shell token: $()" };
|
||||||
|
}
|
||||||
|
emptySegment = false;
|
||||||
|
return "include";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return { ok: false, reason: result.reason, segments: [] };
|
||||||
|
}
|
||||||
|
if (emptySegment || result.parts.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: result.parts.length === 0 ? "empty command" : "empty pipeline segment",
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true, segments: result.parts };
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenizeShellSegment(segment: string): string[] | null {
|
function tokenizeShellSegment(segment: string): string[] | null {
|
||||||
@@ -652,27 +699,62 @@ function tokenizeShellSegment(segment: string): string[] | null {
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSegmentsFromParts(
|
||||||
|
parts: string[],
|
||||||
|
cwd?: string,
|
||||||
|
env?: NodeJS.ProcessEnv,
|
||||||
|
): ExecCommandSegment[] | null {
|
||||||
|
const segments: ExecCommandSegment[] = [];
|
||||||
|
for (const raw of parts) {
|
||||||
|
const argv = tokenizeShellSegment(raw);
|
||||||
|
if (!argv || argv.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
segments.push({
|
||||||
|
raw,
|
||||||
|
argv,
|
||||||
|
resolution: resolveCommandResolutionFromArgv(argv, cwd, env),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
export function analyzeShellCommand(params: {
|
export function analyzeShellCommand(params: {
|
||||||
command: string;
|
command: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}): ExecCommandAnalysis {
|
}): ExecCommandAnalysis {
|
||||||
|
// First try splitting by chain operators (&&, ||, ;)
|
||||||
|
const chainParts = splitCommandChain(params.command);
|
||||||
|
if (chainParts) {
|
||||||
|
const chains: ExecCommandSegment[][] = [];
|
||||||
|
const allSegments: ExecCommandSegment[] = [];
|
||||||
|
|
||||||
|
for (const part of chainParts) {
|
||||||
|
const pipelineSplit = splitShellPipeline(part);
|
||||||
|
if (!pipelineSplit.ok) {
|
||||||
|
return { ok: false, reason: pipelineSplit.reason, segments: [] };
|
||||||
|
}
|
||||||
|
const segments = parseSegmentsFromParts(pipelineSplit.segments, params.cwd, params.env);
|
||||||
|
if (!segments) {
|
||||||
|
return { ok: false, reason: "unable to parse shell segment", segments: [] };
|
||||||
|
}
|
||||||
|
chains.push(segments);
|
||||||
|
allSegments.push(...segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, segments: allSegments, chains };
|
||||||
|
}
|
||||||
|
|
||||||
|
// No chain operators, parse as simple pipeline
|
||||||
const split = splitShellPipeline(params.command);
|
const split = splitShellPipeline(params.command);
|
||||||
if (!split.ok) {
|
if (!split.ok) {
|
||||||
return { ok: false, reason: split.reason, segments: [] };
|
return { ok: false, reason: split.reason, segments: [] };
|
||||||
}
|
}
|
||||||
const segments: ExecCommandSegment[] = [];
|
const segments = parseSegmentsFromParts(split.segments, params.cwd, params.env);
|
||||||
for (const raw of split.segments) {
|
if (!segments) {
|
||||||
const argv = tokenizeShellSegment(raw);
|
|
||||||
if (!argv || argv.length === 0) {
|
|
||||||
return { ok: false, reason: "unable to parse shell segment", segments: [] };
|
return { ok: false, reason: "unable to parse shell segment", segments: [] };
|
||||||
}
|
}
|
||||||
segments.push({
|
|
||||||
raw,
|
|
||||||
argv,
|
|
||||||
resolution: resolveCommandResolutionFromArgv(argv, params.cwd, params.env),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { ok: true, segments };
|
return { ok: true, segments };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,27 +853,27 @@ export type ExecAllowlistEvaluation = {
|
|||||||
allowlistMatches: ExecAllowlistEntry[];
|
allowlistMatches: ExecAllowlistEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function evaluateExecAllowlist(params: {
|
function evaluateSegments(
|
||||||
analysis: ExecCommandAnalysis;
|
segments: ExecCommandSegment[],
|
||||||
|
params: {
|
||||||
allowlist: ExecAllowlistEntry[];
|
allowlist: ExecAllowlistEntry[];
|
||||||
safeBins: Set<string>;
|
safeBins: Set<string>;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
skillBins?: Set<string>;
|
skillBins?: Set<string>;
|
||||||
autoAllowSkills?: boolean;
|
autoAllowSkills?: boolean;
|
||||||
}): ExecAllowlistEvaluation {
|
},
|
||||||
const allowlistMatches: ExecAllowlistEntry[] = [];
|
): { satisfied: boolean; matches: ExecAllowlistEntry[] } {
|
||||||
if (!params.analysis.ok || params.analysis.segments.length === 0) {
|
const matches: ExecAllowlistEntry[] = [];
|
||||||
return { allowlistSatisfied: false, allowlistMatches };
|
|
||||||
}
|
|
||||||
const allowSkills = params.autoAllowSkills === true && (params.skillBins?.size ?? 0) > 0;
|
const allowSkills = params.autoAllowSkills === true && (params.skillBins?.size ?? 0) > 0;
|
||||||
const allowlistSatisfied = params.analysis.segments.every((segment) => {
|
|
||||||
|
const satisfied = segments.every((segment) => {
|
||||||
const candidatePath = resolveAllowlistCandidatePath(segment.resolution, params.cwd);
|
const candidatePath = resolveAllowlistCandidatePath(segment.resolution, params.cwd);
|
||||||
const candidateResolution =
|
const candidateResolution =
|
||||||
candidatePath && segment.resolution
|
candidatePath && segment.resolution
|
||||||
? { ...segment.resolution, resolvedPath: candidatePath }
|
? { ...segment.resolution, resolvedPath: candidatePath }
|
||||||
: segment.resolution;
|
: segment.resolution;
|
||||||
const match = matchAllowlist(params.allowlist, candidateResolution);
|
const match = matchAllowlist(params.allowlist, candidateResolution);
|
||||||
if (match) allowlistMatches.push(match);
|
if (match) matches.push(match);
|
||||||
const safe = isSafeBinUsage({
|
const safe = isSafeBinUsage({
|
||||||
argv: segment.argv,
|
argv: segment.argv,
|
||||||
resolution: segment.resolution,
|
resolution: segment.resolution,
|
||||||
@@ -804,7 +886,230 @@ export function evaluateExecAllowlist(params: {
|
|||||||
: false;
|
: false;
|
||||||
return Boolean(match || safe || skillAllow);
|
return Boolean(match || safe || skillAllow);
|
||||||
});
|
});
|
||||||
return { allowlistSatisfied, allowlistMatches };
|
|
||||||
|
return { satisfied, matches };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluateExecAllowlist(params: {
|
||||||
|
analysis: ExecCommandAnalysis;
|
||||||
|
allowlist: ExecAllowlistEntry[];
|
||||||
|
safeBins: Set<string>;
|
||||||
|
cwd?: string;
|
||||||
|
skillBins?: Set<string>;
|
||||||
|
autoAllowSkills?: boolean;
|
||||||
|
}): ExecAllowlistEvaluation {
|
||||||
|
const allowlistMatches: ExecAllowlistEntry[] = [];
|
||||||
|
if (!params.analysis.ok || params.analysis.segments.length === 0) {
|
||||||
|
return { allowlistSatisfied: false, allowlistMatches };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the analysis contains chains, evaluate each chain part separately
|
||||||
|
if (params.analysis.chains) {
|
||||||
|
for (const chainSegments of params.analysis.chains) {
|
||||||
|
const result = evaluateSegments(chainSegments, {
|
||||||
|
allowlist: params.allowlist,
|
||||||
|
safeBins: params.safeBins,
|
||||||
|
cwd: params.cwd,
|
||||||
|
skillBins: params.skillBins,
|
||||||
|
autoAllowSkills: params.autoAllowSkills,
|
||||||
|
});
|
||||||
|
if (!result.satisfied) {
|
||||||
|
return { allowlistSatisfied: false, allowlistMatches: [] };
|
||||||
|
}
|
||||||
|
allowlistMatches.push(...result.matches);
|
||||||
|
}
|
||||||
|
return { allowlistSatisfied: true, allowlistMatches };
|
||||||
|
}
|
||||||
|
|
||||||
|
// No chains, evaluate all segments together
|
||||||
|
const result = evaluateSegments(params.analysis.segments, {
|
||||||
|
allowlist: params.allowlist,
|
||||||
|
safeBins: params.safeBins,
|
||||||
|
cwd: params.cwd,
|
||||||
|
skillBins: params.skillBins,
|
||||||
|
autoAllowSkills: params.autoAllowSkills,
|
||||||
|
});
|
||||||
|
return { allowlistSatisfied: result.satisfied, allowlistMatches: result.matches };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a command string by chain operators (&&, ||, ;) while respecting quotes.
|
||||||
|
* Returns null when no chain is present or when the chain is malformed.
|
||||||
|
*/
|
||||||
|
function splitCommandChain(command: string): string[] | null {
|
||||||
|
const parts: string[] = [];
|
||||||
|
let buf = "";
|
||||||
|
let inSingle = false;
|
||||||
|
let inDouble = false;
|
||||||
|
let escaped = false;
|
||||||
|
let foundChain = false;
|
||||||
|
let invalidChain = false;
|
||||||
|
|
||||||
|
const pushPart = () => {
|
||||||
|
const trimmed = buf.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
parts.push(trimmed);
|
||||||
|
buf = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
buf = "";
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < command.length; i += 1) {
|
||||||
|
const ch = command[i];
|
||||||
|
if (escaped) {
|
||||||
|
buf += ch;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inSingle && !inDouble && ch === "\\") {
|
||||||
|
escaped = true;
|
||||||
|
buf += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inSingle) {
|
||||||
|
if (ch === "'") inSingle = false;
|
||||||
|
buf += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inDouble) {
|
||||||
|
if (ch === '"') inDouble = false;
|
||||||
|
buf += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "'") {
|
||||||
|
inSingle = true;
|
||||||
|
buf += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"') {
|
||||||
|
inDouble = true;
|
||||||
|
buf += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === "&" && command[i + 1] === "&") {
|
||||||
|
if (!pushPart()) invalidChain = true;
|
||||||
|
i += 1;
|
||||||
|
foundChain = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "|" && command[i + 1] === "|") {
|
||||||
|
if (!pushPart()) invalidChain = true;
|
||||||
|
i += 1;
|
||||||
|
foundChain = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === ";") {
|
||||||
|
if (!pushPart()) invalidChain = true;
|
||||||
|
foundChain = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf += ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushedFinal = pushPart();
|
||||||
|
if (!foundChain) return null;
|
||||||
|
if (invalidChain || !pushedFinal) return null;
|
||||||
|
return parts.length > 0 ? parts : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecAllowlistAnalysis = {
|
||||||
|
analysisOk: boolean;
|
||||||
|
allowlistSatisfied: boolean;
|
||||||
|
allowlistMatches: ExecAllowlistEntry[];
|
||||||
|
segments: ExecCommandSegment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata.
|
||||||
|
*/
|
||||||
|
export function evaluateShellAllowlist(params: {
|
||||||
|
command: string;
|
||||||
|
allowlist: ExecAllowlistEntry[];
|
||||||
|
safeBins: Set<string>;
|
||||||
|
cwd?: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
skillBins?: Set<string>;
|
||||||
|
autoAllowSkills?: boolean;
|
||||||
|
}): ExecAllowlistAnalysis {
|
||||||
|
const chainParts = splitCommandChain(params.command);
|
||||||
|
if (!chainParts) {
|
||||||
|
const analysis = analyzeShellCommand({
|
||||||
|
command: params.command,
|
||||||
|
cwd: params.cwd,
|
||||||
|
env: params.env,
|
||||||
|
});
|
||||||
|
if (!analysis.ok) {
|
||||||
|
return {
|
||||||
|
analysisOk: false,
|
||||||
|
allowlistSatisfied: false,
|
||||||
|
allowlistMatches: [],
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const evaluation = evaluateExecAllowlist({
|
||||||
|
analysis,
|
||||||
|
allowlist: params.allowlist,
|
||||||
|
safeBins: params.safeBins,
|
||||||
|
cwd: params.cwd,
|
||||||
|
skillBins: params.skillBins,
|
||||||
|
autoAllowSkills: params.autoAllowSkills,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
analysisOk: true,
|
||||||
|
allowlistSatisfied: evaluation.allowlistSatisfied,
|
||||||
|
allowlistMatches: evaluation.allowlistMatches,
|
||||||
|
segments: analysis.segments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowlistMatches: ExecAllowlistEntry[] = [];
|
||||||
|
const segments: ExecCommandSegment[] = [];
|
||||||
|
|
||||||
|
for (const part of chainParts) {
|
||||||
|
const analysis = analyzeShellCommand({
|
||||||
|
command: part,
|
||||||
|
cwd: params.cwd,
|
||||||
|
env: params.env,
|
||||||
|
});
|
||||||
|
if (!analysis.ok) {
|
||||||
|
return {
|
||||||
|
analysisOk: false,
|
||||||
|
allowlistSatisfied: false,
|
||||||
|
allowlistMatches: [],
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(...analysis.segments);
|
||||||
|
const evaluation = evaluateExecAllowlist({
|
||||||
|
analysis,
|
||||||
|
allowlist: params.allowlist,
|
||||||
|
safeBins: params.safeBins,
|
||||||
|
cwd: params.cwd,
|
||||||
|
skillBins: params.skillBins,
|
||||||
|
autoAllowSkills: params.autoAllowSkills,
|
||||||
|
});
|
||||||
|
allowlistMatches.push(...evaluation.allowlistMatches);
|
||||||
|
if (!evaluation.allowlistSatisfied) {
|
||||||
|
return {
|
||||||
|
analysisOk: true,
|
||||||
|
allowlistSatisfied: false,
|
||||||
|
allowlistMatches,
|
||||||
|
segments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
analysisOk: true,
|
||||||
|
allowlistSatisfied: true,
|
||||||
|
allowlistMatches,
|
||||||
|
segments,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requiresExecApproval(params: {
|
export function requiresExecApproval(params: {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import path from "node:path";
|
|||||||
import {
|
import {
|
||||||
addAllowlistEntry,
|
addAllowlistEntry,
|
||||||
analyzeArgvCommand,
|
analyzeArgvCommand,
|
||||||
analyzeShellCommand,
|
|
||||||
evaluateExecAllowlist,
|
evaluateExecAllowlist,
|
||||||
|
evaluateShellAllowlist,
|
||||||
requiresExecApproval,
|
requiresExecApproval,
|
||||||
normalizeExecApprovals,
|
normalizeExecApprovals,
|
||||||
recordAllowlistUse,
|
recordAllowlistUse,
|
||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
resolveExecApprovalsSocketPath,
|
resolveExecApprovalsSocketPath,
|
||||||
saveExecApprovals,
|
saveExecApprovals,
|
||||||
type ExecApprovalsFile,
|
type ExecApprovalsFile,
|
||||||
|
type ExecAllowlistEntry,
|
||||||
|
type ExecCommandSegment,
|
||||||
} from "../infra/exec-approvals.js";
|
} from "../infra/exec-approvals.js";
|
||||||
import {
|
import {
|
||||||
requestExecHostViaSocket,
|
requestExecHostViaSocket,
|
||||||
@@ -585,13 +587,31 @@ async function handleInvoke(
|
|||||||
const sessionKey = params.sessionKey?.trim() || "node";
|
const sessionKey = params.sessionKey?.trim() || "node";
|
||||||
const runId = params.runId?.trim() || crypto.randomUUID();
|
const runId = params.runId?.trim() || crypto.randomUUID();
|
||||||
const env = sanitizeEnv(params.env ?? undefined);
|
const env = sanitizeEnv(params.env ?? undefined);
|
||||||
const analysis = rawCommand
|
|
||||||
? analyzeShellCommand({ command: rawCommand, cwd: params.cwd ?? undefined, env })
|
|
||||||
: analyzeArgvCommand({ argv, cwd: params.cwd ?? undefined, env });
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
|
const agentExec = agentId ? resolveAgentConfig(cfg, agentId)?.tools?.exec : undefined;
|
||||||
const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
|
const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
|
||||||
const bins = autoAllowSkills ? await skillBins.current() : new Set<string>();
|
const bins = autoAllowSkills ? await skillBins.current() : new Set<string>();
|
||||||
|
let analysisOk = false;
|
||||||
|
let allowlistMatches: ExecAllowlistEntry[] = [];
|
||||||
|
let allowlistSatisfied = false;
|
||||||
|
let segments: ExecCommandSegment[] = [];
|
||||||
|
if (rawCommand) {
|
||||||
|
const allowlistEval = evaluateShellAllowlist({
|
||||||
|
command: rawCommand,
|
||||||
|
allowlist: approvals.allowlist,
|
||||||
|
safeBins,
|
||||||
|
cwd: params.cwd ?? undefined,
|
||||||
|
env,
|
||||||
|
skillBins: bins,
|
||||||
|
autoAllowSkills,
|
||||||
|
});
|
||||||
|
analysisOk = allowlistEval.analysisOk;
|
||||||
|
allowlistMatches = allowlistEval.allowlistMatches;
|
||||||
|
allowlistSatisfied =
|
||||||
|
security === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
|
||||||
|
segments = allowlistEval.segments;
|
||||||
|
} else {
|
||||||
|
const analysis = analyzeArgvCommand({ argv, cwd: params.cwd ?? undefined, env });
|
||||||
const allowlistEval = evaluateExecAllowlist({
|
const allowlistEval = evaluateExecAllowlist({
|
||||||
analysis,
|
analysis,
|
||||||
allowlist: approvals.allowlist,
|
allowlist: approvals.allowlist,
|
||||||
@@ -600,9 +620,12 @@ async function handleInvoke(
|
|||||||
skillBins: bins,
|
skillBins: bins,
|
||||||
autoAllowSkills,
|
autoAllowSkills,
|
||||||
});
|
});
|
||||||
const allowlistMatches = allowlistEval.allowlistMatches;
|
analysisOk = analysis.ok;
|
||||||
const allowlistSatisfied =
|
allowlistMatches = allowlistEval.allowlistMatches;
|
||||||
security === "allowlist" && analysis.ok ? allowlistEval.allowlistSatisfied : false;
|
allowlistSatisfied =
|
||||||
|
security === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
|
||||||
|
segments = analysis.segments;
|
||||||
|
}
|
||||||
|
|
||||||
const useMacAppExec = process.platform === "darwin";
|
const useMacAppExec = process.platform === "darwin";
|
||||||
if (useMacAppExec) {
|
if (useMacAppExec) {
|
||||||
@@ -709,7 +732,7 @@ async function handleInvoke(
|
|||||||
const requiresAsk = requiresExecApproval({
|
const requiresAsk = requiresExecApproval({
|
||||||
ask,
|
ask,
|
||||||
security,
|
security,
|
||||||
analysisOk: analysis.ok,
|
analysisOk,
|
||||||
allowlistSatisfied,
|
allowlistSatisfied,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -737,15 +760,15 @@ async function handleInvoke(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (approvalDecision === "allow-always" && security === "allowlist") {
|
if (approvalDecision === "allow-always" && security === "allowlist") {
|
||||||
if (analysis.ok) {
|
if (analysisOk) {
|
||||||
for (const segment of analysis.segments) {
|
for (const segment of segments) {
|
||||||
const pattern = segment.resolution?.resolvedPath ?? "";
|
const pattern = segment.resolution?.resolvedPath ?? "";
|
||||||
if (pattern) addAllowlistEntry(approvals.file, agentId, pattern);
|
if (pattern) addAllowlistEntry(approvals.file, agentId, pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (security === "allowlist" && (!analysis.ok || !allowlistSatisfied) && !approvedByAsk) {
|
if (security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
|
||||||
await sendNodeEvent(
|
await sendNodeEvent(
|
||||||
client,
|
client,
|
||||||
"exec.denied",
|
"exec.denied",
|
||||||
@@ -774,7 +797,7 @@ async function handleInvoke(
|
|||||||
agentId,
|
agentId,
|
||||||
match,
|
match,
|
||||||
cmdText,
|
cmdText,
|
||||||
analysis.segments[0]?.resolution?.resolvedPath,
|
segments[0]?.resolution?.resolvedPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user