refactor(security): simplify system.run approval model

This commit is contained in:
Peter Steinberger
2026-03-11 01:42:47 +00:00
parent 5716e52417
commit 68c674d37c
32 changed files with 332 additions and 207 deletions

View File

@@ -7,7 +7,7 @@ import { callGatewayTool } from "./tools/gateway.js";
export type RequestExecApprovalDecisionParams = { export type RequestExecApprovalDecisionParams = {
id: string; id: string;
command: string; command?: string;
commandArgv?: string[]; commandArgv?: string[];
systemRunPlan?: SystemRunApprovalPlan; systemRunPlan?: SystemRunApprovalPlan;
env?: Record<string, string>; env?: Record<string, string>;
@@ -35,8 +35,8 @@ function buildExecApprovalRequestToolParams(
): ExecApprovalRequestToolParams { ): ExecApprovalRequestToolParams {
return { return {
id: params.id, id: params.id,
command: params.command, ...(params.command ? { command: params.command } : {}),
commandArgv: params.commandArgv, ...(params.commandArgv ? { commandArgv: params.commandArgv } : {}),
systemRunPlan: params.systemRunPlan, systemRunPlan: params.systemRunPlan,
env: params.env, env: params.env,
cwd: params.cwd, cwd: params.cwd,
@@ -150,7 +150,7 @@ export async function requestExecApprovalDecision(
type HostExecApprovalParams = { type HostExecApprovalParams = {
approvalId: string; approvalId: string;
command: string; command?: string;
commandArgv?: string[]; commandArgv?: string[];
systemRunPlan?: SystemRunApprovalPlan; systemRunPlan?: SystemRunApprovalPlan;
env?: Record<string, string>; env?: Record<string, string>;

View File

@@ -125,7 +125,7 @@ export async function executeNodeHostCommand(
throw new Error("invalid system.run.prepare response"); throw new Error("invalid system.run.prepare response");
} }
const runArgv = prepared.plan.argv; const runArgv = prepared.plan.argv;
const runRawCommand = prepared.plan.rawCommand ?? prepared.cmdText; const runRawCommand = prepared.plan.commandText;
const runCwd = prepared.plan.cwd ?? params.workdir; const runCwd = prepared.plan.cwd ?? params.workdir;
const runAgentId = prepared.plan.agentId ?? params.agentId; const runAgentId = prepared.plan.agentId ?? params.agentId;
const runSessionKey = prepared.plan.sessionKey ?? params.sessionKey; const runSessionKey = prepared.plan.sessionKey ?? params.sessionKey;
@@ -238,8 +238,6 @@ export async function executeNodeHostCommand(
// Register first so the returned approval ID is actionable immediately. // Register first so the returned approval ID is actionable immediately.
const registration = await registerExecApprovalRequestForHostOrThrow({ const registration = await registerExecApprovalRequestForHostOrThrow({
approvalId, approvalId,
command: prepared.cmdText,
commandArgv: prepared.plan.argv,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
env: nodeEnv, env: nodeEnv,
workdir: runCwd, workdir: runCwd,
@@ -391,7 +389,7 @@ export async function executeNodeHostCommand(
warningText, warningText,
approvalSlug, approvalSlug,
approvalId, approvalId,
command: prepared.cmdText, command: prepared.plan.commandText,
cwd: runCwd, cwd: runCwd,
host: "node", host: "node",
nodeId, nodeId,

View File

@@ -135,11 +135,10 @@ function setupNodeInvokeMock(params: {
function createSystemRunPreparePayload(cwd: string | null) { function createSystemRunPreparePayload(cwd: string | null) {
return { return {
payload: { payload: {
cmdText: "echo hi",
plan: { plan: {
argv: ["echo", "hi"], argv: ["echo", "hi"],
cwd, cwd,
rawCommand: "echo hi", commandText: "echo hi",
agentId: null, agentId: null,
sessionKey: null, sessionKey: null,
}, },
@@ -662,10 +661,9 @@ describe("nodes run", () => {
onApprovalRequest: (approvalParams) => { onApprovalRequest: (approvalParams) => {
expect(approvalParams).toMatchObject({ expect(approvalParams).toMatchObject({
id: expect.any(String), id: expect.any(String),
command: "echo hi",
commandArgv: ["echo", "hi"],
systemRunPlan: expect.objectContaining({ systemRunPlan: expect.objectContaining({
argv: ["echo", "hi"], argv: ["echo", "hi"],
commandText: "echo hi",
}), }),
nodeId: NODE_ID, nodeId: NODE_ID,
host: "node", host: "node",

View File

@@ -97,11 +97,11 @@ describe("createNodesTool screen_record duration guardrails", () => {
if (payload?.command === "system.run.prepare") { if (payload?.command === "system.run.prepare") {
return { return {
payload: { payload: {
cmdText: "echo hi",
plan: { plan: {
argv: ["bash", "-lc", "echo hi"], argv: ["bash", "-lc", "echo hi"],
cwd: null, cwd: null,
rawCommand: null, commandText: 'bash -lc "echo hi"',
commandPreview: "echo hi",
agentId: null, agentId: null,
sessionKey: null, sessionKey: null,
}, },

View File

@@ -664,7 +664,7 @@ export function createNodesTool(options?: {
} }
const runParams = { const runParams = {
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand ?? prepared.cmdText, rawCommand: prepared.plan.commandText,
cwd: prepared.plan.cwd ?? cwd, cwd: prepared.plan.cwd ?? cwd,
env, env,
timeoutMs: commandTimeoutMs, timeoutMs: commandTimeoutMs,
@@ -699,8 +699,6 @@ export function createNodesTool(options?: {
{ ...gatewayOpts, timeoutMs: APPROVAL_TIMEOUT_MS + 5_000 }, { ...gatewayOpts, timeoutMs: APPROVAL_TIMEOUT_MS + 5_000 },
{ {
id: approvalId, id: approvalId,
command: prepared.cmdText,
commandArgv: prepared.plan.argv,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
cwd: prepared.plan.cwd ?? cwd, cwd: prepared.plan.cwd ?? cwd,
nodeId, nodeId,

View File

@@ -36,17 +36,16 @@ const renderGatewayPortHealthDiagnostics = vi.fn(() => ["diag: unhealthy port"])
const renderRestartDiagnostics = vi.fn(() => ["diag: unhealthy runtime"]); const renderRestartDiagnostics = vi.fn(() => ["diag: unhealthy runtime"]);
const resolveGatewayPort = vi.fn(() => 18789); const resolveGatewayPort = vi.fn(() => 18789);
const findGatewayPidsOnPortSync = vi.fn<(port: number) => number[]>(() => []); const findGatewayPidsOnPortSync = vi.fn<(port: number) => number[]>(() => []);
const probeGateway = const probeGateway = vi.fn<
vi.fn< (opts: {
(opts: { url: string;
url: string; auth?: { token?: string; password?: string };
auth?: { token?: string; password?: string }; timeoutMs: number;
timeoutMs: number; }) => Promise<{
}) => Promise<{ ok: boolean;
ok: boolean; configSnapshot: unknown;
configSnapshot: unknown; }>
}> >();
>();
const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(() => true); const isRestartEnabled = vi.fn<(config?: { commands?: unknown }) => boolean>(() => true);
const loadConfig = vi.fn(() => ({})); const loadConfig = vi.fn(() => ({}));

View File

@@ -186,11 +186,10 @@ describe("nodes-cli coverage", () => {
}); });
expect(invoke?.params?.timeoutMs).toBe(5000); expect(invoke?.params?.timeoutMs).toBe(5000);
const approval = getApprovalRequestCall(); const approval = getApprovalRequestCall();
expect(approval?.params?.["commandArgv"]).toEqual(["echo", "hi"]);
expect(approval?.params?.["systemRunPlan"]).toEqual({ expect(approval?.params?.["systemRunPlan"]).toEqual({
argv: ["echo", "hi"], argv: ["echo", "hi"],
cwd: "/tmp", cwd: "/tmp",
rawCommand: "echo hi", commandText: "echo hi",
commandPreview: null, commandPreview: null,
agentId: "main", agentId: "main",
sessionKey: null, sessionKey: null,
@@ -221,11 +220,10 @@ describe("nodes-cli coverage", () => {
runId: expect.any(String), runId: expect.any(String),
}); });
const approval = getApprovalRequestCall(); const approval = getApprovalRequestCall();
expect(approval?.params?.["commandArgv"]).toEqual(["/bin/sh", "-lc", "echo hi"]);
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: '/bin/sh -lc "echo hi"', commandText: '/bin/sh -lc "echo hi"',
commandPreview: "echo hi", commandPreview: "echo hi",
agentId: "main", agentId: "main",
sessionKey: null, sessionKey: null,

View File

@@ -189,7 +189,6 @@ async function maybeRequestNodesRunApproval(params: {
opts: NodesRunOpts; opts: NodesRunOpts;
nodeId: string; nodeId: string;
agentId: string | undefined; agentId: string | undefined;
preparedCmdText: string;
approvalPlan: ReturnType<typeof requirePreparedRunPayload>["plan"]; approvalPlan: ReturnType<typeof requirePreparedRunPayload>["plan"];
hostSecurity: ExecSecurity; hostSecurity: ExecSecurity;
hostAsk: ExecAsk; hostAsk: ExecAsk;
@@ -215,8 +214,6 @@ async function maybeRequestNodesRunApproval(params: {
params.opts, params.opts,
{ {
id: approvalId, id: approvalId,
command: params.preparedCmdText,
commandArgv: params.approvalPlan.argv,
systemRunPlan: params.approvalPlan, systemRunPlan: params.approvalPlan,
cwd: params.approvalPlan.cwd, cwd: params.approvalPlan.cwd,
nodeId: params.nodeId, nodeId: params.nodeId,
@@ -272,7 +269,7 @@ function buildSystemRunInvokeParams(params: {
command: "system.run", command: "system.run",
params: { params: {
command: params.approvalPlan.argv, command: params.approvalPlan.argv,
rawCommand: params.approvalPlan.rawCommand, rawCommand: params.approvalPlan.commandText,
cwd: params.approvalPlan.cwd, cwd: params.approvalPlan.cwd,
env: params.nodeEnv, env: params.nodeEnv,
timeoutMs: params.timeoutMs, timeoutMs: params.timeoutMs,
@@ -403,7 +400,6 @@ export function registerNodesInvokeCommands(nodes: Command) {
opts, opts,
nodeId, nodeId,
agentId, agentId,
preparedCmdText: preparedContext.prepared.cmdText,
approvalPlan, approvalPlan,
hostSecurity: approvals.hostSecurity, hostSecurity: approvals.hostSecurity,
hostAsk: approvals.hostAsk, hostAsk: approvals.hostAsk,

View File

@@ -16,6 +16,7 @@ import type { DiscordExecApprovalConfig } from "../../config/types.discord.js";
import { GatewayClient } from "../../gateway/client.js"; import { GatewayClient } from "../../gateway/client.js";
import { createOperatorApprovalsGatewayClient } from "../../gateway/operator-approvals-client.js"; import { createOperatorApprovalsGatewayClient } from "../../gateway/operator-approvals-client.js";
import type { EventFrame } from "../../gateway/protocol/index.js"; import type { EventFrame } from "../../gateway/protocol/index.js";
import { resolveExecApprovalCommandDisplay } from "../../infra/exec-approval-command-display.js";
import { getExecApprovalApproverDmNoticeText } from "../../infra/exec-approval-reply.js"; import { getExecApprovalApproverDmNoticeText } from "../../infra/exec-approval-reply.js";
import type { import type {
ExecApprovalDecision, ExecApprovalDecision,
@@ -257,12 +258,11 @@ function createExecApprovalRequestContainer(params: {
accountId: string; accountId: string;
actionRow?: Row<Button>; actionRow?: Row<Button>;
}): ExecApprovalContainer { }): ExecApprovalContainer {
const commandText = params.request.request.command; const { commandText, commandPreview: secondaryPreview } = resolveExecApprovalCommandDisplay(
const commandPreview = formatCommandPreview(commandText, 1000); params.request.request,
const commandSecondaryPreview = formatOptionalCommandPreview(
params.request.request.commandPreview,
500,
); );
const commandPreview = formatCommandPreview(commandText, 1000);
const commandSecondaryPreview = formatOptionalCommandPreview(secondaryPreview, 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({
@@ -286,12 +286,11 @@ function createResolvedContainer(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
accountId: string; accountId: string;
}): ExecApprovalContainer { }): ExecApprovalContainer {
const commandText = params.request.request.command; const { commandText, commandPreview: secondaryPreview } = resolveExecApprovalCommandDisplay(
const commandPreview = formatCommandPreview(commandText, 500); params.request.request,
const commandSecondaryPreview = formatOptionalCommandPreview(
params.request.request.commandPreview,
300,
); );
const commandPreview = formatCommandPreview(commandText, 500);
const commandSecondaryPreview = formatOptionalCommandPreview(secondaryPreview, 300);
const decisionLabel = const decisionLabel =
params.decision === "allow-once" params.decision === "allow-once"
@@ -324,12 +323,11 @@ function createExpiredContainer(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
accountId: string; accountId: string;
}): ExecApprovalContainer { }): ExecApprovalContainer {
const commandText = params.request.request.command; const { commandText, commandPreview: secondaryPreview } = resolveExecApprovalCommandDisplay(
const commandPreview = formatCommandPreview(commandText, 500); params.request.request,
const commandSecondaryPreview = formatOptionalCommandPreview(
params.request.request.commandPreview,
300,
); );
const commandPreview = formatCommandPreview(commandText, 500);
const commandSecondaryPreview = formatOptionalCommandPreview(secondaryPreview, 300);
return new ExecApprovalContainer({ return new ExecApprovalContainer({
cfg: params.cfg, cfg: params.cfg,

View File

@@ -245,7 +245,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
record.request.systemRunPlan = { record.request.systemRunPlan = {
argv: ["/usr/bin/echo", "SAFE"], argv: ["/usr/bin/echo", "SAFE"],
cwd: "/real/cwd", cwd: "/real/cwd",
rawCommand: "/usr/bin/echo SAFE", commandText: "/usr/bin/echo SAFE",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
}; };
@@ -282,7 +282,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
expect.objectContaining({ expect.objectContaining({
argv: ["/usr/bin/echo", "SAFE"], argv: ["/usr/bin/echo", "SAFE"],
cwd: "/real/cwd", cwd: "/real/cwd",
rawCommand: "/usr/bin/echo SAFE", commandText: "/usr/bin/echo SAFE",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
}), }),

View File

@@ -1,5 +1,5 @@
import { resolveSystemRunApprovalRuntimeContext } from "../infra/system-run-approval-context.js"; import { resolveSystemRunApprovalRuntimeContext } from "../infra/system-run-approval-context.js";
import { resolveSystemRunCommand } from "../infra/system-run-command.js"; import { resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
import type { ExecApprovalRecord } from "./exec-approval-manager.js"; import type { ExecApprovalRecord } from "./exec-approval-manager.js";
import { import {
systemRunApprovalGuardError, systemRunApprovalGuardError,
@@ -117,7 +117,7 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
const next: Record<string, unknown> = pickSystemRunParams(obj); const next: Record<string, unknown> = pickSystemRunParams(obj);
if (!wantsApprovalOverride) { if (!wantsApprovalOverride) {
const cmdTextResolution = resolveSystemRunCommand({ const cmdTextResolution = resolveSystemRunCommandRequest({
command: p.command, command: p.command,
rawCommand: p.rawCommand, rawCommand: p.rawCommand,
}); });
@@ -230,8 +230,8 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
if (runtimeContext.plan) { if (runtimeContext.plan) {
next.command = [...runtimeContext.plan.argv]; next.command = [...runtimeContext.plan.argv];
next.systemRunPlan = runtimeContext.plan; next.systemRunPlan = runtimeContext.plan;
if (runtimeContext.rawCommand) { if (runtimeContext.commandText) {
next.rawCommand = runtimeContext.rawCommand; next.rawCommand = runtimeContext.commandText;
} else { } else {
delete next.rawCommand; delete next.rawCommand;
} }

View File

@@ -88,14 +88,14 @@ export const ExecApprovalsNodeSetParamsSchema = Type.Object(
export const ExecApprovalRequestParamsSchema = Type.Object( export const ExecApprovalRequestParamsSchema = Type.Object(
{ {
id: Type.Optional(NonEmptyString), id: Type.Optional(NonEmptyString),
command: NonEmptyString, command: Type.Optional(NonEmptyString),
commandArgv: Type.Optional(Type.Array(Type.String())), commandArgv: Type.Optional(Type.Array(Type.String())),
systemRunPlan: Type.Optional( systemRunPlan: Type.Optional(
Type.Object( 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()]), commandText: Type.String(),
commandPreview: Type.Optional(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()]),

View File

@@ -75,7 +75,6 @@ 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,
@@ -92,6 +91,10 @@ export function createExecApprovalHandlers(
); );
return; return;
} }
if (!effectiveCommandText) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "command is required"));
return;
}
if ( if (
host === "node" && host === "node" &&
(!Array.isArray(effectiveCommandArgv) || effectiveCommandArgv.length === 0) (!Array.isArray(effectiveCommandArgv) || effectiveCommandArgv.length === 0)
@@ -123,8 +126,8 @@ export function createExecApprovalHandlers(
} }
const request = { const request = {
command: effectiveCommandText, command: effectiveCommandText,
commandPreview: effectiveCommandPreview, commandPreview: host === "node" ? undefined : approvalContext.commandPreview,
commandArgv: effectiveCommandArgv, commandArgv: host === "node" ? undefined : effectiveCommandArgv,
envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined, envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined,
systemRunBinding: systemRunBinding?.binding ?? null, systemRunBinding: systemRunBinding?.binding ?? null,
systemRunPlan: approvalContext.plan, systemRunPlan: approvalContext.plan,

View File

@@ -305,7 +305,7 @@ describe("exec approval handlers", () => {
systemRunPlan: { systemRunPlan: {
argv: ["/usr/bin/echo", "ok"], argv: ["/usr/bin/echo", "ok"],
cwd: "/tmp", cwd: "/tmp",
rawCommand: "/usr/bin/echo ok", commandText: "/usr/bin/echo ok",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
}, },
@@ -358,7 +358,7 @@ describe("exec approval handlers", () => {
requestParams.systemRunPlan = { requestParams.systemRunPlan = {
argv: commandArgv, argv: commandArgv,
cwd: cwdValue, cwd: cwdValue,
rawCommand: commandText, commandText: commandText ?? commandArgv.join(" "),
agentId: agentId:
typeof (requestParams as { agentId?: unknown }).agentId === "string" typeof (requestParams as { agentId?: unknown }).agentId === "string"
? ((requestParams as { agentId: string }).agentId ?? null) ? ((requestParams as { agentId: string }).agentId ?? null)
@@ -586,7 +586,7 @@ describe("exec approval handlers", () => {
systemRunPlan: { systemRunPlan: {
argv: ["/usr/bin/echo", "ok"], argv: ["/usr/bin/echo", "ok"],
cwd: "/real/cwd", cwd: "/real/cwd",
rawCommand: "/usr/bin/echo ok", commandText: "/usr/bin/echo ok",
commandPreview: "echo ok", commandPreview: "echo ok",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
@@ -597,15 +597,15 @@ 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["commandPreview"]).toBeUndefined();
expect(request["commandArgv"]).toEqual(["/usr/bin/echo", "ok"]); expect(request["commandArgv"]).toBeUndefined();
expect(request["cwd"]).toBe("/real/cwd"); expect(request["cwd"]).toBe("/real/cwd");
expect(request["agentId"]).toBe("main"); expect(request["agentId"]).toBe("main");
expect(request["sessionKey"]).toBe("agent:main:main"); expect(request["sessionKey"]).toBe("agent:main:main");
expect(request["systemRunPlan"]).toEqual({ expect(request["systemRunPlan"]).toEqual({
argv: ["/usr/bin/echo", "ok"], argv: ["/usr/bin/echo", "ok"],
cwd: "/real/cwd", cwd: "/real/cwd",
rawCommand: "/usr/bin/echo ok", commandText: "/usr/bin/echo ok",
commandPreview: "echo ok", commandPreview: "echo ok",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
@@ -625,7 +625,7 @@ describe("exec approval handlers", () => {
systemRunPlan: { systemRunPlan: {
argv: ["./env", "sh", "-c", "jq --version"], argv: ["./env", "sh", "-c", "jq --version"],
cwd: "/real/cwd", cwd: "/real/cwd",
rawCommand: './env sh -c "jq --version"', commandText: './env sh -c "jq --version"',
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",
}, },
@@ -635,7 +635,10 @@ 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('./env sh -c "jq --version"'); expect(request["command"]).toBe('./env sh -c "jq --version"');
expect(request["commandPreview"]).toBe("jq --version"); expect(request["commandPreview"]).toBeUndefined();
expect((request["systemRunPlan"] as { commandPreview?: string }).commandPreview).toBe(
"jq --version",
);
}); });
it("accepts resolve during broadcast", async () => { it("accepts resolve during broadcast", async () => {

View File

@@ -0,0 +1,28 @@
import type { ExecApprovalRequestPayload } from "./exec-approvals.js";
function normalizePreview(commandText: string, commandPreview?: string | null): string | null {
const preview = commandPreview?.trim() ?? "";
if (!preview || preview === commandText) {
return null;
}
return preview;
}
export function resolveExecApprovalCommandDisplay(request: ExecApprovalRequestPayload): {
commandText: string;
commandPreview: string | null;
} {
if (request.host === "node" && request.systemRunPlan) {
return {
commandText: request.systemRunPlan.commandText,
commandPreview: normalizePreview(
request.systemRunPlan.commandText,
request.systemRunPlan.commandPreview,
),
};
}
return {
commandText: request.command,
commandPreview: normalizePreview(request.command, request.commandPreview),
};
}

View File

@@ -16,6 +16,7 @@ import {
normalizeMessageChannel, normalizeMessageChannel,
type DeliverableMessageChannel, type DeliverableMessageChannel,
} from "../utils/message-channel.js"; } from "../utils/message-channel.js";
import { resolveExecApprovalCommandDisplay } from "./exec-approval-command-display.js";
import { buildExecApprovalPendingReplyPayload } from "./exec-approval-reply.js"; import { buildExecApprovalPendingReplyPayload } from "./exec-approval-reply.js";
import type { import type {
ExecApprovalDecision, ExecApprovalDecision,
@@ -211,7 +212,9 @@ function formatApprovalCommand(command: string): { inline: boolean; text: string
function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) { function buildRequestMessage(request: ExecApprovalRequest, nowMs: number) {
const lines: string[] = ["🔒 Exec approval required", `ID: ${request.id}`]; const lines: string[] = ["🔒 Exec approval required", `ID: ${request.id}`];
const command = formatApprovalCommand(request.request.command); const command = formatApprovalCommand(
resolveExecApprovalCommandDisplay(request.request).commandText,
);
if (command.inline) { if (command.inline) {
lines.push(`Command: ${command.text}`); lines.push(`Command: ${command.text}`);
} else { } else {
@@ -375,7 +378,7 @@ function buildRequestPayloadForTarget(
approvalId: request.id, approvalId: request.id,
approvalSlug: request.id.slice(0, 8), approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id, approvalCommandId: request.id,
command: request.request.command, command: resolveExecApprovalCommandDisplay(request.request).commandText,
cwd: request.request.cwd ?? undefined, cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway", host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined, nodeId: request.request.nodeId ?? undefined,

View File

@@ -52,7 +52,7 @@ export type SystemRunApprovalFileOperand = {
export type SystemRunApprovalPlan = { export type SystemRunApprovalPlan = {
argv: string[]; argv: string[];
cwd: string | null; cwd: string | null;
rawCommand: string | null; commandText: string;
commandPreview?: string | null; commandPreview?: string | null;
agentId: string | null; agentId: string | null;
sessionKey: string | null; sessionKey: string | null;

View File

@@ -50,10 +50,15 @@ export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprova
if (candidate.mutableFileOperand !== undefined && mutableFileOperand === null) { if (candidate.mutableFileOperand !== undefined && mutableFileOperand === null) {
return null; return null;
} }
const commandText =
normalizeNonEmptyString(candidate.commandText) ?? normalizeNonEmptyString(candidate.rawCommand);
if (!commandText) {
return null;
}
return { return {
argv, argv,
cwd: normalizeNonEmptyString(candidate.cwd), cwd: normalizeNonEmptyString(candidate.cwd),
rawCommand: normalizeNonEmptyString(candidate.rawCommand), commandText,
commandPreview: normalizeNonEmptyString(candidate.commandPreview), commandPreview: normalizeNonEmptyString(candidate.commandPreview),
agentId: normalizeNonEmptyString(candidate.agentId), agentId: normalizeNonEmptyString(candidate.agentId),
sessionKey: normalizeNonEmptyString(candidate.sessionKey), sessionKey: normalizeNonEmptyString(candidate.sessionKey),

View File

@@ -9,7 +9,7 @@ describe("resolveSystemRunApprovalRequestContext", () => {
systemRunPlan: { systemRunPlan: {
argv: ["./env", "sh", "-c", "jq --version"], argv: ["./env", "sh", "-c", "jq --version"],
cwd: "/tmp", cwd: "/tmp",
rawCommand: './env sh -c "jq --version"', commandText: './env sh -c "jq --version"',
commandPreview: "jq --version", commandPreview: "jq --version",
agentId: "main", agentId: "main",
sessionKey: "agent:main:main", sessionKey: "agent:main:main",

View File

@@ -1,10 +1,9 @@
import type { SystemRunApprovalPlan } from "./exec-approvals.js"; import type { SystemRunApprovalPlan } from "./exec-approvals.js";
import { normalizeSystemRunApprovalPlan } from "./system-run-approval-binding.js"; import { normalizeSystemRunApprovalPlan } from "./system-run-approval-binding.js";
import { formatExecCommand, resolveSystemRunCommand } from "./system-run-command.js"; import { formatExecCommand, resolveSystemRunCommandRequest } from "./system-run-command.js";
import { normalizeNonEmptyString, normalizeStringArray } from "./system-run-normalize.js"; import { normalizeNonEmptyString, normalizeStringArray } from "./system-run-normalize.js";
type PreparedRunPayload = { type PreparedRunPayload = {
cmdText: string;
plan: SystemRunApprovalPlan; plan: SystemRunApprovalPlan;
}; };
@@ -26,7 +25,7 @@ type SystemRunApprovalRuntimeContext =
cwd: string | null; cwd: string | null;
agentId: string | null; agentId: string | null;
sessionKey: string | null; sessionKey: string | null;
rawCommand: string | null; commandText: string;
} }
| { | {
ok: false; ok: false;
@@ -53,13 +52,33 @@ export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayl
if (!payload || typeof payload !== "object" || Array.isArray(payload)) { if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
return null; return null;
} }
const raw = payload as { cmdText?: unknown; plan?: unknown }; const raw = payload as { plan?: unknown; commandText?: unknown; cmdText?: unknown };
const cmdText = normalizeNonEmptyString(raw.cmdText);
const plan = normalizeSystemRunApprovalPlan(raw.plan); const plan = normalizeSystemRunApprovalPlan(raw.plan);
if (!cmdText || !plan) { if (plan) {
return { plan };
}
if (!raw.plan || typeof raw.plan !== "object" || Array.isArray(raw.plan)) {
return null; return null;
} }
return { cmdText, plan }; const legacyPlan = raw.plan as Record<string, unknown>;
const argv = normalizeStringArray(legacyPlan.argv);
const commandText =
normalizeNonEmptyString(legacyPlan.rawCommand) ??
normalizeNonEmptyString(raw.commandText) ??
normalizeNonEmptyString(raw.cmdText);
if (argv.length === 0 || !commandText) {
return null;
}
return {
plan: {
argv,
cwd: normalizeNonEmptyString(legacyPlan.cwd),
commandText,
commandPreview: normalizeNonEmptyString(legacyPlan.commandPreview),
agentId: normalizeNonEmptyString(legacyPlan.agentId),
sessionKey: normalizeNonEmptyString(legacyPlan.sessionKey),
},
};
} }
export function resolveSystemRunApprovalRequestContext(params: { export function resolveSystemRunApprovalRequestContext(params: {
@@ -72,17 +91,22 @@ export function resolveSystemRunApprovalRequestContext(params: {
sessionKey?: unknown; sessionKey?: unknown;
}): SystemRunApprovalRequestContext { }): SystemRunApprovalRequestContext {
const host = normalizeNonEmptyString(params.host) ?? ""; const host = normalizeNonEmptyString(params.host) ?? "";
const plan = host === "node" ? normalizeSystemRunApprovalPlan(params.systemRunPlan) : null; const normalizedPlan =
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; const commandText = normalizedPlan
? normalizedPlan.commandText || formatExecCommand(normalizedPlan.argv)
: fallbackCommand;
const commandPreview = normalizedPlan
? normalizeCommandPreview(normalizedPlan.commandPreview ?? fallbackCommand, commandText)
: null;
const plan = normalizedPlan ? { ...normalizedPlan, commandPreview } : null;
return { return {
plan, plan,
commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined), commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
commandText, commandText,
commandPreview: plan commandPreview,
? 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,10 +130,10 @@ export function resolveSystemRunApprovalRuntimeContext(params: {
cwd: normalizedPlan.cwd, cwd: normalizedPlan.cwd,
agentId: normalizedPlan.agentId, agentId: normalizedPlan.agentId,
sessionKey: normalizedPlan.sessionKey, sessionKey: normalizedPlan.sessionKey,
rawCommand: normalizedPlan.rawCommand, commandText: normalizedPlan.commandText,
}; };
} }
const command = resolveSystemRunCommand({ const command = resolveSystemRunCommandRequest({
command: params.command, command: params.command,
rawCommand: params.rawCommand, rawCommand: params.rawCommand,
}); });
@@ -123,6 +147,6 @@ export function resolveSystemRunApprovalRuntimeContext(params: {
cwd: normalizeNonEmptyString(params.cwd), cwd: normalizeNonEmptyString(params.cwd),
agentId: normalizeNonEmptyString(params.agentId), agentId: normalizeNonEmptyString(params.agentId),
sessionKey: normalizeNonEmptyString(params.sessionKey), sessionKey: normalizeNonEmptyString(params.sessionKey),
rawCommand: normalizeNonEmptyString(params.rawCommand), commandText: command.commandText,
}; };
} }

View File

@@ -2,7 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { resolveSystemRunCommand } from "./system-run-command.js"; import { resolveSystemRunCommandRequest } from "./system-run-command.js";
type ContractFixture = { type ContractFixture = {
cases: ContractCase[]; cases: ContractCase[];
@@ -28,7 +28,7 @@ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8")) as ContractFixt
describe("system-run command contract fixtures", () => { describe("system-run command contract fixtures", () => {
for (const entry of fixture.cases) { for (const entry of fixture.cases) {
test(entry.name, () => { test(entry.name, () => {
const result = resolveSystemRunCommand({ const result = resolveSystemRunCommandRequest({
command: entry.command, command: entry.command,
rawCommand: entry.rawCommand, rawCommand: entry.rawCommand,
}); });
@@ -48,7 +48,7 @@ describe("system-run command contract fixtures", () => {
if (!result.ok) { if (!result.ok) {
throw new Error(`unexpected validation failure: ${result.message}`); throw new Error(`unexpected validation failure: ${result.message}`);
} }
expect(result.cmdText).toBe(entry.expected.displayCommand); expect(result.commandText).toBe(entry.expected.displayCommand);
}); });
} }
}); });

View File

@@ -3,6 +3,7 @@ import {
extractShellCommandFromArgv, extractShellCommandFromArgv,
formatExecCommand, formatExecCommand,
resolveSystemRunCommand, resolveSystemRunCommand,
resolveSystemRunCommandRequest,
validateSystemRunCommandConsistency, validateSystemRunCommandConsistency,
} from "./system-run-command.js"; } from "./system-run-command.js";
@@ -89,8 +90,8 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.shellCommand).toBe(null); expect(res.shellPayload).toBe(null);
expect(res.cmdText).toBe("echo hi"); expect(res.commandText).toBe("echo hi");
}); });
test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs direct argv", () => { test("validateSystemRunCommandConsistency rejects mismatched rawCommand vs direct argv", () => {
@@ -104,6 +105,7 @@ describe("system run command helpers", () => {
const res = validateSystemRunCommandConsistency({ const res = validateSystemRunCommandConsistency({
argv: ["/bin/sh", "-lc", "echo hi"], argv: ["/bin/sh", "-lc", "echo hi"],
rawCommand: "echo hi", rawCommand: "echo hi",
allowLegacyShellText: true,
}); });
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
if (!res.ok) { if (!res.ok) {
@@ -123,6 +125,7 @@ describe("system run command helpers", () => {
const res = validateSystemRunCommandConsistency({ const res = validateSystemRunCommandConsistency({
argv: ["/usr/bin/env", "bash", "-lc", "echo hi"], argv: ["/usr/bin/env", "bash", "-lc", "echo hi"],
rawCommand: "echo hi", rawCommand: "echo hi",
allowLegacyShellText: true,
}); });
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
if (!res.ok) { if (!res.ok) {
@@ -148,8 +151,8 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.shellCommand).toBe("echo hi"); expect(res.shellPayload).toBe("echo hi");
expect(res.cmdText).toBe(raw); expect(res.commandText).toBe(raw);
expect(res.previewText).toBe(null); expect(res.previewText).toBe(null);
}); });
@@ -177,8 +180,8 @@ describe("system run command helpers", () => {
expect(res.details?.code).toBe("MISSING_COMMAND"); expect(res.details?.code).toBe("MISSING_COMMAND");
}); });
test("resolveSystemRunCommand returns normalized argv and cmdText", () => { test("resolveSystemRunCommandRequest accepts legacy shell payloads but returns canonical command text", () => {
const res = resolveSystemRunCommand({ const res = resolveSystemRunCommandRequest({
command: ["cmd.exe", "/d", "/s", "/c", "echo", "SAFE&&whoami"], command: ["cmd.exe", "/d", "/s", "/c", "echo", "SAFE&&whoami"],
rawCommand: "echo SAFE&&whoami", rawCommand: "echo SAFE&&whoami",
}); });
@@ -187,12 +190,12 @@ describe("system run command helpers", () => {
throw new Error("unreachable"); throw new Error("unreachable");
} }
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.shellPayload).toBe("echo SAFE&&whoami");
expect(res.cmdText).toBe("echo SAFE&&whoami"); expect(res.commandText).toBe("cmd.exe /d /s /c echo SAFE&&whoami");
expect(res.previewText).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 commandText to full argv for shell-wrapper positional-argv carriers", () => {
const res = resolveSystemRunCommand({ const res = resolveSystemRunCommand({
command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"], command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"],
}); });
@@ -200,12 +203,12 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.shellCommand).toBe('$0 "$1"'); expect(res.shellPayload).toBe('$0 "$1"');
expect(res.cmdText).toBe('/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker'); expect(res.commandText).toBe('/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker');
expect(res.previewText).toBe(null); expect(res.previewText).toBe(null);
}); });
test("resolveSystemRunCommand binds cmdText to full argv when env prelude modifies shell wrapper", () => { test("resolveSystemRunCommand binds commandText to full argv when env prelude modifies shell wrapper", () => {
const res = resolveSystemRunCommand({ const res = resolveSystemRunCommand({
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"],
}); });
@@ -213,12 +216,12 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.shellCommand).toBe("echo hi"); expect(res.shellPayload).toBe("echo hi");
expect(res.cmdText).toBe('/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc "echo hi"'); expect(res.commandText).toBe('/usr/bin/env BASH_ENV=/tmp/payload.sh bash -lc "echo hi"');
expect(res.previewText).toBe(null); expect(res.previewText).toBe(null);
}); });
test("resolveSystemRunCommand keeps wrapper preview separate from approval text", () => { test("resolveSystemRunCommand keeps wrapper preview separate from canonical command text", () => {
const res = resolveSystemRunCommand({ const res = resolveSystemRunCommand({
command: ["./env", "sh", "-c", "jq --version"], command: ["./env", "sh", "-c", "jq --version"],
}); });
@@ -226,7 +229,7 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.cmdText).toBe("jq --version"); expect(res.commandText).toBe('./env sh -c "jq --version"');
expect(res.previewText).toBe("jq --version"); expect(res.previewText).toBe("jq --version");
}); });
@@ -239,8 +242,20 @@ describe("system run command helpers", () => {
if (!res.ok) { if (!res.ok) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
expect(res.cmdText).toBe('./env sh -c "jq --version"'); expect(res.commandText).toBe('./env sh -c "jq --version"');
expect(res.previewText).toBe("jq --version"); expect(res.previewText).toBe("jq --version");
expect(res.shellCommand).toBe("jq --version"); expect(res.shellPayload).toBe("jq --version");
});
test("resolveSystemRunCommand rejects legacy shell payload text in strict mode", () => {
const res = resolveSystemRunCommand({
command: ["/bin/sh", "-lc", "echo hi"],
rawCommand: "echo hi",
});
expect(res.ok).toBe(false);
if (res.ok) {
throw new Error("unreachable");
}
expect(res.message).toContain("rawCommand does not match command");
}); });
}); });

View File

@@ -14,8 +14,8 @@ import {
export type SystemRunCommandValidation = export type SystemRunCommandValidation =
| { | {
ok: true; ok: true;
shellCommand: string | null; shellPayload: string | null;
cmdText: string; commandText: string;
previewText: string | null; previewText: string | null;
} }
| { | {
@@ -28,9 +28,8 @@ export type ResolvedSystemRunCommand =
| { | {
ok: true; ok: true;
argv: string[]; argv: string[];
rawCommand: string | null; commandText: string;
shellCommand: string | null; shellPayload: string | null;
cmdText: string;
previewText: string | null; previewText: string | null;
} }
| { | {
@@ -58,6 +57,12 @@ export function extractShellCommandFromArgv(argv: string[]): string | null {
return extractShellWrapperCommand(argv).command; return extractShellWrapperCommand(argv).command;
} }
type SystemRunCommandDisplay = {
shellPayload: string | null;
commandText: string;
previewText: string | null;
};
const POSIX_OR_POWERSHELL_INLINE_WRAPPER_NAMES = new Set([ const POSIX_OR_POWERSHELL_INLINE_WRAPPER_NAMES = new Set([
"ash", "ash",
"bash", "bash",
@@ -100,29 +105,42 @@ function hasTrailingPositionalArgvAfterInlineCommand(argv: string[]): boolean {
return wrapperArgv.slice(inlineCommandIndex + 1).some((entry) => entry.trim().length > 0); return wrapperArgv.slice(inlineCommandIndex + 1).some((entry) => entry.trim().length > 0);
} }
function buildSystemRunCommandDisplay(argv: string[]): SystemRunCommandDisplay {
const shellWrapperResolution = extractShellWrapperCommand(argv);
const shellPayload = shellWrapperResolution.command;
const shellWrapperPositionalArgv = hasTrailingPositionalArgvAfterInlineCommand(argv);
const envManipulationBeforeShellWrapper =
shellWrapperResolution.isWrapper && hasEnvManipulationBeforeShellWrapper(argv);
const formattedArgv = formatExecCommand(argv);
const previewText =
shellPayload !== null && !envManipulationBeforeShellWrapper && !shellWrapperPositionalArgv
? shellPayload.trim()
: null;
return {
shellPayload,
commandText: formattedArgv,
previewText,
};
}
function normalizeRawCommandText(rawCommand?: unknown): string | null {
return typeof rawCommand === "string" && rawCommand.trim().length > 0 ? rawCommand.trim() : null;
}
export function validateSystemRunCommandConsistency(params: { export function validateSystemRunCommandConsistency(params: {
argv: string[]; argv: string[];
rawCommand?: string | null; rawCommand?: string | null;
allowLegacyShellText?: boolean;
}): SystemRunCommandValidation { }): SystemRunCommandValidation {
const raw = const raw = normalizeRawCommandText(params.rawCommand);
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0 const display = buildSystemRunCommandDisplay(params.argv);
? params.rawCommand.trim()
: null;
const shellWrapperResolution = extractShellWrapperCommand(params.argv);
const shellCommand = shellWrapperResolution.command;
const shellWrapperPositionalArgv = hasTrailingPositionalArgvAfterInlineCommand(params.argv);
const envManipulationBeforeShellWrapper =
shellWrapperResolution.isWrapper && hasEnvManipulationBeforeShellWrapper(params.argv);
const mustBindDisplayToFullArgv = envManipulationBeforeShellWrapper || shellWrapperPositionalArgv;
const formattedArgv = formatExecCommand(params.argv);
const legacyShellText =
shellCommand !== null && !mustBindDisplayToFullArgv ? shellCommand.trim() : null;
const previewText = legacyShellText;
const cmdText = raw ?? legacyShellText ?? formattedArgv;
if (raw) { if (raw) {
const matchesCanonicalArgv = raw === formattedArgv; const matchesCanonicalArgv = raw === display.commandText;
const matchesLegacyShellText = legacyShellText !== null && raw === legacyShellText; const matchesLegacyShellText =
params.allowLegacyShellText === true &&
display.previewText !== null &&
raw === display.previewText;
if (!matchesCanonicalArgv && !matchesLegacyShellText) { if (!matchesCanonicalArgv && !matchesLegacyShellText) {
return { return {
ok: false, ok: false,
@@ -130,8 +148,8 @@ export function validateSystemRunCommandConsistency(params: {
details: { details: {
code: "RAW_COMMAND_MISMATCH", code: "RAW_COMMAND_MISMATCH",
rawCommand: raw, rawCommand: raw,
inferred: legacyShellText ?? formattedArgv, inferred: display.commandText,
formattedArgv, formattedArgv: display.commandText,
}, },
}; };
} }
@@ -139,10 +157,9 @@ export function validateSystemRunCommandConsistency(params: {
return { return {
ok: true, ok: true,
// Only treat this as a shell command when argv is a recognized shell wrapper. shellPayload: display.shellPayload,
shellCommand: shellCommand !== null ? shellCommand : null, commandText: display.commandText,
cmdText, previewText: display.previewText,
previewText,
}; };
} }
@@ -150,10 +167,7 @@ export function resolveSystemRunCommand(params: {
command?: unknown; command?: unknown;
rawCommand?: unknown; rawCommand?: unknown;
}): ResolvedSystemRunCommand { }): ResolvedSystemRunCommand {
const raw = const raw = normalizeRawCommandText(params.rawCommand);
typeof params.rawCommand === "string" && params.rawCommand.trim().length > 0
? params.rawCommand.trim()
: null;
const command = Array.isArray(params.command) ? params.command : []; const command = Array.isArray(params.command) ? params.command : [];
if (command.length === 0) { if (command.length === 0) {
if (raw) { if (raw) {
@@ -166,9 +180,8 @@ export function resolveSystemRunCommand(params: {
return { return {
ok: true, ok: true,
argv: [], argv: [],
rawCommand: null, commandText: "",
shellCommand: null, shellPayload: null,
cmdText: "",
previewText: null, previewText: null,
}; };
} }
@@ -177,6 +190,7 @@ export function resolveSystemRunCommand(params: {
const validation = validateSystemRunCommandConsistency({ const validation = validateSystemRunCommandConsistency({
argv, argv,
rawCommand: raw, rawCommand: raw,
allowLegacyShellText: false,
}); });
if (!validation.ok) { if (!validation.ok) {
return { return {
@@ -189,9 +203,54 @@ export function resolveSystemRunCommand(params: {
return { return {
ok: true, ok: true,
argv, argv,
rawCommand: raw, commandText: validation.commandText,
shellCommand: validation.shellCommand, shellPayload: validation.shellPayload,
cmdText: validation.cmdText, previewText: validation.previewText,
};
}
export function resolveSystemRunCommandRequest(params: {
command?: unknown;
rawCommand?: unknown;
}): ResolvedSystemRunCommand {
const raw = normalizeRawCommandText(params.rawCommand);
const command = Array.isArray(params.command) ? params.command : [];
if (command.length === 0) {
if (raw) {
return {
ok: false,
message: "rawCommand requires params.command",
details: { code: "MISSING_COMMAND" },
};
}
return {
ok: true,
argv: [],
commandText: "",
shellPayload: null,
previewText: null,
};
}
const argv = command.map((v) => String(v));
const validation = validateSystemRunCommandConsistency({
argv,
rawCommand: raw,
allowLegacyShellText: true,
});
if (!validation.ok) {
return {
ok: false,
message: validation.message,
details: validation.details ?? { code: "RAW_COMMAND_MISMATCH" },
};
}
return {
ok: true,
argv,
commandText: validation.commandText,
shellPayload: validation.shellPayload,
previewText: validation.previewText, previewText: validation.previewText,
}; };
} }

View File

@@ -101,7 +101,7 @@ describe("hardenApprovedExecutionPaths", () => {
mode: "build-plan", mode: "build-plan",
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: 'env sh -c "echo SAFE"',
expectedCommandPreview: "echo SAFE", expectedCommandPreview: "echo SAFE",
}, },
{ {
@@ -144,7 +144,7 @@ describe("hardenApprovedExecutionPaths", () => {
mode: "build-plan", mode: "build-plan",
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: './env sh -c "echo SAFE"',
checkRawCommandMatchesArgv: true, checkRawCommandMatchesArgv: true,
expectedCommandPreview: "echo SAFE", expectedCommandPreview: "echo SAFE",
}, },
@@ -175,10 +175,10 @@ describe("hardenApprovedExecutionPaths", () => {
} }
expect(prepared.plan.argv).toEqual(testCase.expectedArgv({ pathToken })); expect(prepared.plan.argv).toEqual(testCase.expectedArgv({ pathToken }));
if (testCase.expectedCmdText) { if (testCase.expectedCmdText) {
expect(prepared.cmdText).toBe(testCase.expectedCmdText); expect(prepared.plan.commandText).toBe(testCase.expectedCmdText);
} }
if (testCase.checkRawCommandMatchesArgv) { if (testCase.checkRawCommandMatchesArgv) {
expect(prepared.plan.rawCommand).toBe(formatExecCommand(prepared.plan.argv)); expect(prepared.plan.commandText).toBe(formatExecCommand(prepared.plan.argv));
} }
if ("expectedCommandPreview" in testCase) { if ("expectedCommandPreview" in testCase) {
expect(prepared.plan.commandPreview ?? null).toBe(testCase.expectedCommandPreview); expect(prepared.plan.commandPreview ?? null).toBe(testCase.expectedCommandPreview);

View File

@@ -17,7 +17,7 @@ import {
POSIX_INLINE_COMMAND_FLAGS, POSIX_INLINE_COMMAND_FLAGS,
resolveInlineCommandMatch, resolveInlineCommandMatch,
} from "../infra/shell-inline-command.js"; } from "../infra/shell-inline-command.js";
import { formatExecCommand, resolveSystemRunCommand } from "../infra/system-run-command.js"; import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
export type ApprovedCwdSnapshot = { export type ApprovedCwdSnapshot = {
cwd: string; cwd: string;
@@ -630,8 +630,8 @@ export function buildSystemRunApprovalPlan(params: {
cwd?: unknown; cwd?: unknown;
agentId?: unknown; agentId?: unknown;
sessionKey?: unknown; sessionKey?: unknown;
}): { ok: true; plan: SystemRunApprovalPlan; cmdText: string } | { ok: false; message: string } { }): { ok: true; plan: SystemRunApprovalPlan } | { ok: false; message: string } {
const command = resolveSystemRunCommand({ const command = resolveSystemRunCommandRequest({
command: params.command, command: params.command,
rawCommand: params.rawCommand, rawCommand: params.rawCommand,
}); });
@@ -644,15 +644,15 @@ export function buildSystemRunApprovalPlan(params: {
const hardening = hardenApprovedExecutionPaths({ const hardening = hardenApprovedExecutionPaths({
approvedByAsk: true, approvedByAsk: true,
argv: command.argv, argv: command.argv,
shellCommand: command.shellCommand, shellCommand: command.shellPayload,
cwd: normalizeString(params.cwd) ?? undefined, cwd: normalizeString(params.cwd) ?? undefined,
}); });
if (!hardening.ok) { if (!hardening.ok) {
return { ok: false, message: hardening.message }; return { ok: false, message: hardening.message };
} }
const rawCommand = formatExecCommand(hardening.argv) || null; const commandText = formatExecCommand(hardening.argv);
const commandPreview = const commandPreview =
command.previewText?.trim() && command.previewText.trim() !== rawCommand command.previewText?.trim() && command.previewText.trim() !== commandText
? command.previewText.trim() ? command.previewText.trim()
: null; : null;
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({ const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
@@ -667,12 +667,11 @@ export function buildSystemRunApprovalPlan(params: {
plan: { plan: {
argv: hardening.argv, argv: hardening.argv,
cwd: hardening.cwd ?? null, cwd: hardening.cwd ?? null,
rawCommand, commandText,
commandPreview, 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: commandPreview ?? rawCommand ?? formatExecCommand(hardening.argv),
}; };
} }

View File

@@ -432,7 +432,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
expectInvokeOk(sendInvokeResult, { payloadContains: "app-ok" }); expectInvokeOk(sendInvokeResult, { payloadContains: "app-ok" });
}); });
it("forwards canonical cmdText to mac app exec host for positional-argv shell wrappers", async () => { it("forwards canonical command text to mac app exec host for positional-argv shell wrappers", async () => {
const { runViaMacAppExecHost } = await runSystemInvoke({ const { runViaMacAppExecHost } = await runSystemInvoke({
preferMacAppExecHost: true, preferMacAppExecHost: true,
command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"], command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"],
@@ -505,7 +505,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
approvals: expect.anything(), approvals: expect.anything(),
request: expect.objectContaining({ request: expect.objectContaining({
command: ["env", "sh", "-c", "echo SAFE"], command: ["env", "sh", "-c", "echo SAFE"],
rawCommand: "echo SAFE", rawCommand: 'env sh -c "echo SAFE"',
cwd: canonicalCwd, cwd: canonicalCwd,
}), }),
}); });
@@ -593,7 +593,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({ const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false, preferMacAppExecHost: false,
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand, rawCommand: prepared.plan.commandText,
approved: true, approved: true,
security: "full", security: "full",
ask: "off", ask: "off",
@@ -789,7 +789,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({ const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false, preferMacAppExecHost: false,
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand, rawCommand: prepared.plan.commandText,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
cwd: prepared.plan.cwd ?? tmp, cwd: prepared.plan.cwd ?? tmp,
approved: true, approved: true,
@@ -827,7 +827,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({ const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false, preferMacAppExecHost: false,
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand, rawCommand: prepared.plan.commandText,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
cwd: prepared.plan.cwd ?? tmp, cwd: prepared.plan.cwd ?? tmp,
approved: true, approved: true,
@@ -866,7 +866,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({ const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false, preferMacAppExecHost: false,
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand, rawCommand: prepared.plan.commandText,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
cwd: prepared.plan.cwd ?? tmp, cwd: prepared.plan.cwd ?? tmp,
approved: true, approved: true,
@@ -908,7 +908,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({ const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false, preferMacAppExecHost: false,
command: prepared.plan.argv, command: prepared.plan.argv,
rawCommand: prepared.plan.rawCommand, rawCommand: prepared.plan.commandText,
systemRunPlan: prepared.plan, systemRunPlan: prepared.plan,
cwd: prepared.plan.cwd ?? tmp, cwd: prepared.plan.cwd ?? tmp,
approved: true, approved: true,

View File

@@ -16,7 +16,7 @@ import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../in
import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js"; import { resolveExecSafeBinRuntimePolicy } from "../infra/exec-safe-bin-runtime-policy.js";
import { sanitizeSystemRunEnvOverrides } from "../infra/host-env-security.js"; import { sanitizeSystemRunEnvOverrides } from "../infra/host-env-security.js";
import { normalizeSystemRunApprovalPlan } from "../infra/system-run-approval-binding.js"; import { normalizeSystemRunApprovalPlan } from "../infra/system-run-approval-binding.js";
import { resolveSystemRunCommand } from "../infra/system-run-command.js"; import { resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
import { logWarn } from "../logger.js"; import { logWarn } from "../logger.js";
import { evaluateSystemRunPolicy, resolveExecApprovalDecision } from "./exec-policy.js"; import { evaluateSystemRunPolicy, resolveExecApprovalDecision } from "./exec-policy.js";
import { import {
@@ -56,7 +56,7 @@ type SystemRunDeniedReason =
type SystemRunExecutionContext = { type SystemRunExecutionContext = {
sessionKey: string; sessionKey: string;
runId: string; runId: string;
cmdText: string; commandText: string;
suppressNotifyOnExit: boolean; suppressNotifyOnExit: boolean;
}; };
@@ -64,8 +64,9 @@ type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>;
type SystemRunParsePhase = { type SystemRunParsePhase = {
argv: string[]; argv: string[];
shellCommand: string | null; shellPayload: string | null;
cmdText: string; commandText: string;
commandPreview: string | null;
approvalPlan: import("../infra/exec-approvals.js").SystemRunApprovalPlan | null; approvalPlan: import("../infra/exec-approvals.js").SystemRunApprovalPlan | null;
agentId: string | undefined; agentId: string | undefined;
sessionKey: string; sessionKey: string;
@@ -167,7 +168,7 @@ async function sendSystemRunDenied(
sessionKey: execution.sessionKey, sessionKey: execution.sessionKey,
runId: execution.runId, runId: execution.runId,
host: "node", host: "node",
command: execution.cmdText, command: execution.commandText,
reason: params.reason, reason: params.reason,
suppressNotifyOnExit: execution.suppressNotifyOnExit, suppressNotifyOnExit: execution.suppressNotifyOnExit,
}), }),
@@ -184,7 +185,7 @@ export { buildSystemRunApprovalPlan } from "./invoke-system-run-plan.js";
async function parseSystemRunPhase( async function parseSystemRunPhase(
opts: HandleSystemRunInvokeOptions, opts: HandleSystemRunInvokeOptions,
): Promise<SystemRunParsePhase | null> { ): Promise<SystemRunParsePhase | null> {
const command = resolveSystemRunCommand({ const command = resolveSystemRunCommandRequest({
command: opts.params.command, command: opts.params.command,
rawCommand: opts.params.rawCommand, rawCommand: opts.params.rawCommand,
}); });
@@ -203,8 +204,8 @@ async function parseSystemRunPhase(
return null; return null;
} }
const shellCommand = command.shellCommand; const shellPayload = command.shellPayload;
const cmdText = command.cmdText; const commandText = command.commandText;
const approvalPlan = const approvalPlan =
opts.params.systemRunPlan === undefined opts.params.systemRunPlan === undefined
? null ? null
@@ -222,17 +223,18 @@ async function parseSystemRunPhase(
const suppressNotifyOnExit = opts.params.suppressNotifyOnExit === true; const suppressNotifyOnExit = opts.params.suppressNotifyOnExit === true;
const envOverrides = sanitizeSystemRunEnvOverrides({ const envOverrides = sanitizeSystemRunEnvOverrides({
overrides: opts.params.env ?? undefined, overrides: opts.params.env ?? undefined,
shellWrapper: shellCommand !== null, shellWrapper: shellPayload !== null,
}); });
return { return {
argv: command.argv, argv: command.argv,
shellCommand, shellPayload,
cmdText, commandText,
commandPreview: command.previewText,
approvalPlan, approvalPlan,
agentId, agentId,
sessionKey, sessionKey,
runId, runId,
execution: { sessionKey, runId, cmdText, suppressNotifyOnExit }, execution: { sessionKey, runId, commandText, suppressNotifyOnExit },
approvalDecision: resolveExecApprovalDecision(opts.params.approvalDecision), approvalDecision: resolveExecApprovalDecision(opts.params.approvalDecision),
envOverrides, envOverrides,
env: opts.sanitizeEnv(envOverrides), env: opts.sanitizeEnv(envOverrides),
@@ -270,7 +272,7 @@ async function evaluateSystemRunPolicyPhase(
}); });
const bins = autoAllowSkills ? await opts.skillBins.current() : []; const bins = autoAllowSkills ? await opts.skillBins.current() : [];
let { analysisOk, allowlistMatches, allowlistSatisfied, segments } = evaluateSystemRunAllowlist({ let { analysisOk, allowlistMatches, allowlistSatisfied, segments } = evaluateSystemRunAllowlist({
shellCommand: parsed.shellCommand, shellCommand: parsed.shellPayload,
argv: parsed.argv, argv: parsed.argv,
approvals, approvals,
security, security,
@@ -283,7 +285,7 @@ async function evaluateSystemRunPolicyPhase(
autoAllowSkills, autoAllowSkills,
}); });
const isWindows = process.platform === "win32"; const isWindows = process.platform === "win32";
const cmdInvocation = parsed.shellCommand const cmdInvocation = parsed.shellPayload
? opts.isCmdExeInvocation(segments[0]?.argv ?? []) ? opts.isCmdExeInvocation(segments[0]?.argv ?? [])
: opts.isCmdExeInvocation(parsed.argv); : opts.isCmdExeInvocation(parsed.argv);
const policy = evaluateSystemRunPolicy({ const policy = evaluateSystemRunPolicy({
@@ -295,7 +297,7 @@ async function evaluateSystemRunPolicyPhase(
approved: parsed.approved, approved: parsed.approved,
isWindows, isWindows,
cmdInvocation, cmdInvocation,
shellWrapperInvocation: parsed.shellCommand !== null, shellWrapperInvocation: parsed.shellPayload !== null,
}); });
analysisOk = policy.analysisOk; analysisOk = policy.analysisOk;
allowlistSatisfied = policy.allowlistSatisfied; allowlistSatisfied = policy.allowlistSatisfied;
@@ -308,7 +310,7 @@ async function evaluateSystemRunPolicyPhase(
} }
// Fail closed if policy/runtime drift re-allows unapproved shell wrappers. // Fail closed if policy/runtime drift re-allows unapproved shell wrappers.
if (security === "allowlist" && parsed.shellCommand && !policy.approvedByAsk) { if (security === "allowlist" && parsed.shellPayload && !policy.approvedByAsk) {
await sendSystemRunDenied(opts, parsed.execution, { await sendSystemRunDenied(opts, parsed.execution, {
reason: "approval-required", reason: "approval-required",
message: "SYSTEM_RUN_DENIED: approval required", message: "SYSTEM_RUN_DENIED: approval required",
@@ -319,7 +321,7 @@ async function evaluateSystemRunPolicyPhase(
const hardenedPaths = hardenApprovedExecutionPaths({ const hardenedPaths = hardenApprovedExecutionPaths({
approvedByAsk: policy.approvedByAsk, approvedByAsk: policy.approvedByAsk,
argv: parsed.argv, argv: parsed.argv,
shellCommand: parsed.shellCommand, shellCommand: parsed.shellPayload,
cwd: parsed.cwd, cwd: parsed.cwd,
}); });
if (!hardenedPaths.ok) { if (!hardenedPaths.ok) {
@@ -340,7 +342,7 @@ async function evaluateSystemRunPolicyPhase(
const plannedAllowlistArgv = resolvePlannedAllowlistArgv({ const plannedAllowlistArgv = resolvePlannedAllowlistArgv({
security, security,
shellCommand: parsed.shellCommand, shellCommand: parsed.shellPayload,
policy, policy,
segments, segments,
}); });
@@ -405,7 +407,7 @@ async function executeSystemRunPhase(
command: phase.plannedAllowlistArgv ?? phase.argv, command: phase.plannedAllowlistArgv ?? phase.argv,
// Forward canonical display text so companion approval/prompt surfaces bind to // Forward canonical display text so companion approval/prompt surfaces bind to
// the exact command context already validated on the node-host. // the exact command context already validated on the node-host.
rawCommand: phase.cmdText || null, rawCommand: phase.commandText || null,
cwd: phase.cwd ?? null, cwd: phase.cwd ?? null,
env: phase.envOverrides ?? null, env: phase.envOverrides ?? null,
timeoutMs: phase.timeoutMs ?? null, timeoutMs: phase.timeoutMs ?? null,
@@ -437,7 +439,7 @@ async function executeSystemRunPhase(
await opts.sendExecFinishedEvent({ await opts.sendExecFinishedEvent({
sessionKey: phase.sessionKey, sessionKey: phase.sessionKey,
runId: phase.runId, runId: phase.runId,
cmdText: phase.cmdText, commandText: phase.commandText,
result, result,
suppressNotifyOnExit: phase.suppressNotifyOnExit, suppressNotifyOnExit: phase.suppressNotifyOnExit,
}); });
@@ -476,7 +478,7 @@ async function executeSystemRunPhase(
phase.approvals.file, phase.approvals.file,
phase.agentId, phase.agentId,
match, match,
phase.cmdText, phase.commandText,
phase.segments[0]?.resolution?.resolvedPath, phase.segments[0]?.resolution?.resolvedPath,
); );
} }
@@ -496,7 +498,7 @@ async function executeSystemRunPhase(
security: phase.security, security: phase.security,
isWindows: phase.isWindows, isWindows: phase.isWindows,
policy: phase.policy, policy: phase.policy,
shellCommand: phase.shellCommand, shellCommand: phase.shellPayload,
segments: phase.segments, segments: phase.segments,
}); });
@@ -505,7 +507,7 @@ async function executeSystemRunPhase(
await opts.sendExecFinishedEvent({ await opts.sendExecFinishedEvent({
sessionKey: phase.sessionKey, sessionKey: phase.sessionKey,
runId: phase.runId, runId: phase.runId,
cmdText: phase.cmdText, commandText: phase.commandText,
result, result,
suppressNotifyOnExit: phase.suppressNotifyOnExit, suppressNotifyOnExit: phase.suppressNotifyOnExit,
}); });

View File

@@ -51,7 +51,7 @@ export type ExecFinishedResult = {
export type ExecFinishedEventParams = { export type ExecFinishedEventParams = {
sessionKey: string; sessionKey: string;
runId: string; runId: string;
cmdText: string; commandText: string;
result: ExecFinishedResult; result: ExecFinishedResult;
suppressNotifyOnExit?: boolean; suppressNotifyOnExit?: boolean;
}; };

View File

@@ -350,7 +350,7 @@ async function sendExecFinishedEvent(
sessionKey: params.sessionKey, sessionKey: params.sessionKey,
runId: params.runId, runId: params.runId,
host: "node", host: "node",
command: params.cmdText, command: params.commandText,
exitCode: params.result.exitCode ?? undefined, exitCode: params.result.exitCode ?? undefined,
timedOut: params.result.timedOut, timedOut: params.result.timedOut,
success: params.result.success, success: params.result.success,
@@ -505,7 +505,6 @@ export async function handleInvoke(
return; return;
} }
await sendJsonPayloadResult(client, frame, { await sendJsonPayloadResult(client, frame, {
cmdText: prepared.cmdText,
plan: prepared.plan, plan: prepared.plan,
}); });
} catch (err) { } catch (err) {
@@ -549,8 +548,8 @@ export async function handleInvoke(
sendInvokeResult: async (result) => { sendInvokeResult: async (result) => {
await sendInvokeResult(client, frame, result); await sendInvokeResult(client, frame, result);
}, },
sendExecFinishedEvent: async ({ sessionKey, runId, cmdText, result }) => { sendExecFinishedEvent: async ({ sessionKey, runId, commandText, result }) => {
await sendExecFinishedEvent({ client, sessionKey, runId, cmdText, result }); await sendExecFinishedEvent({ client, sessionKey, runId, commandText, result });
}, },
preferMacAppExecHost, preferMacAppExecHost,
}); });

View File

@@ -3,6 +3,7 @@ import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { GatewayClient } from "../gateway/client.js"; import { GatewayClient } from "../gateway/client.js";
import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js"; import { createOperatorApprovalsGatewayClient } from "../gateway/operator-approvals-client.js";
import type { EventFrame } from "../gateway/protocol/index.js"; import type { EventFrame } from "../gateway/protocol/index.js";
import { resolveExecApprovalCommandDisplay } from "../infra/exec-approval-command-display.js";
import { import {
buildExecApprovalPendingReplyPayload, buildExecApprovalPendingReplyPayload,
type ExecApprovalPendingReplyParams, type ExecApprovalPendingReplyParams,
@@ -312,7 +313,7 @@ export class TelegramExecApprovalHandler {
approvalId: request.id, approvalId: request.id,
approvalSlug: request.id.slice(0, 8), approvalSlug: request.id.slice(0, 8),
approvalCommandId: request.id, approvalCommandId: request.id,
command: request.request.command, command: resolveExecApprovalCommandDisplay(request.request).commandText,
cwd: request.request.cwd ?? undefined, cwd: request.request.cwd ?? undefined,
host: request.request.host === "node" ? "node" : "gateway", host: request.request.host === "node" ? "node" : "gateway",
nodeId: request.request.nodeId ?? undefined, nodeId: request.request.nodeId ?? undefined,

View File

@@ -10,19 +10,18 @@ type SystemRunPrepareInput = {
export function buildSystemRunPreparePayload(params: SystemRunPrepareInput) { export function buildSystemRunPreparePayload(params: SystemRunPrepareInput) {
const argv = Array.isArray(params.command) ? params.command.map(String) : []; const argv = Array.isArray(params.command) ? params.command.map(String) : [];
const rawCommand = const previewCommand =
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 commandText = formatExecCommand(argv) || "";
const commandPreview = rawCommand && rawCommand !== formattedArgv ? rawCommand : null; const commandPreview = previewCommand && previewCommand !== commandText ? previewCommand : null;
return { return {
payload: { payload: {
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: formattedArgv, commandText,
commandPreview, 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,

View File

@@ -18,12 +18,12 @@
} }
}, },
{ {
"name": "shell wrapper accepts shell payload raw command when no positional argv carriers", "name": "shell wrapper accepts shell payload raw command at ingress",
"command": ["/bin/sh", "-lc", "echo hi"], "command": ["/bin/sh", "-lc", "echo hi"],
"rawCommand": "echo hi", "rawCommand": "echo hi",
"expected": { "expected": {
"valid": true, "valid": true,
"displayCommand": "echo hi" "displayCommand": "/bin/sh -lc \"echo hi\""
} }
}, },
{ {
@@ -45,12 +45,12 @@
} }
}, },
{ {
"name": "env wrapper shell payload accepted when prelude has no env modifiers", "name": "env wrapper shell payload accepted at ingress when prelude has no env modifiers",
"command": ["/usr/bin/env", "bash", "-lc", "echo hi"], "command": ["/usr/bin/env", "bash", "-lc", "echo hi"],
"rawCommand": "echo hi", "rawCommand": "echo hi",
"expected": { "expected": {
"valid": true, "valid": true,
"displayCommand": "echo hi" "displayCommand": "/usr/bin/env bash -lc \"echo hi\""
} }
}, },
{ {