mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(security): bind system.run approvals to exact argv text
This commit is contained in:
@@ -222,6 +222,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.
|
- Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.
|
||||||
- Kimi Coding/Anthropic tools compatibility: normalize `anthropic-messages` tool payloads to OpenAI-style `tools[].function` + compatible `tool_choice` when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.
|
- Kimi Coding/Anthropic tools compatibility: normalize `anthropic-messages` tool payloads to OpenAI-style `tools[].function` + compatible `tool_choice` when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.
|
||||||
- Heartbeat/workspace-path guardrails: append explicit workspace `HEARTBEAT.md` path guidance (and `docs/heartbeat.md` avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.
|
- Heartbeat/workspace-path guardrails: append explicit workspace `HEARTBEAT.md` path guidance (and `docs/heartbeat.md` avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.
|
||||||
|
- Node/system.run approvals: bind approval prompts to the exact executed argv text and show shell payload only as a secondary preview, closing basename-spoofed wrapper approval mismatches. Thanks @tdjackey.
|
||||||
- Subagents/kill-complete announce race: when a late `subagent-complete` lifecycle event arrives after an earlier kill marker, clear stale kill suppression/cleanup flags and re-run announce cleanup so finished runs no longer get silently swallowed. (#37024) Thanks @cmfinlan.
|
- Subagents/kill-complete announce race: when a late `subagent-complete` lifecycle event arrives after an earlier kill marker, clear stale kill suppression/cleanup flags and re-run announce cleanup so finished runs no longer get silently swallowed. (#37024) Thanks @cmfinlan.
|
||||||
- Agents/tool-result cleanup timeout hardening: on embedded runner teardown idle timeouts, clear pending tool-call state without persisting synthetic `missing tool result` entries, preventing timeout cleanups from poisoning follow-up turns; adds regression coverage for timeout clear-vs-flush behavior. (#37081) Thanks @Coyote-Den.
|
- Agents/tool-result cleanup timeout hardening: on embedded runner teardown idle timeouts, clear pending tool-call state without persisting synthetic `missing tool result` entries, preventing timeout cleanups from poisoning follow-up turns; adds regression coverage for timeout clear-vs-flush behavior. (#37081) Thanks @Coyote-Den.
|
||||||
- Agents/openai-completions stream timeout hardening: ensure runtime undici global dispatchers use extended streaming body/header timeouts (including env-proxy dispatcher mode) before embedded runs, reducing forced mid-stream `terminated` failures on long generations; adds regression coverage for dispatcher selection and idempotent reconfiguration. (#9708) Thanks @scottchguard.
|
- Agents/openai-completions stream timeout hardening: ensure runtime undici global dispatchers use extended streaming body/header timeouts (including env-proxy dispatcher mode) before embedded runs, reducing forced mid-stream `terminated` failures on long generations; adds regression coverage for dispatcher selection and idempotent reconfiguration. (#9708) Thanks @scottchguard.
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ describe("nodes-cli coverage", () => {
|
|||||||
expect(invoke?.params?.command).toBe("system.run");
|
expect(invoke?.params?.command).toBe("system.run");
|
||||||
expect(invoke?.params?.params).toEqual({
|
expect(invoke?.params?.params).toEqual({
|
||||||
command: ["echo", "hi"],
|
command: ["echo", "hi"],
|
||||||
rawCommand: null,
|
rawCommand: "echo hi",
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
env: { FOO: "bar" },
|
env: { FOO: "bar" },
|
||||||
timeoutMs: 1200,
|
timeoutMs: 1200,
|
||||||
@@ -190,7 +190,8 @@ describe("nodes-cli coverage", () => {
|
|||||||
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
||||||
argv: ["echo", "hi"],
|
argv: ["echo", "hi"],
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
rawCommand: null,
|
rawCommand: "echo hi",
|
||||||
|
commandPreview: null,
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
});
|
});
|
||||||
@@ -213,7 +214,7 @@ describe("nodes-cli coverage", () => {
|
|||||||
expect(invoke?.params?.command).toBe("system.run");
|
expect(invoke?.params?.command).toBe("system.run");
|
||||||
expect(invoke?.params?.params).toMatchObject({
|
expect(invoke?.params?.params).toMatchObject({
|
||||||
command: ["/bin/sh", "-lc", "echo hi"],
|
command: ["/bin/sh", "-lc", "echo hi"],
|
||||||
rawCommand: "echo hi",
|
rawCommand: '/bin/sh -lc "echo hi"',
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
approved: true,
|
approved: true,
|
||||||
approvalDecision: "allow-once",
|
approvalDecision: "allow-once",
|
||||||
@@ -224,7 +225,8 @@ describe("nodes-cli coverage", () => {
|
|||||||
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
||||||
argv: ["/bin/sh", "-lc", "echo hi"],
|
argv: ["/bin/sh", "-lc", "echo hi"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
rawCommand: "echo hi",
|
rawCommand: '/bin/sh -lc "echo hi"',
|
||||||
|
commandPreview: "echo hi",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ type ExecApprovalContainerParams = {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
commandPreview: string;
|
commandPreview: string;
|
||||||
|
commandSecondaryPreview?: string | null;
|
||||||
metadataLines?: string[];
|
metadataLines?: string[];
|
||||||
actionRow?: Row<Button>;
|
actionRow?: Row<Button>;
|
||||||
footer?: string;
|
footer?: string;
|
||||||
@@ -121,6 +122,11 @@ class ExecApprovalContainer extends DiscordUiContainer {
|
|||||||
}
|
}
|
||||||
components.push(new Separator({ divider: true, spacing: "small" }));
|
components.push(new Separator({ divider: true, spacing: "small" }));
|
||||||
components.push(new TextDisplay(`### Command\n\`\`\`\n${params.commandPreview}\n\`\`\``));
|
components.push(new TextDisplay(`### Command\n\`\`\`\n${params.commandPreview}\n\`\`\``));
|
||||||
|
if (params.commandSecondaryPreview) {
|
||||||
|
components.push(
|
||||||
|
new TextDisplay(`### Shell Preview\n\`\`\`\n${params.commandSecondaryPreview}\n\`\`\``),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (params.metadataLines?.length) {
|
if (params.metadataLines?.length) {
|
||||||
components.push(new TextDisplay(params.metadataLines.join("\n")));
|
components.push(new TextDisplay(params.metadataLines.join("\n")));
|
||||||
}
|
}
|
||||||
@@ -235,6 +241,16 @@ function formatCommandPreview(commandText: string, maxChars: number): string {
|
|||||||
return commandRaw.replace(/`/g, "\u200b`");
|
return commandRaw.replace(/`/g, "\u200b`");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatOptionalCommandPreview(
|
||||||
|
commandText: string | null | undefined,
|
||||||
|
maxChars: number,
|
||||||
|
): string | null {
|
||||||
|
if (!commandText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatCommandPreview(commandText, maxChars);
|
||||||
|
}
|
||||||
|
|
||||||
function createExecApprovalRequestContainer(params: {
|
function createExecApprovalRequestContainer(params: {
|
||||||
request: ExecApprovalRequest;
|
request: ExecApprovalRequest;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
@@ -243,6 +259,10 @@ function createExecApprovalRequestContainer(params: {
|
|||||||
}): ExecApprovalContainer {
|
}): ExecApprovalContainer {
|
||||||
const commandText = params.request.request.command;
|
const commandText = params.request.request.command;
|
||||||
const commandPreview = formatCommandPreview(commandText, 1000);
|
const commandPreview = formatCommandPreview(commandText, 1000);
|
||||||
|
const commandSecondaryPreview = formatOptionalCommandPreview(
|
||||||
|
params.request.request.commandPreview,
|
||||||
|
500,
|
||||||
|
);
|
||||||
const expiresAtSeconds = Math.max(0, Math.floor(params.request.expiresAtMs / 1000));
|
const expiresAtSeconds = Math.max(0, Math.floor(params.request.expiresAtMs / 1000));
|
||||||
|
|
||||||
return new ExecApprovalContainer({
|
return new ExecApprovalContainer({
|
||||||
@@ -251,6 +271,7 @@ function createExecApprovalRequestContainer(params: {
|
|||||||
title: "Exec Approval Required",
|
title: "Exec Approval Required",
|
||||||
description: "A command needs your approval.",
|
description: "A command needs your approval.",
|
||||||
commandPreview,
|
commandPreview,
|
||||||
|
commandSecondaryPreview,
|
||||||
metadataLines: buildExecApprovalMetadataLines(params.request),
|
metadataLines: buildExecApprovalMetadataLines(params.request),
|
||||||
actionRow: params.actionRow,
|
actionRow: params.actionRow,
|
||||||
footer: `Expires <t:${expiresAtSeconds}:R> · ID: ${params.request.id}`,
|
footer: `Expires <t:${expiresAtSeconds}:R> · ID: ${params.request.id}`,
|
||||||
@@ -267,6 +288,10 @@ function createResolvedContainer(params: {
|
|||||||
}): ExecApprovalContainer {
|
}): ExecApprovalContainer {
|
||||||
const commandText = params.request.request.command;
|
const commandText = params.request.request.command;
|
||||||
const commandPreview = formatCommandPreview(commandText, 500);
|
const commandPreview = formatCommandPreview(commandText, 500);
|
||||||
|
const commandSecondaryPreview = formatOptionalCommandPreview(
|
||||||
|
params.request.request.commandPreview,
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
|
||||||
const decisionLabel =
|
const decisionLabel =
|
||||||
params.decision === "allow-once"
|
params.decision === "allow-once"
|
||||||
@@ -288,6 +313,7 @@ function createResolvedContainer(params: {
|
|||||||
title: `Exec Approval: ${decisionLabel}`,
|
title: `Exec Approval: ${decisionLabel}`,
|
||||||
description: params.resolvedBy ? `Resolved by ${params.resolvedBy}` : "Resolved",
|
description: params.resolvedBy ? `Resolved by ${params.resolvedBy}` : "Resolved",
|
||||||
commandPreview,
|
commandPreview,
|
||||||
|
commandSecondaryPreview,
|
||||||
footer: `ID: ${params.request.id}`,
|
footer: `ID: ${params.request.id}`,
|
||||||
accentColor,
|
accentColor,
|
||||||
});
|
});
|
||||||
@@ -300,6 +326,10 @@ function createExpiredContainer(params: {
|
|||||||
}): ExecApprovalContainer {
|
}): ExecApprovalContainer {
|
||||||
const commandText = params.request.request.command;
|
const commandText = params.request.request.command;
|
||||||
const commandPreview = formatCommandPreview(commandText, 500);
|
const commandPreview = formatCommandPreview(commandText, 500);
|
||||||
|
const commandSecondaryPreview = formatOptionalCommandPreview(
|
||||||
|
params.request.request.commandPreview,
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
|
||||||
return new ExecApprovalContainer({
|
return new ExecApprovalContainer({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
@@ -307,6 +337,7 @@ function createExpiredContainer(params: {
|
|||||||
title: "Exec Approval: Expired",
|
title: "Exec Approval: Expired",
|
||||||
description: "This approval request has expired.",
|
description: "This approval request has expired.",
|
||||||
commandPreview,
|
commandPreview,
|
||||||
|
commandSecondaryPreview,
|
||||||
footer: `ID: ${params.request.id}`,
|
footer: `ID: ${params.request.id}`,
|
||||||
accentColor: "#99AAB5",
|
accentColor: "#99AAB5",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -278,7 +278,15 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
const forwarded = result.params as Record<string, unknown>;
|
const forwarded = result.params as Record<string, unknown>;
|
||||||
expect(forwarded.command).toEqual(["/usr/bin/echo", "SAFE"]);
|
expect(forwarded.command).toEqual(["/usr/bin/echo", "SAFE"]);
|
||||||
expect(forwarded.rawCommand).toBe("/usr/bin/echo SAFE");
|
expect(forwarded.rawCommand).toBe("/usr/bin/echo SAFE");
|
||||||
expect(forwarded.systemRunPlan).toEqual(record.request.systemRunPlan);
|
expect(forwarded.systemRunPlan).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
argv: ["/usr/bin/echo", "SAFE"],
|
||||||
|
cwd: "/real/cwd",
|
||||||
|
rawCommand: "/usr/bin/echo SAFE",
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
}),
|
||||||
|
);
|
||||||
expect(forwarded.cwd).toBe("/real/cwd");
|
expect(forwarded.cwd).toBe("/real/cwd");
|
||||||
expect(forwarded.agentId).toBe("main");
|
expect(forwarded.agentId).toBe("main");
|
||||||
expect(forwarded.sessionKey).toBe("agent:main:main");
|
expect(forwarded.sessionKey).toBe("agent:main:main");
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const ExecApprovalRequestParamsSchema = Type.Object(
|
|||||||
argv: Type.Array(Type.String()),
|
argv: Type.Array(Type.String()),
|
||||||
cwd: Type.Union([Type.String(), Type.Null()]),
|
cwd: Type.Union([Type.String(), Type.Null()]),
|
||||||
rawCommand: Type.Union([Type.String(), Type.Null()]),
|
rawCommand: Type.Union([Type.String(), Type.Null()]),
|
||||||
|
commandPreview: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||||
agentId: Type.Union([Type.String(), Type.Null()]),
|
agentId: Type.Union([Type.String(), Type.Null()]),
|
||||||
sessionKey: Type.Union([Type.String(), Type.Null()]),
|
sessionKey: Type.Union([Type.String(), Type.Null()]),
|
||||||
mutableFileOperand: Type.Optional(
|
mutableFileOperand: Type.Optional(
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function createExecApprovalHandlers(
|
|||||||
const effectiveAgentId = approvalContext.agentId;
|
const effectiveAgentId = approvalContext.agentId;
|
||||||
const effectiveSessionKey = approvalContext.sessionKey;
|
const effectiveSessionKey = approvalContext.sessionKey;
|
||||||
const effectiveCommandText = approvalContext.commandText;
|
const effectiveCommandText = approvalContext.commandText;
|
||||||
|
const effectiveCommandPreview = approvalContext.commandPreview;
|
||||||
if (host === "node" && !nodeId) {
|
if (host === "node" && !nodeId) {
|
||||||
respond(
|
respond(
|
||||||
false,
|
false,
|
||||||
@@ -122,6 +123,7 @@ export function createExecApprovalHandlers(
|
|||||||
}
|
}
|
||||||
const request = {
|
const request = {
|
||||||
command: effectiveCommandText,
|
command: effectiveCommandText,
|
||||||
|
commandPreview: effectiveCommandPreview,
|
||||||
commandArgv: effectiveCommandArgv,
|
commandArgv: effectiveCommandArgv,
|
||||||
envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined,
|
envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined,
|
||||||
systemRunBinding: systemRunBinding?.binding ?? null,
|
systemRunBinding: systemRunBinding?.binding ?? null,
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ describe("exec approval handlers", () => {
|
|||||||
argv: ["/usr/bin/echo", "ok"],
|
argv: ["/usr/bin/echo", "ok"],
|
||||||
cwd: "/real/cwd",
|
cwd: "/real/cwd",
|
||||||
rawCommand: "/usr/bin/echo ok",
|
rawCommand: "/usr/bin/echo ok",
|
||||||
|
commandPreview: "echo ok",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "agent:main:main",
|
sessionKey: "agent:main:main",
|
||||||
},
|
},
|
||||||
@@ -596,6 +597,7 @@ describe("exec approval handlers", () => {
|
|||||||
expect(requested).toBeTruthy();
|
expect(requested).toBeTruthy();
|
||||||
const request = (requested?.payload as { request?: Record<string, unknown> })?.request ?? {};
|
const request = (requested?.payload as { request?: Record<string, unknown> })?.request ?? {};
|
||||||
expect(request["command"]).toBe("/usr/bin/echo ok");
|
expect(request["command"]).toBe("/usr/bin/echo ok");
|
||||||
|
expect(request["commandPreview"]).toBe("echo ok");
|
||||||
expect(request["commandArgv"]).toEqual(["/usr/bin/echo", "ok"]);
|
expect(request["commandArgv"]).toEqual(["/usr/bin/echo", "ok"]);
|
||||||
expect(request["cwd"]).toBe("/real/cwd");
|
expect(request["cwd"]).toBe("/real/cwd");
|
||||||
expect(request["agentId"]).toBe("main");
|
expect(request["agentId"]).toBe("main");
|
||||||
@@ -604,11 +606,38 @@ describe("exec approval handlers", () => {
|
|||||||
argv: ["/usr/bin/echo", "ok"],
|
argv: ["/usr/bin/echo", "ok"],
|
||||||
cwd: "/real/cwd",
|
cwd: "/real/cwd",
|
||||||
rawCommand: "/usr/bin/echo ok",
|
rawCommand: "/usr/bin/echo ok",
|
||||||
|
commandPreview: "echo ok",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "agent:main:main",
|
sessionKey: "agent:main:main",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives a command preview from the fallback command for older node plans", async () => {
|
||||||
|
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
||||||
|
await requestExecApproval({
|
||||||
|
handlers,
|
||||||
|
respond,
|
||||||
|
context,
|
||||||
|
params: {
|
||||||
|
timeoutMs: 10,
|
||||||
|
command: "jq --version",
|
||||||
|
commandArgv: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
systemRunPlan: {
|
||||||
|
argv: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
cwd: "/real/cwd",
|
||||||
|
rawCommand: './env sh -c "jq --version"',
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const requested = broadcasts.find((entry) => entry.event === "exec.approval.requested");
|
||||||
|
expect(requested).toBeTruthy();
|
||||||
|
const request = (requested?.payload as { request?: Record<string, unknown> })?.request ?? {};
|
||||||
|
expect(request["command"]).toBe('./env sh -c "jq --version"');
|
||||||
|
expect(request["commandPreview"]).toBe("jq --version");
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts resolve during broadcast", async () => {
|
it("accepts resolve during broadcast", async () => {
|
||||||
const manager = new ExecApprovalManager();
|
const manager = new ExecApprovalManager();
|
||||||
const handlers = createExecApprovalHandlers(manager);
|
const handlers = createExecApprovalHandlers(manager);
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export type SystemRunApprovalPlan = {
|
|||||||
argv: string[];
|
argv: string[];
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
rawCommand: string | null;
|
rawCommand: string | null;
|
||||||
|
commandPreview?: string | null;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
sessionKey: string | null;
|
sessionKey: string | null;
|
||||||
mutableFileOperand?: SystemRunApprovalFileOperand | null;
|
mutableFileOperand?: SystemRunApprovalFileOperand | null;
|
||||||
@@ -60,6 +61,7 @@ export type SystemRunApprovalPlan = {
|
|||||||
|
|
||||||
export type ExecApprovalRequestPayload = {
|
export type ExecApprovalRequestPayload = {
|
||||||
command: string;
|
command: string;
|
||||||
|
commandPreview?: string | null;
|
||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
// Optional UI-safe env key preview for approval prompts.
|
// Optional UI-safe env key preview for approval prompts.
|
||||||
envKeys?: string[];
|
envKeys?: string[];
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprova
|
|||||||
argv,
|
argv,
|
||||||
cwd: normalizeNonEmptyString(candidate.cwd),
|
cwd: normalizeNonEmptyString(candidate.cwd),
|
||||||
rawCommand: normalizeNonEmptyString(candidate.rawCommand),
|
rawCommand: normalizeNonEmptyString(candidate.rawCommand),
|
||||||
|
commandPreview: normalizeNonEmptyString(candidate.commandPreview),
|
||||||
agentId: normalizeNonEmptyString(candidate.agentId),
|
agentId: normalizeNonEmptyString(candidate.agentId),
|
||||||
sessionKey: normalizeNonEmptyString(candidate.sessionKey),
|
sessionKey: normalizeNonEmptyString(candidate.sessionKey),
|
||||||
mutableFileOperand: mutableFileOperand ?? undefined,
|
mutableFileOperand: mutableFileOperand ?? undefined,
|
||||||
|
|||||||
40
src/infra/system-run-approval-context.test.ts
Normal file
40
src/infra/system-run-approval-context.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { resolveSystemRunApprovalRequestContext } from "./system-run-approval-context.js";
|
||||||
|
|
||||||
|
describe("resolveSystemRunApprovalRequestContext", () => {
|
||||||
|
test("uses full approval text and separate preview for node system.run plans", () => {
|
||||||
|
const context = resolveSystemRunApprovalRequestContext({
|
||||||
|
host: "node",
|
||||||
|
command: "jq --version",
|
||||||
|
systemRunPlan: {
|
||||||
|
argv: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
rawCommand: './env sh -c "jq --version"',
|
||||||
|
commandPreview: "jq --version",
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context.commandText).toBe('./env sh -c "jq --version"');
|
||||||
|
expect(context.commandPreview).toBe("jq --version");
|
||||||
|
expect(context.commandArgv).toEqual(["./env", "sh", "-c", "jq --version"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("derives preview from fallback command for older node plans", () => {
|
||||||
|
const context = resolveSystemRunApprovalRequestContext({
|
||||||
|
host: "node",
|
||||||
|
command: "jq --version",
|
||||||
|
systemRunPlan: {
|
||||||
|
argv: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
rawCommand: './env sh -c "jq --version"',
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(context.commandText).toBe('./env sh -c "jq --version"');
|
||||||
|
expect(context.commandPreview).toBe("jq --version");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ type SystemRunApprovalRequestContext = {
|
|||||||
plan: SystemRunApprovalPlan | null;
|
plan: SystemRunApprovalPlan | null;
|
||||||
commandArgv: string[] | undefined;
|
commandArgv: string[] | undefined;
|
||||||
commandText: string;
|
commandText: string;
|
||||||
|
commandPreview: string | null;
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
sessionKey: string | null;
|
sessionKey: string | null;
|
||||||
@@ -37,6 +38,17 @@ function normalizeCommandText(value: unknown): string {
|
|||||||
return typeof value === "string" ? value : "";
|
return typeof value === "string" ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCommandPreview(
|
||||||
|
value: string | null | undefined,
|
||||||
|
authoritative: string,
|
||||||
|
): string | null {
|
||||||
|
const preview = normalizeNonEmptyString(value);
|
||||||
|
if (!preview || preview === authoritative) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return preview;
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayload | null {
|
export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayload | null {
|
||||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -63,10 +75,14 @@ export function resolveSystemRunApprovalRequestContext(params: {
|
|||||||
const plan = host === "node" ? normalizeSystemRunApprovalPlan(params.systemRunPlan) : null;
|
const plan = host === "node" ? normalizeSystemRunApprovalPlan(params.systemRunPlan) : null;
|
||||||
const fallbackArgv = normalizeStringArray(params.commandArgv);
|
const fallbackArgv = normalizeStringArray(params.commandArgv);
|
||||||
const fallbackCommand = normalizeCommandText(params.command);
|
const fallbackCommand = normalizeCommandText(params.command);
|
||||||
|
const commandText = plan ? (plan.rawCommand ?? formatExecCommand(plan.argv)) : fallbackCommand;
|
||||||
return {
|
return {
|
||||||
plan,
|
plan,
|
||||||
commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
|
commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
|
||||||
commandText: plan ? (plan.rawCommand ?? formatExecCommand(plan.argv)) : fallbackCommand,
|
commandText,
|
||||||
|
commandPreview: plan
|
||||||
|
? normalizeCommandPreview(plan.commandPreview ?? fallbackCommand, commandText)
|
||||||
|
: null,
|
||||||
cwd: plan?.cwd ?? normalizeNonEmptyString(params.cwd),
|
cwd: plan?.cwd ?? normalizeNonEmptyString(params.cwd),
|
||||||
agentId: plan?.agentId ?? normalizeNonEmptyString(params.agentId),
|
agentId: plan?.agentId ?? normalizeNonEmptyString(params.agentId),
|
||||||
sessionKey: plan?.sessionKey ?? normalizeNonEmptyString(params.sessionKey),
|
sessionKey: plan?.sessionKey ?? normalizeNonEmptyString(params.sessionKey),
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ describe("system run command helpers", () => {
|
|||||||
rawCommand: "echo hi",
|
rawCommand: "echo hi",
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
expect(res.previewText).toBe("echo hi");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validateSystemRunCommandConsistency rejects shell-only rawCommand for positional-argv carrier wrappers", () => {
|
test("validateSystemRunCommandConsistency rejects shell-only rawCommand for positional-argv carrier wrappers", () => {
|
||||||
@@ -121,6 +125,10 @@ describe("system run command helpers", () => {
|
|||||||
rawCommand: "echo hi",
|
rawCommand: "echo hi",
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
expect(res.previewText).toBe("echo hi");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validateSystemRunCommandConsistency rejects shell-only rawCommand for env assignment prelude", () => {
|
test("validateSystemRunCommandConsistency rejects shell-only rawCommand for env assignment prelude", () => {
|
||||||
@@ -142,6 +150,7 @@ describe("system run command helpers", () => {
|
|||||||
}
|
}
|
||||||
expect(res.shellCommand).toBe("echo hi");
|
expect(res.shellCommand).toBe("echo hi");
|
||||||
expect(res.cmdText).toBe(raw);
|
expect(res.cmdText).toBe(raw);
|
||||||
|
expect(res.previewText).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validateSystemRunCommandConsistency rejects cmd.exe /c trailing-arg smuggling", () => {
|
test("validateSystemRunCommandConsistency rejects cmd.exe /c trailing-arg smuggling", () => {
|
||||||
@@ -180,6 +189,7 @@ describe("system run command helpers", () => {
|
|||||||
expect(res.argv).toEqual(["cmd.exe", "/d", "/s", "/c", "echo", "SAFE&&whoami"]);
|
expect(res.argv).toEqual(["cmd.exe", "/d", "/s", "/c", "echo", "SAFE&&whoami"]);
|
||||||
expect(res.shellCommand).toBe("echo SAFE&&whoami");
|
expect(res.shellCommand).toBe("echo SAFE&&whoami");
|
||||||
expect(res.cmdText).toBe("echo SAFE&&whoami");
|
expect(res.cmdText).toBe("echo SAFE&&whoami");
|
||||||
|
expect(res.previewText).toBe("echo SAFE&&whoami");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolveSystemRunCommand binds cmdText to full argv for shell-wrapper positional-argv carriers", () => {
|
test("resolveSystemRunCommand binds cmdText to full argv for shell-wrapper positional-argv carriers", () => {
|
||||||
@@ -192,6 +202,7 @@ describe("system run command helpers", () => {
|
|||||||
}
|
}
|
||||||
expect(res.shellCommand).toBe('$0 "$1"');
|
expect(res.shellCommand).toBe('$0 "$1"');
|
||||||
expect(res.cmdText).toBe('/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker');
|
expect(res.cmdText).toBe('/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker');
|
||||||
|
expect(res.previewText).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolveSystemRunCommand binds cmdText to full argv when env prelude modifies shell wrapper", () => {
|
test("resolveSystemRunCommand binds cmdText to full argv when env prelude modifies shell wrapper", () => {
|
||||||
@@ -204,5 +215,32 @@ describe("system run command helpers", () => {
|
|||||||
}
|
}
|
||||||
expect(res.shellCommand).toBe("echo hi");
|
expect(res.shellCommand).toBe("echo hi");
|
||||||
expect(res.cmdText).toBe('/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc "echo hi"');
|
expect(res.cmdText).toBe('/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc "echo hi"');
|
||||||
|
expect(res.previewText).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSystemRunCommand keeps wrapper preview separate from approval text", () => {
|
||||||
|
const res = resolveSystemRunCommand({
|
||||||
|
command: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
expect(res.cmdText).toBe("jq --version");
|
||||||
|
expect(res.previewText).toBe("jq --version");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveSystemRunCommand accepts canonical full argv text for wrapper approvals", () => {
|
||||||
|
const res = resolveSystemRunCommand({
|
||||||
|
command: ["./env", "sh", "-c", "jq --version"],
|
||||||
|
rawCommand: './env sh -c "jq --version"',
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
expect(res.cmdText).toBe('./env sh -c "jq --version"');
|
||||||
|
expect(res.previewText).toBe("jq --version");
|
||||||
|
expect(res.shellCommand).toBe("jq --version");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type SystemRunCommandValidation =
|
|||||||
ok: true;
|
ok: true;
|
||||||
shellCommand: string | null;
|
shellCommand: string | null;
|
||||||
cmdText: string;
|
cmdText: string;
|
||||||
|
previewText: string | null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
ok: false;
|
ok: false;
|
||||||
@@ -30,6 +31,7 @@ export type ResolvedSystemRunCommand =
|
|||||||
rawCommand: string | null;
|
rawCommand: string | null;
|
||||||
shellCommand: string | null;
|
shellCommand: string | null;
|
||||||
cmdText: string;
|
cmdText: string;
|
||||||
|
previewText: string | null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
ok: false;
|
ok: false;
|
||||||
@@ -112,35 +114,35 @@ export function validateSystemRunCommandConsistency(params: {
|
|||||||
const envManipulationBeforeShellWrapper =
|
const envManipulationBeforeShellWrapper =
|
||||||
shellWrapperResolution.isWrapper && hasEnvManipulationBeforeShellWrapper(params.argv);
|
shellWrapperResolution.isWrapper && hasEnvManipulationBeforeShellWrapper(params.argv);
|
||||||
const mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv;
|
const mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv;
|
||||||
const inferred =
|
const formattedArgv = formatExecCommand(params.argv);
|
||||||
shellCommand !== null && !mustBindDisplayToFullArgv
|
const legacyShellText =
|
||||||
? shellCommand.trim()
|
shellCommand !== null && !mustBindDisplayToFullArgv ? shellCommand.trim() : null;
|
||||||
: formatExecCommand(params.argv);
|
const previewText = legacyShellText;
|
||||||
|
const cmdText = raw ?? legacyShellText ?? formattedArgv;
|
||||||
|
|
||||||
if (raw && raw !== inferred) {
|
if (raw) {
|
||||||
return {
|
const matchesCanonicalArgv = raw === formattedArgv;
|
||||||
ok: false,
|
const matchesLegacyShellText = legacyShellText !== null && raw === legacyShellText;
|
||||||
message: "INVALID_REQUEST: rawCommand does not match command",
|
if (!matchesCanonicalArgv && !matchesLegacyShellText) {
|
||||||
details: {
|
return {
|
||||||
code: "RAW_COMMAND_MISMATCH",
|
ok: false,
|
||||||
rawCommand: raw,
|
message: "INVALID_REQUEST: rawCommand does not match command",
|
||||||
inferred,
|
details: {
|
||||||
},
|
code: "RAW_COMMAND_MISMATCH",
|
||||||
};
|
rawCommand: raw,
|
||||||
|
inferred: legacyShellText ?? formattedArgv,
|
||||||
|
formattedArgv,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
// Only treat this as a shell command when argv is a recognized shell wrapper.
|
// Only treat this as a shell command when argv is a recognized shell wrapper.
|
||||||
// For direct argv execution and shell wrappers with env prelude modifiers,
|
shellCommand: shellCommand !== null ? shellCommand : null,
|
||||||
// rawCommand is purely display/approval text and must match the formatted argv.
|
cmdText,
|
||||||
shellCommand:
|
previewText,
|
||||||
shellCommand !== null
|
|
||||||
? envManipulationBeforeShellWrapper
|
|
||||||
? shellCommand
|
|
||||||
: (raw ?? shellCommand)
|
|
||||||
: null,
|
|
||||||
cmdText: raw ?? inferred,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +169,7 @@ export function resolveSystemRunCommand(params: {
|
|||||||
rawCommand: null,
|
rawCommand: null,
|
||||||
shellCommand: null,
|
shellCommand: null,
|
||||||
cmdText: "",
|
cmdText: "",
|
||||||
|
previewText: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,5 +192,6 @@ export function resolveSystemRunCommand(params: {
|
|||||||
rawCommand: raw,
|
rawCommand: raw,
|
||||||
shellCommand: validation.shellCommand,
|
shellCommand: validation.shellCommand,
|
||||||
cmdText: validation.cmdText,
|
cmdText: validation.cmdText,
|
||||||
|
previewText: validation.previewText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type HardeningCase = {
|
|||||||
expectedArgvChanged?: boolean;
|
expectedArgvChanged?: boolean;
|
||||||
expectedCmdText?: string;
|
expectedCmdText?: string;
|
||||||
checkRawCommandMatchesArgv?: boolean;
|
checkRawCommandMatchesArgv?: boolean;
|
||||||
|
expectedCommandPreview?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ScriptOperandFixture = {
|
type ScriptOperandFixture = {
|
||||||
@@ -101,6 +102,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
argv: ["env", "sh", "-c", "echo SAFE"],
|
argv: ["env", "sh", "-c", "echo SAFE"],
|
||||||
expectedArgv: () => ["env", "sh", "-c", "echo SAFE"],
|
expectedArgv: () => ["env", "sh", "-c", "echo SAFE"],
|
||||||
expectedCmdText: "echo SAFE",
|
expectedCmdText: "echo SAFE",
|
||||||
|
expectedCommandPreview: "echo SAFE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "preserves dispatch-wrapper argv during approval hardening",
|
name: "preserves dispatch-wrapper argv during approval hardening",
|
||||||
@@ -135,6 +137,16 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
withPathToken: true,
|
withPathToken: true,
|
||||||
expectedArgv: ({ pathToken }) => [pathToken!.expected, "hello"],
|
expectedArgv: ({ pathToken }) => [pathToken!.expected, "hello"],
|
||||||
checkRawCommandMatchesArgv: true,
|
checkRawCommandMatchesArgv: true,
|
||||||
|
expectedCommandPreview: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stores full approval text and preview for path-qualified env wrappers",
|
||||||
|
mode: "build-plan",
|
||||||
|
argv: ["./env", "sh", "-c", "echo SAFE"],
|
||||||
|
expectedArgv: () => ["./env", "sh", "-c", "echo SAFE"],
|
||||||
|
expectedCmdText: "echo SAFE",
|
||||||
|
checkRawCommandMatchesArgv: true,
|
||||||
|
expectedCommandPreview: "echo SAFE",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -168,6 +180,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
if (testCase.checkRawCommandMatchesArgv) {
|
if (testCase.checkRawCommandMatchesArgv) {
|
||||||
expect(prepared.plan.rawCommand).toBe(formatExecCommand(prepared.plan.argv));
|
expect(prepared.plan.rawCommand).toBe(formatExecCommand(prepared.plan.argv));
|
||||||
}
|
}
|
||||||
|
if ("expectedCommandPreview" in testCase) {
|
||||||
|
expect(prepared.plan.commandPreview ?? null).toBe(testCase.expectedCommandPreview);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -650,9 +650,11 @@ export function buildSystemRunApprovalPlan(params: {
|
|||||||
if (!hardening.ok) {
|
if (!hardening.ok) {
|
||||||
return { ok: false, message: hardening.message };
|
return { ok: false, message: hardening.message };
|
||||||
}
|
}
|
||||||
const rawCommand = hardening.argvChanged
|
const rawCommand = formatExecCommand(hardening.argv) || null;
|
||||||
? formatExecCommand(hardening.argv) || null
|
const commandPreview =
|
||||||
: command.cmdText.trim() || null;
|
command.previewText?.trim() && command.previewText.trim() !== rawCommand
|
||||||
|
? command.previewText.trim()
|
||||||
|
: null;
|
||||||
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
|
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
|
||||||
argv: hardening.argv,
|
argv: hardening.argv,
|
||||||
cwd: hardening.cwd,
|
cwd: hardening.cwd,
|
||||||
@@ -666,10 +668,11 @@ export function buildSystemRunApprovalPlan(params: {
|
|||||||
argv: hardening.argv,
|
argv: hardening.argv,
|
||||||
cwd: hardening.cwd ?? null,
|
cwd: hardening.cwd ?? null,
|
||||||
rawCommand,
|
rawCommand,
|
||||||
|
commandPreview,
|
||||||
agentId: normalizeString(params.agentId),
|
agentId: normalizeString(params.agentId),
|
||||||
sessionKey: normalizeString(params.sessionKey),
|
sessionKey: normalizeString(params.sessionKey),
|
||||||
mutableFileOperand: mutableFileOperand.snapshot ?? undefined,
|
mutableFileOperand: mutableFileOperand.snapshot ?? undefined,
|
||||||
},
|
},
|
||||||
cmdText: rawCommand ?? formatExecCommand(hardening.argv),
|
cmdText: commandPreview ?? rawCommand ?? formatExecCommand(hardening.argv),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { formatExecCommand } from "../infra/system-run-command.js";
|
||||||
|
|
||||||
type SystemRunPrepareInput = {
|
type SystemRunPrepareInput = {
|
||||||
command?: unknown;
|
command?: unknown;
|
||||||
rawCommand?: unknown;
|
rawCommand?: unknown;
|
||||||
@@ -12,13 +14,16 @@ export function buildSystemRunPreparePayload(params: SystemRunPrepareInput) {
|
|||||||
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
|
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
|
||||||
? params.rawCommand
|
? params.rawCommand
|
||||||
: null;
|
: null;
|
||||||
|
const formattedArgv = formatExecCommand(argv) || null;
|
||||||
|
const commandPreview = rawCommand && rawCommand !== formattedArgv ? rawCommand : null;
|
||||||
return {
|
return {
|
||||||
payload: {
|
payload: {
|
||||||
cmdText: rawCommand ?? argv.join(" "),
|
cmdText: rawCommand ?? argv.join(" "),
|
||||||
plan: {
|
plan: {
|
||||||
argv,
|
argv,
|
||||||
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
||||||
rawCommand,
|
rawCommand: formattedArgv,
|
||||||
|
commandPreview,
|
||||||
agentId: typeof params.agentId === "string" ? params.agentId : null,
|
agentId: typeof params.agentId === "string" ? params.agentId : null,
|
||||||
sessionKey: typeof params.sessionKey === "string" ? params.sessionKey : null,
|
sessionKey: typeof params.sessionKey === "string" ? params.sessionKey : null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,6 +53,15 @@
|
|||||||
"displayCommand": "echo hi"
|
"displayCommand": "echo hi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "env wrapper accepts canonical full argv raw command",
|
||||||
|
"command": ["/usr/bin/env", "bash", "-lc", "echo hi"],
|
||||||
|
"rawCommand": "/usr/bin/env bash -lc \"echo hi\"",
|
||||||
|
"expected": {
|
||||||
|
"valid": true,
|
||||||
|
"displayCommand": "/usr/bin/env bash -lc \"echo hi\""
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "env assignment prelude requires full argv display binding",
|
"name": "env assignment prelude requires full argv display binding",
|
||||||
"command": ["/usr/bin/env", "BASH_ENV=/tmp/payload.sh", "bash", "-lc", "echo hi"],
|
"command": ["/usr/bin/env", "BASH_ENV=/tmp/payload.sh", "bash", "-lc", "echo hi"],
|
||||||
|
|||||||
Reference in New Issue
Block a user