refactor!: remove versioned system-run approval contract
This commit is contained in:
@@ -94,6 +94,11 @@ Docs: https://docs.openclaw.ai
|
|||||||
- OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (`response.create` with `generate:false`), enable it by default for `openai/*`, and expose `params.openaiWsWarmup` for per-model enable/disable control.
|
- OpenAI/WebSocket warm-up: add optional OpenAI Responses WebSocket warm-up (`response.create` with `generate:false`), enable it by default for `openai/*`, and expose `params.openaiWsWarmup` for per-model enable/disable control.
|
||||||
- Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (`task_completion`) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured `internalEvents`.
|
- Agents/Subagents runtime events: replace ad-hoc subagent completion system-message handoff with typed internal completion events (`task_completion`) that are rendered consistently across direct and queued announce paths, with gateway/CLI plumbing for structured `internalEvents`.
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
- **BREAKING:** Node exec approval payloads now require `systemRunPlan`. `host=node` approval requests without that plan are rejected.
|
||||||
|
- **BREAKING:** Node `system.run` execution now pins path-token commands to the canonical executable path (`realpath`) in both allowlist and approval execution flows. Integrations/tests that asserted token-form argv (for example `tr`) must now accept canonical paths (for example `/usr/bin/tr`).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
- Security/Sandbox media reads: eliminate sandbox media TOCTOU symlink-retarget escapes by enforcing root-scoped boundary-safe reads at attachment/image load time and consolidating shared safe-read helpers across sandbox media callsites. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||||
@@ -257,7 +262,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Models/OpenAI Codex config schema parity: accept `openai-codex-responses` in the config model API schema and TypeScript `ModelApi` union, with regression coverage for config validation. Landed from contributor PR #27501 by @AytuncYildizli. Thanks @AytuncYildizli.
|
- Models/OpenAI Codex config schema parity: accept `openai-codex-responses` in the config model API schema and TypeScript `ModelApi` union, with regression coverage for config validation. Landed from contributor PR #27501 by @AytuncYildizli. Thanks @AytuncYildizli.
|
||||||
- Agents/Models config: preserve agent-level provider `apiKey` and `baseUrl` during merge-mode `models.json` updates when agent values are present. (#27293) thanks @Sid-Qin.
|
- Agents/Models config: preserve agent-level provider `apiKey` and `baseUrl` during merge-mode `models.json` updates when agent values are present. (#27293) thanks @Sid-Qin.
|
||||||
- Azure OpenAI Responses: force `store=true` for `azure-openai-responses` direct responses API calls to avoid multi-turn 400 failures. Landed from contributor PR #27499 by @polarbear-Yang. (#27497)
|
- Azure OpenAI Responses: force `store=true` for `azure-openai-responses` direct responses API calls to avoid multi-turn 400 failures. Landed from contributor PR #27499 by @polarbear-Yang. (#27497)
|
||||||
- Security/Node exec approvals: require structured `commandArgv` approvals for `host=node`, enforce versioned `systemRunBindingV1` matching for argv/cwd/session/agent/env context with fail-closed behavior on missing/mismatched bindings, and add `GIT_EXTERNAL_DIFF` to blocked host env keys. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting.
|
- Security/Node exec approvals: require structured `commandArgv` approvals for `host=node`, enforce `systemRunBinding` matching for argv/cwd/session/agent/env context with fail-closed behavior on missing/mismatched bindings, and add `GIT_EXTERNAL_DIFF` to blocked host env keys. This ships in the next npm release (`2026.2.26`). Thanks @tdjackey for reporting.
|
||||||
- Security/Command authorization: enforce sender authorization for natural-language abort triggers (`stop`-like text) and `/models` listings, preventing unauthorized session aborts and model-auth metadata disclosure. This ships in the next npm release (`2026.2.27`). Thanks @tdjackey for reporting.
|
- Security/Command authorization: enforce sender authorization for natural-language abort triggers (`stop`-like text) and `/models` listings, preventing unauthorized session aborts and model-auth metadata disclosure. This ships in the next npm release (`2026.2.27`). Thanks @tdjackey for reporting.
|
||||||
- Security/Plugin channel HTTP auth: normalize protected `/api/channels` path checks against canonicalized request paths (case + percent-decoding + slash normalization), resolve encoded dot-segment traversal variants, and fail closed on malformed `%`-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting.
|
- Security/Plugin channel HTTP auth: normalize protected `/api/channels` path checks against canonicalized request paths (case + percent-decoding + slash normalization), resolve encoded dot-segment traversal variants, and fail closed on malformed `%`-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting.
|
||||||
- Security/Gateway node pairing: pin paired-device `platform`/`deviceFamily` metadata across reconnects and bind those fields into device-auth signatures, so reconnect metadata spoofing cannot expand node command allowlists without explicit repair pairing. This ships in the next npm release (`2026.2.26`). Thanks @76embiid21 for reporting.
|
- Security/Gateway node pairing: pin paired-device `platform`/`deviceFamily` metadata across reconnects and bind those fields into device-auth signatures, so reconnect metadata spoofing cannot expand node command allowlists without explicit repair pairing. This ships in the next npm release (`2026.2.26`). Thanks @76embiid21 for reporting.
|
||||||
|
|||||||
@@ -2822,7 +2822,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
public let id: String?
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let commandargv: [String]?
|
public let commandargv: [String]?
|
||||||
public let systemrunplanv2: [String: AnyCodable]?
|
public let systemrunplan: [String: AnyCodable]?
|
||||||
public let env: [String: AnyCodable]?
|
public let env: [String: AnyCodable]?
|
||||||
public let cwd: AnyCodable?
|
public let cwd: AnyCodable?
|
||||||
public let nodeid: AnyCodable?
|
public let nodeid: AnyCodable?
|
||||||
@@ -2843,7 +2843,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
id: String?,
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
commandargv: [String]?,
|
commandargv: [String]?,
|
||||||
systemrunplanv2: [String: AnyCodable]?,
|
systemrunplan: [String: AnyCodable]?,
|
||||||
env: [String: AnyCodable]?,
|
env: [String: AnyCodable]?,
|
||||||
cwd: AnyCodable?,
|
cwd: AnyCodable?,
|
||||||
nodeid: AnyCodable?,
|
nodeid: AnyCodable?,
|
||||||
@@ -2863,7 +2863,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
self.id = id
|
self.id = id
|
||||||
self.command = command
|
self.command = command
|
||||||
self.commandargv = commandargv
|
self.commandargv = commandargv
|
||||||
self.systemrunplanv2 = systemrunplanv2
|
self.systemrunplan = systemrunplan
|
||||||
self.env = env
|
self.env = env
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
@@ -2885,7 +2885,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
case id
|
case id
|
||||||
case command
|
case command
|
||||||
case commandargv = "commandArgv"
|
case commandargv = "commandArgv"
|
||||||
case systemrunplanv2 = "systemRunPlanV2"
|
case systemrunplan = "systemRunPlan"
|
||||||
case env
|
case env
|
||||||
case cwd
|
case cwd
|
||||||
case nodeid = "nodeId"
|
case nodeid = "nodeId"
|
||||||
|
|||||||
@@ -2822,7 +2822,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
public let id: String?
|
public let id: String?
|
||||||
public let command: String
|
public let command: String
|
||||||
public let commandargv: [String]?
|
public let commandargv: [String]?
|
||||||
public let systemrunplanv2: [String: AnyCodable]?
|
public let systemrunplan: [String: AnyCodable]?
|
||||||
public let env: [String: AnyCodable]?
|
public let env: [String: AnyCodable]?
|
||||||
public let cwd: AnyCodable?
|
public let cwd: AnyCodable?
|
||||||
public let nodeid: AnyCodable?
|
public let nodeid: AnyCodable?
|
||||||
@@ -2843,7 +2843,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
id: String?,
|
id: String?,
|
||||||
command: String,
|
command: String,
|
||||||
commandargv: [String]?,
|
commandargv: [String]?,
|
||||||
systemrunplanv2: [String: AnyCodable]?,
|
systemrunplan: [String: AnyCodable]?,
|
||||||
env: [String: AnyCodable]?,
|
env: [String: AnyCodable]?,
|
||||||
cwd: AnyCodable?,
|
cwd: AnyCodable?,
|
||||||
nodeid: AnyCodable?,
|
nodeid: AnyCodable?,
|
||||||
@@ -2863,7 +2863,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
self.id = id
|
self.id = id
|
||||||
self.command = command
|
self.command = command
|
||||||
self.commandargv = commandargv
|
self.commandargv = commandargv
|
||||||
self.systemrunplanv2 = systemrunplanv2
|
self.systemrunplan = systemrunplan
|
||||||
self.env = env
|
self.env = env
|
||||||
self.cwd = cwd
|
self.cwd = cwd
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
@@ -2885,7 +2885,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
|||||||
case id
|
case id
|
||||||
case command
|
case command
|
||||||
case commandargv = "commandArgv"
|
case commandargv = "commandArgv"
|
||||||
case systemrunplanv2 = "systemRunPlanV2"
|
case systemrunplan = "systemRunPlan"
|
||||||
case env
|
case env
|
||||||
case cwd
|
case cwd
|
||||||
case nodeid = "nodeId"
|
case nodeid = "nodeId"
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ The Gateway treats these as **claims** and enforces server-side allowlists.
|
|||||||
|
|
||||||
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.
|
- When an exec request needs approval, the gateway broadcasts `exec.approval.requested`.
|
||||||
- Operator clients resolve by calling `exec.approval.resolve` (requires `operator.approvals` scope).
|
- Operator clients resolve by calling `exec.approval.resolve` (requires `operator.approvals` scope).
|
||||||
|
- For `host=node`, `exec.approval.request` must include `systemRunPlan` (canonical `argv`/`cwd`/`rawCommand`/session metadata). Requests missing `systemRunPlan` are rejected.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ When a prompt is required, the gateway broadcasts `exec.approval.requested` to o
|
|||||||
The Control UI and macOS app resolve it via `exec.approval.resolve`, then the gateway forwards the
|
The Control UI and macOS app resolve it via `exec.approval.resolve`, then the gateway forwards the
|
||||||
approved request to the node host.
|
approved request to the node host.
|
||||||
|
|
||||||
|
For `host=node`, approval requests include a canonical `systemRunPlan` payload. The gateway uses
|
||||||
|
that plan as the authoritative command/cwd/session context when forwarding approved `system.run`
|
||||||
|
requests.
|
||||||
|
|
||||||
When approvals are required, the exec tool returns immediately with an approval id. Use that id to
|
When approvals are required, the exec tool returns immediately with an approval id. Use that id to
|
||||||
correlate later system events (`Exec finished` / `Exec denied`). If no decision arrives before the
|
correlate later system events (`Exec finished` / `Exec denied`). If no decision arrives before the
|
||||||
timeout, the request is treated as an approval timeout and surfaced as a denial reason.
|
timeout, the request is treated as an approval timeout and surfaced as a denial reason.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ExecAsk, ExecSecurity } from "../infra/exec-approvals.js";
|
import type { ExecAsk, ExecSecurity, SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
|
DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
|
||||||
DEFAULT_APPROVAL_TIMEOUT_MS,
|
DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||||
@@ -9,6 +9,7 @@ export type RequestExecApprovalDecisionParams = {
|
|||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
|
systemRunPlan?: SystemRunApprovalPlan;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
@@ -28,6 +29,7 @@ type ExecApprovalRequestToolParams = {
|
|||||||
id: string;
|
id: string;
|
||||||
command: string;
|
command: string;
|
||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
|
systemRunPlan?: SystemRunApprovalPlan;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
@@ -52,6 +54,7 @@ function buildExecApprovalRequestToolParams(
|
|||||||
id: params.id,
|
id: params.id,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
commandArgv: params.commandArgv,
|
commandArgv: params.commandArgv,
|
||||||
|
systemRunPlan: params.systemRunPlan,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
nodeId: params.nodeId,
|
nodeId: params.nodeId,
|
||||||
@@ -156,6 +159,7 @@ export async function requestExecApprovalDecisionForHost(params: {
|
|||||||
approvalId: string;
|
approvalId: string;
|
||||||
command: string;
|
command: string;
|
||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
|
systemRunPlan?: SystemRunApprovalPlan;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
workdir: string;
|
workdir: string;
|
||||||
host: "gateway" | "node";
|
host: "gateway" | "node";
|
||||||
@@ -174,6 +178,7 @@ export async function requestExecApprovalDecisionForHost(params: {
|
|||||||
id: params.approvalId,
|
id: params.approvalId,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
commandArgv: params.commandArgv,
|
commandArgv: params.commandArgv,
|
||||||
|
systemRunPlan: params.systemRunPlan,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
cwd: params.workdir,
|
cwd: params.workdir,
|
||||||
nodeId: params.nodeId,
|
nodeId: params.nodeId,
|
||||||
@@ -194,6 +199,7 @@ export async function registerExecApprovalRequestForHost(params: {
|
|||||||
approvalId: string;
|
approvalId: string;
|
||||||
command: string;
|
command: string;
|
||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
|
systemRunPlan?: SystemRunApprovalPlan;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
workdir: string;
|
workdir: string;
|
||||||
host: "gateway" | "node";
|
host: "gateway" | "node";
|
||||||
@@ -212,6 +218,7 @@ export async function registerExecApprovalRequestForHost(params: {
|
|||||||
id: params.approvalId,
|
id: params.approvalId,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
commandArgv: params.commandArgv,
|
commandArgv: params.commandArgv,
|
||||||
|
systemRunPlan: params.systemRunPlan,
|
||||||
env: params.env,
|
env: params.env,
|
||||||
cwd: params.workdir,
|
cwd: params.workdir,
|
||||||
nodeId: params.nodeId,
|
nodeId: params.nodeId,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "../infra/exec-approvals.js";
|
} from "../infra/exec-approvals.js";
|
||||||
import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
|
import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
|
||||||
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
||||||
|
import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js";
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
import {
|
import {
|
||||||
registerExecApprovalRequestForHost,
|
registerExecApprovalRequestForHost,
|
||||||
@@ -95,6 +96,31 @@ export async function executeNodeHostCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const argv = buildNodeShellCommand(params.command, nodeInfo?.platform);
|
const argv = buildNodeShellCommand(params.command, nodeInfo?.platform);
|
||||||
|
const prepareRaw = await callGatewayTool<{ payload?: unknown }>(
|
||||||
|
"node.invoke",
|
||||||
|
{ timeoutMs: 15_000 },
|
||||||
|
{
|
||||||
|
nodeId,
|
||||||
|
command: "system.run.prepare",
|
||||||
|
params: {
|
||||||
|
command: argv,
|
||||||
|
rawCommand: params.command,
|
||||||
|
cwd: params.workdir,
|
||||||
|
agentId: params.agentId,
|
||||||
|
sessionKey: params.sessionKey,
|
||||||
|
},
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const prepared = parsePreparedSystemRunPayload(prepareRaw?.payload);
|
||||||
|
if (!prepared) {
|
||||||
|
throw new Error("invalid system.run.prepare response");
|
||||||
|
}
|
||||||
|
const runArgv = prepared.plan.argv;
|
||||||
|
const runRawCommand = prepared.plan.rawCommand ?? prepared.cmdText;
|
||||||
|
const runCwd = prepared.plan.cwd ?? params.workdir;
|
||||||
|
const runAgentId = prepared.plan.agentId ?? params.agentId;
|
||||||
|
const runSessionKey = prepared.plan.sessionKey ?? params.sessionKey;
|
||||||
|
|
||||||
const nodeEnv = params.requestedEnv ? { ...params.requestedEnv } : undefined;
|
const nodeEnv = params.requestedEnv ? { ...params.requestedEnv } : undefined;
|
||||||
const baseAllowlistEval = evaluateShellAllowlist({
|
const baseAllowlistEval = evaluateShellAllowlist({
|
||||||
@@ -170,13 +196,13 @@ export async function executeNodeHostCommand(
|
|||||||
nodeId,
|
nodeId,
|
||||||
command: "system.run",
|
command: "system.run",
|
||||||
params: {
|
params: {
|
||||||
command: argv,
|
command: runArgv,
|
||||||
rawCommand: params.command,
|
rawCommand: runRawCommand,
|
||||||
cwd: params.workdir,
|
cwd: runCwd,
|
||||||
env: nodeEnv,
|
env: nodeEnv,
|
||||||
timeoutMs: typeof params.timeoutSec === "number" ? params.timeoutSec * 1000 : undefined,
|
timeoutMs: typeof params.timeoutSec === "number" ? params.timeoutSec * 1000 : undefined,
|
||||||
agentId: params.agentId,
|
agentId: runAgentId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: runSessionKey,
|
||||||
approved: approvedByAsk,
|
approved: approvedByAsk,
|
||||||
approvalDecision: approvalDecision ?? undefined,
|
approvalDecision: approvalDecision ?? undefined,
|
||||||
runId: runId ?? undefined,
|
runId: runId ?? undefined,
|
||||||
@@ -197,16 +223,17 @@ 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 registerExecApprovalRequestForHost({
|
const registration = await registerExecApprovalRequestForHost({
|
||||||
approvalId,
|
approvalId,
|
||||||
command: params.command,
|
command: prepared.cmdText,
|
||||||
commandArgv: argv,
|
commandArgv: prepared.plan.argv,
|
||||||
|
systemRunPlan: prepared.plan,
|
||||||
env: nodeEnv,
|
env: nodeEnv,
|
||||||
workdir: params.workdir,
|
workdir: runCwd,
|
||||||
host: "node",
|
host: "node",
|
||||||
nodeId,
|
nodeId,
|
||||||
security: hostSecurity,
|
security: hostSecurity,
|
||||||
ask: hostAsk,
|
ask: hostAsk,
|
||||||
agentId: params.agentId,
|
agentId: runAgentId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: runSessionKey,
|
||||||
turnSourceChannel: params.turnSourceChannel,
|
turnSourceChannel: params.turnSourceChannel,
|
||||||
turnSourceTo: params.turnSourceTo,
|
turnSourceTo: params.turnSourceTo,
|
||||||
turnSourceAccountId: params.turnSourceAccountId,
|
turnSourceAccountId: params.turnSourceAccountId,
|
||||||
|
|||||||
@@ -27,6 +27,33 @@ let callGatewayTool: typeof import("./tools/gateway.js").callGatewayTool;
|
|||||||
let createExecTool: typeof import("./bash-tools.exec.js").createExecTool;
|
let createExecTool: typeof import("./bash-tools.exec.js").createExecTool;
|
||||||
let detectCommandObfuscation: typeof import("../infra/exec-obfuscation-detect.js").detectCommandObfuscation;
|
let detectCommandObfuscation: typeof import("../infra/exec-obfuscation-detect.js").detectCommandObfuscation;
|
||||||
|
|
||||||
|
function buildPreparedSystemRunPayload(rawInvokeParams: unknown) {
|
||||||
|
const invoke = (rawInvokeParams ?? {}) as {
|
||||||
|
params?: {
|
||||||
|
command?: unknown;
|
||||||
|
rawCommand?: unknown;
|
||||||
|
cwd?: unknown;
|
||||||
|
agentId?: unknown;
|
||||||
|
sessionKey?: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const params = invoke.params ?? {};
|
||||||
|
const argv = Array.isArray(params.command) ? params.command.map(String) : [];
|
||||||
|
const rawCommand = typeof params.rawCommand === "string" ? params.rawCommand : null;
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: rawCommand ?? argv.join(" "),
|
||||||
|
plan: {
|
||||||
|
argv,
|
||||||
|
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
||||||
|
rawCommand,
|
||||||
|
agentId: typeof params.agentId === "string" ? params.agentId : null,
|
||||||
|
sessionKey: typeof params.sessionKey === "string" ? params.sessionKey : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("exec approvals", () => {
|
describe("exec approvals", () => {
|
||||||
let previousHome: string | undefined;
|
let previousHome: string | undefined;
|
||||||
let previousUserProfile: string | undefined;
|
let previousUserProfile: string | undefined;
|
||||||
@@ -71,8 +98,14 @@ describe("exec approvals", () => {
|
|||||||
return { decision: "allow-once" };
|
return { decision: "allow-once" };
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
invokeParams = params;
|
const invoke = params as { command?: string };
|
||||||
return { ok: true };
|
if (invoke.command === "system.run.prepare") {
|
||||||
|
return buildPreparedSystemRunPayload(params);
|
||||||
|
}
|
||||||
|
if (invoke.command === "system.run") {
|
||||||
|
invokeParams = params;
|
||||||
|
return { payload: { success: true, stdout: "ok" } };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
@@ -116,12 +149,16 @@ describe("exec approvals", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
vi.mocked(callGatewayTool).mockImplementation(async (method, _opts, params) => {
|
||||||
calls.push(method);
|
calls.push(method);
|
||||||
if (method === "exec.approvals.node.get") {
|
if (method === "exec.approvals.node.get") {
|
||||||
return { file: approvalsFile };
|
return { file: approvalsFile };
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const invoke = params as { command?: string };
|
||||||
|
if (invoke.command === "system.run.prepare") {
|
||||||
|
return buildPreparedSystemRunPayload(params);
|
||||||
|
}
|
||||||
return { payload: { success: true, stdout: "ok" } };
|
return { payload: { success: true, stdout: "ok" } };
|
||||||
}
|
}
|
||||||
// exec.approval.request should NOT be called when allowlist is satisfied
|
// exec.approval.request should NOT be called when allowlist is satisfied
|
||||||
@@ -266,7 +303,8 @@ describe("exec approvals", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
const nodeInvokeCommands: string[] = [];
|
||||||
|
vi.mocked(callGatewayTool).mockImplementation(async (method, _opts, params) => {
|
||||||
calls.push(method);
|
calls.push(method);
|
||||||
if (method === "exec.approval.request") {
|
if (method === "exec.approval.request") {
|
||||||
return { status: "accepted", id: "approval-id" };
|
return { status: "accepted", id: "approval-id" };
|
||||||
@@ -275,6 +313,13 @@ describe("exec approvals", () => {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const invoke = params as { command?: string };
|
||||||
|
if (invoke.command) {
|
||||||
|
nodeInvokeCommands.push(invoke.command);
|
||||||
|
}
|
||||||
|
if (invoke.command === "system.run.prepare") {
|
||||||
|
return buildPreparedSystemRunPayload(params);
|
||||||
|
}
|
||||||
return { payload: { success: true, stdout: "should-not-run" } };
|
return { payload: { success: true, stdout: "should-not-run" } };
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
@@ -289,7 +334,7 @@ describe("exec approvals", () => {
|
|||||||
|
|
||||||
const result = await tool.execute("call5", { command: "echo hi | sh" });
|
const result = await tool.execute("call5", { command: "echo hi | sh" });
|
||||||
expect(result.details.status).toBe("approval-pending");
|
expect(result.details.status).toBe("approval-pending");
|
||||||
await expect.poll(() => calls.filter((call) => call === "node.invoke").length).toBe(0);
|
await expect.poll(() => nodeInvokeCommands.includes("system.run")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("denies gateway obfuscated command when approval request times out", async () => {
|
it("denies gateway obfuscated command when approval request times out", async () => {
|
||||||
|
|||||||
@@ -356,6 +356,21 @@ describe("nodes run", () => {
|
|||||||
return mockNodeList(["system.run"]);
|
return mockNodeList(["system.run"]);
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const command = (params as { command?: string } | undefined)?.command;
|
||||||
|
if (command === "system.run.prepare") {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: "echo hi",
|
||||||
|
plan: {
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
rawCommand: "echo hi",
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
expect(params).toMatchObject({
|
expect(params).toMatchObject({
|
||||||
nodeId: NODE_ID,
|
nodeId: NODE_ID,
|
||||||
command: "system.run",
|
command: "system.run",
|
||||||
@@ -391,6 +406,21 @@ describe("nodes run", () => {
|
|||||||
return mockNodeList(["system.run"]);
|
return mockNodeList(["system.run"]);
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const command = (params as { command?: string } | undefined)?.command;
|
||||||
|
if (command === "system.run.prepare") {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: "echo hi",
|
||||||
|
plan: {
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
cwd: null,
|
||||||
|
rawCommand: "echo hi",
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
invokeCalls += 1;
|
invokeCalls += 1;
|
||||||
if (invokeCalls === 1) {
|
if (invokeCalls === 1) {
|
||||||
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
||||||
@@ -411,6 +441,10 @@ describe("nodes run", () => {
|
|||||||
expect(params).toMatchObject({
|
expect(params).toMatchObject({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
command: "echo hi",
|
command: "echo hi",
|
||||||
|
commandArgv: ["echo", "hi"],
|
||||||
|
systemRunPlan: expect.objectContaining({
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
}),
|
||||||
nodeId: NODE_ID,
|
nodeId: NODE_ID,
|
||||||
host: "node",
|
host: "node",
|
||||||
timeoutMs: 120_000,
|
timeoutMs: 120_000,
|
||||||
@@ -429,11 +463,26 @@ describe("nodes run", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("fails with user denied when approval decision is deny", async () => {
|
it("fails with user denied when approval decision is deny", async () => {
|
||||||
callGateway.mockImplementation(async ({ method }) => {
|
callGateway.mockImplementation(async ({ method, params }) => {
|
||||||
if (method === "node.list") {
|
if (method === "node.list") {
|
||||||
return mockNodeList(["system.run"]);
|
return mockNodeList(["system.run"]);
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const command = (params as { command?: string } | undefined)?.command;
|
||||||
|
if (command === "system.run.prepare") {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: "echo hi",
|
||||||
|
plan: {
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
cwd: null,
|
||||||
|
rawCommand: "echo hi",
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
||||||
}
|
}
|
||||||
if (method === "exec.approval.request") {
|
if (method === "exec.approval.request") {
|
||||||
@@ -446,11 +495,26 @@ describe("nodes run", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("fails closed for timeout and invalid approval decisions", async () => {
|
it("fails closed for timeout and invalid approval decisions", async () => {
|
||||||
callGateway.mockImplementation(async ({ method }) => {
|
callGateway.mockImplementation(async ({ method, params }) => {
|
||||||
if (method === "node.list") {
|
if (method === "node.list") {
|
||||||
return mockNodeList(["system.run"]);
|
return mockNodeList(["system.run"]);
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const command = (params as { command?: string } | undefined)?.command;
|
||||||
|
if (command === "system.run.prepare") {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: "echo hi",
|
||||||
|
plan: {
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
cwd: null,
|
||||||
|
rawCommand: "echo hi",
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
||||||
}
|
}
|
||||||
if (method === "exec.approval.request") {
|
if (method === "exec.approval.request") {
|
||||||
@@ -460,11 +524,26 @@ describe("nodes run", () => {
|
|||||||
});
|
});
|
||||||
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: approval timed out");
|
await expect(executeNodes(BASE_RUN_INPUT)).rejects.toThrow("exec denied: approval timed out");
|
||||||
|
|
||||||
callGateway.mockImplementation(async ({ method }) => {
|
callGateway.mockImplementation(async ({ method, params }) => {
|
||||||
if (method === "node.list") {
|
if (method === "node.list") {
|
||||||
return mockNodeList(["system.run"]);
|
return mockNodeList(["system.run"]);
|
||||||
}
|
}
|
||||||
if (method === "node.invoke") {
|
if (method === "node.invoke") {
|
||||||
|
const command = (params as { command?: string } | undefined)?.command;
|
||||||
|
if (command === "system.run.prepare") {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
cmdText: "echo hi",
|
||||||
|
plan: {
|
||||||
|
argv: ["echo", "hi"],
|
||||||
|
cwd: null,
|
||||||
|
rawCommand: "echo hi",
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
throw new Error("SYSTEM_RUN_DENIED: approval required");
|
||||||
}
|
}
|
||||||
if (method === "exec.approval.request") {
|
if (method === "exec.approval.request") {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from "../../cli/nodes-screen.js";
|
} from "../../cli/nodes-screen.js";
|
||||||
import { parseDurationMs } from "../../cli/parse-duration.js";
|
import { parseDurationMs } from "../../cli/parse-duration.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { parsePreparedSystemRunPayload } from "../../infra/system-run-approval-context.js";
|
||||||
import { formatExecCommand } from "../../infra/system-run-command.js";
|
import { formatExecCommand } from "../../infra/system-run-command.js";
|
||||||
import { imageMimeFromFormat } from "../../media/mime.js";
|
import { imageMimeFromFormat } from "../../media/mime.js";
|
||||||
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
|
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
|
||||||
@@ -530,14 +531,36 @@ export function createNodesTool(options?: {
|
|||||||
typeof params.needsScreenRecording === "boolean"
|
typeof params.needsScreenRecording === "boolean"
|
||||||
? params.needsScreenRecording
|
? params.needsScreenRecording
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const prepareRaw = await callGatewayTool<{ payload?: unknown }>(
|
||||||
|
"node.invoke",
|
||||||
|
gatewayOpts,
|
||||||
|
{
|
||||||
|
nodeId,
|
||||||
|
command: "system.run.prepare",
|
||||||
|
params: {
|
||||||
|
command,
|
||||||
|
rawCommand: formatExecCommand(command),
|
||||||
|
cwd,
|
||||||
|
agentId,
|
||||||
|
sessionKey,
|
||||||
|
},
|
||||||
|
timeoutMs: invokeTimeoutMs,
|
||||||
|
idempotencyKey: crypto.randomUUID(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const prepared = parsePreparedSystemRunPayload(prepareRaw?.payload);
|
||||||
|
if (!prepared) {
|
||||||
|
throw new Error("invalid system.run.prepare response");
|
||||||
|
}
|
||||||
const runParams = {
|
const runParams = {
|
||||||
command,
|
command: prepared.plan.argv,
|
||||||
cwd,
|
rawCommand: prepared.plan.rawCommand ?? prepared.cmdText,
|
||||||
|
cwd: prepared.plan.cwd ?? cwd,
|
||||||
env,
|
env,
|
||||||
timeoutMs: commandTimeoutMs,
|
timeoutMs: commandTimeoutMs,
|
||||||
needsScreenRecording,
|
needsScreenRecording,
|
||||||
agentId,
|
agentId: prepared.plan.agentId ?? agentId,
|
||||||
sessionKey,
|
sessionKey: prepared.plan.sessionKey ?? sessionKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// First attempt without approval flags.
|
// First attempt without approval flags.
|
||||||
@@ -560,20 +583,20 @@ export function createNodesTool(options?: {
|
|||||||
// Node requires approval – create a pending approval request on
|
// Node requires approval – create a pending approval request on
|
||||||
// the gateway and wait for the user to approve/deny via the UI.
|
// the gateway and wait for the user to approve/deny via the UI.
|
||||||
const APPROVAL_TIMEOUT_MS = 120_000;
|
const APPROVAL_TIMEOUT_MS = 120_000;
|
||||||
const cmdText = formatExecCommand(command);
|
|
||||||
const approvalId = crypto.randomUUID();
|
const approvalId = crypto.randomUUID();
|
||||||
const approvalResult = await callGatewayTool(
|
const approvalResult = await callGatewayTool(
|
||||||
"exec.approval.request",
|
"exec.approval.request",
|
||||||
{ ...gatewayOpts, timeoutMs: APPROVAL_TIMEOUT_MS + 5_000 },
|
{ ...gatewayOpts, timeoutMs: APPROVAL_TIMEOUT_MS + 5_000 },
|
||||||
{
|
{
|
||||||
id: approvalId,
|
id: approvalId,
|
||||||
command: cmdText,
|
command: prepared.cmdText,
|
||||||
commandArgv: command,
|
commandArgv: prepared.plan.argv,
|
||||||
cwd,
|
systemRunPlan: prepared.plan,
|
||||||
|
cwd: prepared.plan.cwd ?? cwd,
|
||||||
nodeId,
|
nodeId,
|
||||||
host: "node",
|
host: "node",
|
||||||
agentId,
|
agentId: prepared.plan.agentId ?? agentId,
|
||||||
sessionKey,
|
sessionKey: prepared.plan.sessionKey ?? sessionKey,
|
||||||
turnSourceChannel,
|
turnSourceChannel,
|
||||||
turnSourceTo,
|
turnSourceTo,
|
||||||
turnSourceAccountId,
|
turnSourceAccountId,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => {
|
|||||||
payload: {
|
payload: {
|
||||||
cmdText: rawCommand ?? argv.join(" "),
|
cmdText: rawCommand ?? argv.join(" "),
|
||||||
plan: {
|
plan: {
|
||||||
version: 2,
|
|
||||||
argv,
|
argv,
|
||||||
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
cwd: typeof params.cwd === "string" ? params.cwd : null,
|
||||||
rawCommand,
|
rawCommand,
|
||||||
@@ -185,8 +184,7 @@ 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?.["commandArgv"]).toEqual(["echo", "hi"]);
|
||||||
expect(approval?.params?.["systemRunPlanV2"]).toEqual({
|
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
||||||
version: 2,
|
|
||||||
argv: ["echo", "hi"],
|
argv: ["echo", "hi"],
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
rawCommand: null,
|
rawCommand: null,
|
||||||
@@ -220,8 +218,7 @@ describe("nodes-cli coverage", () => {
|
|||||||
});
|
});
|
||||||
const approval = getApprovalRequestCall();
|
const approval = getApprovalRequestCall();
|
||||||
expect(approval?.params?.["commandArgv"]).toEqual(["/bin/sh", "-lc", "echo hi"]);
|
expect(approval?.params?.["commandArgv"]).toEqual(["/bin/sh", "-lc", "echo hi"]);
|
||||||
expect(approval?.params?.["systemRunPlanV2"]).toEqual({
|
expect(approval?.params?.["systemRunPlan"]).toEqual({
|
||||||
version: 2,
|
|
||||||
argv: ["/bin/sh", "-lc", "echo hi"],
|
argv: ["/bin/sh", "-lc", "echo hi"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
rawCommand: "echo hi",
|
rawCommand: "echo hi",
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ async function maybeRequestNodesRunApproval(params: {
|
|||||||
id: approvalId,
|
id: approvalId,
|
||||||
command: params.preparedCmdText,
|
command: params.preparedCmdText,
|
||||||
commandArgv: params.approvalPlan.argv,
|
commandArgv: params.approvalPlan.argv,
|
||||||
systemRunPlanV2: params.approvalPlan,
|
systemRunPlan: params.approvalPlan,
|
||||||
cwd: params.approvalPlan.cwd,
|
cwd: params.approvalPlan.cwd,
|
||||||
nodeId: params.nodeId,
|
nodeId: params.nodeId,
|
||||||
host: "node",
|
host: "node",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { buildSystemRunApprovalBindingV1 } from "../infra/system-run-approval-binding.js";
|
import { buildSystemRunApprovalBinding } from "../infra/system-run-approval-binding.js";
|
||||||
import { evaluateSystemRunApprovalMatch } from "./node-invoke-system-run-approval-match.js";
|
import { evaluateSystemRunApprovalMatch } from "./node-invoke-system-run-approval-match.js";
|
||||||
|
|
||||||
describe("evaluateSystemRunApprovalMatch", () => {
|
describe("evaluateSystemRunApprovalMatch", () => {
|
||||||
@@ -29,7 +29,7 @@ describe("evaluateSystemRunApprovalMatch", () => {
|
|||||||
request: {
|
request: {
|
||||||
host: "node",
|
host: "node",
|
||||||
command: "echo SAFE",
|
command: "echo SAFE",
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["echo", "SAFE"],
|
argv: ["echo", "SAFE"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -51,7 +51,7 @@ describe("evaluateSystemRunApprovalMatch", () => {
|
|||||||
request: {
|
request: {
|
||||||
host: "node",
|
host: "node",
|
||||||
command: "echo SAFE",
|
command: "echo SAFE",
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["echo SAFE"],
|
argv: ["echo SAFE"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -77,7 +77,7 @@ describe("evaluateSystemRunApprovalMatch", () => {
|
|||||||
request: {
|
request: {
|
||||||
host: "node",
|
host: "node",
|
||||||
command: "git diff",
|
command: "git diff",
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -104,7 +104,7 @@ describe("evaluateSystemRunApprovalMatch", () => {
|
|||||||
request: {
|
request: {
|
||||||
host: "node",
|
host: "node",
|
||||||
command: "git diff",
|
command: "git diff",
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -149,7 +149,7 @@ describe("evaluateSystemRunApprovalMatch", () => {
|
|||||||
host: "node",
|
host: "node",
|
||||||
command: "echo STALE",
|
command: "echo STALE",
|
||||||
commandArgv: ["echo STALE"],
|
commandArgv: ["echo STALE"],
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["echo", "SAFE"],
|
argv: ["echo", "SAFE"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { ExecApprovalRequestPayload } from "../infra/exec-approvals.js";
|
import type { ExecApprovalRequestPayload } from "../infra/exec-approvals.js";
|
||||||
import {
|
import {
|
||||||
buildSystemRunApprovalBindingV1,
|
buildSystemRunApprovalBinding,
|
||||||
missingSystemRunApprovalBindingV1,
|
missingSystemRunApprovalBinding,
|
||||||
matchSystemRunApprovalBindingV1,
|
matchSystemRunApprovalBinding,
|
||||||
type SystemRunApprovalMatchResult,
|
type SystemRunApprovalMatchResult,
|
||||||
} from "../infra/system-run-approval-binding.js";
|
} from "../infra/system-run-approval-binding.js";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export function evaluateSystemRunApprovalMatch(params: {
|
|||||||
return requestMismatch();
|
return requestMismatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualBinding = buildSystemRunApprovalBindingV1({
|
const actualBinding = buildSystemRunApprovalBinding({
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
cwd: params.binding.cwd,
|
cwd: params.binding.cwd,
|
||||||
agentId: params.binding.agentId,
|
agentId: params.binding.agentId,
|
||||||
@@ -41,13 +41,13 @@ export function evaluateSystemRunApprovalMatch(params: {
|
|||||||
env: params.binding.env,
|
env: params.binding.env,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedBinding = params.request.systemRunBindingV1;
|
const expectedBinding = params.request.systemRunBinding;
|
||||||
if (!expectedBinding) {
|
if (!expectedBinding) {
|
||||||
return missingSystemRunApprovalBindingV1({
|
return missingSystemRunApprovalBinding({
|
||||||
actualEnvKeys: actualBinding.envKeys,
|
actualEnvKeys: actualBinding.envKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return matchSystemRunApprovalBindingV1({
|
return matchSystemRunApprovalBinding({
|
||||||
expected: expectedBinding,
|
expected: expectedBinding,
|
||||||
actual: actualBinding.binding,
|
actual: actualBinding.binding,
|
||||||
actualEnvKeys: actualBinding.envKeys,
|
actualEnvKeys: actualBinding.envKeys,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildSystemRunApprovalBindingV1,
|
buildSystemRunApprovalBinding,
|
||||||
buildSystemRunApprovalEnvBinding,
|
buildSystemRunApprovalEnvBinding,
|
||||||
} from "../infra/system-run-approval-binding.js";
|
} from "../infra/system-run-approval-binding.js";
|
||||||
import { ExecApprovalManager, type ExecApprovalRecord } from "./exec-approval-manager.js";
|
import { ExecApprovalManager, type ExecApprovalRecord } from "./exec-approval-manager.js";
|
||||||
@@ -30,7 +30,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
nodeId: "node-1",
|
nodeId: "node-1",
|
||||||
command,
|
command,
|
||||||
commandArgv,
|
commandArgv,
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: effectiveBindingArgv,
|
argv: effectiveBindingArgv,
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -229,17 +229,16 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
expectAllowOnceForwardingResult(result);
|
expectAllowOnceForwardingResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses systemRunPlanV2 for forwarded command context and ignores caller tampering", () => {
|
test("uses systemRunPlan for forwarded command context and ignores caller tampering", () => {
|
||||||
const record = makeRecord("echo SAFE", ["echo", "SAFE"]);
|
const record = makeRecord("echo SAFE", ["echo", "SAFE"]);
|
||||||
record.request.systemRunPlanV2 = {
|
record.request.systemRunPlan = {
|
||||||
version: 2,
|
|
||||||
argv: ["/usr/bin/echo", "SAFE"],
|
argv: ["/usr/bin/echo", "SAFE"],
|
||||||
cwd: "/real/cwd",
|
cwd: "/real/cwd",
|
||||||
rawCommand: "/usr/bin/echo SAFE",
|
rawCommand: "/usr/bin/echo SAFE",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "agent:main:main",
|
sessionKey: "agent:main:main",
|
||||||
};
|
};
|
||||||
record.request.systemRunBindingV1 = buildSystemRunApprovalBindingV1({
|
record.request.systemRunBinding = buildSystemRunApprovalBinding({
|
||||||
argv: ["/usr/bin/echo", "SAFE"],
|
argv: ["/usr/bin/echo", "SAFE"],
|
||||||
cwd: "/real/cwd",
|
cwd: "/real/cwd",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
@@ -297,8 +296,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
|
|
||||||
test("rejects env hash mismatch", () => {
|
test("rejects env hash mismatch", () => {
|
||||||
const record = makeRecord("git diff", ["git", "diff"]);
|
const record = makeRecord("git diff", ["git", "diff"]);
|
||||||
record.request.systemRunBindingV1 = {
|
record.request.systemRunBinding = {
|
||||||
version: 1,
|
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -329,8 +327,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
test("accepts matching env hash with reordered keys", () => {
|
test("accepts matching env hash with reordered keys", () => {
|
||||||
const record = makeRecord("git diff", ["git", "diff"]);
|
const record = makeRecord("git diff", ["git", "diff"]);
|
||||||
const binding = buildSystemRunApprovalEnvBinding({ SAFE_A: "1", SAFE_B: "2" });
|
const binding = buildSystemRunApprovalEnvBinding({ SAFE_A: "1", SAFE_B: "2" });
|
||||||
record.request.systemRunBindingV1 = {
|
record.request.systemRunBinding = {
|
||||||
version: 1,
|
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -363,7 +360,7 @@ describe("sanitizeSystemRunParamsForForwarding", () => {
|
|||||||
nodeId: "node-1",
|
nodeId: "node-1",
|
||||||
command: "echo SAFE",
|
command: "echo SAFE",
|
||||||
commandArgv: ["echo", "SAFE"],
|
commandArgv: ["echo", "SAFE"],
|
||||||
systemRunBindingV1: buildSystemRunApprovalBindingV1({
|
systemRunBinding: buildSystemRunApprovalBinding({
|
||||||
argv: ["echo", "SAFE"],
|
argv: ["echo", "SAFE"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runtimeContext = resolveSystemRunApprovalRuntimeContext({
|
const runtimeContext = resolveSystemRunApprovalRuntimeContext({
|
||||||
planV2: snapshot.request.systemRunPlanV2 ?? null,
|
plan: snapshot.request.systemRunPlan ?? null,
|
||||||
command: p.command,
|
command: p.command,
|
||||||
rawCommand: p.rawCommand,
|
rawCommand: p.rawCommand,
|
||||||
cwd: p.cwd,
|
cwd: p.cwd,
|
||||||
@@ -223,8 +223,8 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
|||||||
details: runtimeContext.details,
|
details: runtimeContext.details,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (runtimeContext.planV2) {
|
if (runtimeContext.plan) {
|
||||||
next.command = [...runtimeContext.planV2.argv];
|
next.command = [...runtimeContext.plan.argv];
|
||||||
if (runtimeContext.rawCommand) {
|
if (runtimeContext.rawCommand) {
|
||||||
next.rawCommand = runtimeContext.rawCommand;
|
next.rawCommand = runtimeContext.rawCommand;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -90,10 +90,9 @@ export const ExecApprovalRequestParamsSchema = Type.Object(
|
|||||||
id: Type.Optional(NonEmptyString),
|
id: Type.Optional(NonEmptyString),
|
||||||
command: NonEmptyString,
|
command: NonEmptyString,
|
||||||
commandArgv: Type.Optional(Type.Array(Type.String())),
|
commandArgv: Type.Optional(Type.Array(Type.String())),
|
||||||
systemRunPlanV2: Type.Optional(
|
systemRunPlan: Type.Optional(
|
||||||
Type.Object(
|
Type.Object(
|
||||||
{
|
{
|
||||||
version: Type.Literal(2),
|
|
||||||
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()]),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
||||||
type ExecApprovalDecision,
|
type ExecApprovalDecision,
|
||||||
} from "../../infra/exec-approvals.js";
|
} from "../../infra/exec-approvals.js";
|
||||||
import { buildSystemRunApprovalBindingV1 } from "../../infra/system-run-approval-binding.js";
|
import { buildSystemRunApprovalBinding } from "../../infra/system-run-approval-binding.js";
|
||||||
import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js";
|
import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js";
|
||||||
import type { ExecApprovalManager } from "../exec-approval-manager.js";
|
import type { ExecApprovalManager } from "../exec-approval-manager.js";
|
||||||
import {
|
import {
|
||||||
@@ -48,7 +48,7 @@ export function createExecApprovalHandlers(
|
|||||||
commandArgv?: string[];
|
commandArgv?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
systemRunPlanV2?: unknown;
|
systemRunPlan?: unknown;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
host?: string;
|
host?: string;
|
||||||
security?: string;
|
security?: string;
|
||||||
@@ -73,7 +73,7 @@ export function createExecApprovalHandlers(
|
|||||||
host,
|
host,
|
||||||
command: p.command,
|
command: p.command,
|
||||||
commandArgv: p.commandArgv,
|
commandArgv: p.commandArgv,
|
||||||
systemRunPlanV2: p.systemRunPlanV2,
|
systemRunPlan: p.systemRunPlan,
|
||||||
cwd: p.cwd,
|
cwd: p.cwd,
|
||||||
agentId: p.agentId,
|
agentId: p.agentId,
|
||||||
sessionKey: p.sessionKey,
|
sessionKey: p.sessionKey,
|
||||||
@@ -91,6 +91,14 @@ export function createExecApprovalHandlers(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (host === "node" && !approvalContext.plan) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, "systemRunPlan is required for host=node"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
host === "node" &&
|
host === "node" &&
|
||||||
(!Array.isArray(effectiveCommandArgv) || effectiveCommandArgv.length === 0)
|
(!Array.isArray(effectiveCommandArgv) || effectiveCommandArgv.length === 0)
|
||||||
@@ -102,9 +110,9 @@ export function createExecApprovalHandlers(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const systemRunBindingV1 =
|
const systemRunBinding =
|
||||||
host === "node"
|
host === "node"
|
||||||
? buildSystemRunApprovalBindingV1({
|
? buildSystemRunApprovalBinding({
|
||||||
argv: effectiveCommandArgv,
|
argv: effectiveCommandArgv,
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
agentId: effectiveAgentId,
|
agentId: effectiveAgentId,
|
||||||
@@ -123,9 +131,9 @@ export function createExecApprovalHandlers(
|
|||||||
const request = {
|
const request = {
|
||||||
command: effectiveCommandText,
|
command: effectiveCommandText,
|
||||||
commandArgv: effectiveCommandArgv,
|
commandArgv: effectiveCommandArgv,
|
||||||
envKeys: systemRunBindingV1?.envKeys?.length ? systemRunBindingV1.envKeys : undefined,
|
envKeys: systemRunBinding?.envKeys?.length ? systemRunBinding.envKeys : undefined,
|
||||||
systemRunBindingV1: systemRunBindingV1?.binding ?? null,
|
systemRunBinding: systemRunBinding?.binding ?? null,
|
||||||
systemRunPlanV2: approvalContext.planV2,
|
systemRunPlan: approvalContext.plan,
|
||||||
cwd: effectiveCwd ?? null,
|
cwd: effectiveCwd ?? null,
|
||||||
nodeId: host === "node" ? nodeId : null,
|
nodeId: host === "node" ? nodeId : null,
|
||||||
host: host || null,
|
host: host || null,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { emitAgentEvent } from "../../infra/agent-events.js";
|
import { emitAgentEvent } from "../../infra/agent-events.js";
|
||||||
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
|
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
|
||||||
import { buildSystemRunApprovalBindingV1 } from "../../infra/system-run-approval-binding.js";
|
import { buildSystemRunApprovalBinding } from "../../infra/system-run-approval-binding.js";
|
||||||
import { resetLogger, setLoggerOverride } from "../../logging.js";
|
import { resetLogger, setLoggerOverride } from "../../logging.js";
|
||||||
import { ExecApprovalManager } from "../exec-approval-manager.js";
|
import { ExecApprovalManager } from "../exec-approval-manager.js";
|
||||||
import { validateExecApprovalRequestParams } from "../protocol/index.js";
|
import { validateExecApprovalRequestParams } from "../protocol/index.js";
|
||||||
@@ -249,6 +249,13 @@ describe("exec approval handlers", () => {
|
|||||||
const defaultExecApprovalRequestParams = {
|
const defaultExecApprovalRequestParams = {
|
||||||
command: "echo ok",
|
command: "echo ok",
|
||||||
commandArgv: ["echo", "ok"],
|
commandArgv: ["echo", "ok"],
|
||||||
|
systemRunPlan: {
|
||||||
|
argv: ["/usr/bin/echo", "ok"],
|
||||||
|
cwd: "/tmp",
|
||||||
|
rawCommand: "/usr/bin/echo ok",
|
||||||
|
agentId: "main",
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
nodeId: "node-1",
|
nodeId: "node-1",
|
||||||
host: "node",
|
host: "node",
|
||||||
@@ -278,6 +285,37 @@ describe("exec approval handlers", () => {
|
|||||||
...defaultExecApprovalRequestParams,
|
...defaultExecApprovalRequestParams,
|
||||||
...params.params,
|
...params.params,
|
||||||
} as unknown as ExecApprovalRequestArgs["params"];
|
} as unknown as ExecApprovalRequestArgs["params"];
|
||||||
|
const hasExplicitPlan = !!params.params && Object.hasOwn(params.params, "systemRunPlan");
|
||||||
|
if (
|
||||||
|
!hasExplicitPlan &&
|
||||||
|
(requestParams as { host?: string }).host === "node" &&
|
||||||
|
Array.isArray((requestParams as { commandArgv?: unknown }).commandArgv)
|
||||||
|
) {
|
||||||
|
const commandArgv = (requestParams as { commandArgv: unknown[] }).commandArgv.map((entry) =>
|
||||||
|
String(entry),
|
||||||
|
);
|
||||||
|
const cwdValue =
|
||||||
|
typeof (requestParams as { cwd?: unknown }).cwd === "string"
|
||||||
|
? ((requestParams as { cwd: string }).cwd ?? null)
|
||||||
|
: null;
|
||||||
|
const commandText =
|
||||||
|
typeof (requestParams as { command?: unknown }).command === "string"
|
||||||
|
? ((requestParams as { command: string }).command ?? null)
|
||||||
|
: null;
|
||||||
|
requestParams.systemRunPlan = {
|
||||||
|
argv: commandArgv,
|
||||||
|
cwd: cwdValue,
|
||||||
|
rawCommand: commandText,
|
||||||
|
agentId:
|
||||||
|
typeof (requestParams as { agentId?: unknown }).agentId === "string"
|
||||||
|
? ((requestParams as { agentId: string }).agentId ?? null)
|
||||||
|
: null,
|
||||||
|
sessionKey:
|
||||||
|
typeof (requestParams as { sessionKey?: unknown }).sessionKey === "string"
|
||||||
|
? ((requestParams as { sessionKey: string }).sessionKey ?? null)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
return params.handlers["exec.approval.request"]({
|
return params.handlers["exec.approval.request"]({
|
||||||
params: requestParams,
|
params: requestParams,
|
||||||
respond: params.respond as unknown as ExecApprovalRequestArgs["respond"],
|
respond: params.respond as unknown as ExecApprovalRequestArgs["respond"],
|
||||||
@@ -385,21 +423,21 @@ describe("exec approval handlers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects host=node approval requests without commandArgv", async () => {
|
it("rejects host=node approval requests without systemRunPlan", async () => {
|
||||||
const { handlers, respond, context } = createExecApprovalFixture();
|
const { handlers, respond, context } = createExecApprovalFixture();
|
||||||
await requestExecApproval({
|
await requestExecApproval({
|
||||||
handlers,
|
handlers,
|
||||||
respond,
|
respond,
|
||||||
context,
|
context,
|
||||||
params: {
|
params: {
|
||||||
commandArgv: undefined,
|
systemRunPlan: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(respond).toHaveBeenCalledWith(
|
expect(respond).toHaveBeenCalledWith(
|
||||||
false,
|
false,
|
||||||
undefined,
|
undefined,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
message: "commandArgv is required for host=node",
|
message: "systemRunPlan is required for host=node",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -462,8 +500,8 @@ 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["envKeys"]).toEqual(["A_VAR", "Z_VAR"]);
|
expect(request["envKeys"]).toEqual(["A_VAR", "Z_VAR"]);
|
||||||
expect(request["systemRunBindingV1"]).toEqual(
|
expect(request["systemRunBinding"]).toEqual(
|
||||||
buildSystemRunApprovalBindingV1({
|
buildSystemRunApprovalBinding({
|
||||||
argv: ["echo", "ok"],
|
argv: ["echo", "ok"],
|
||||||
cwd: "/tmp",
|
cwd: "/tmp",
|
||||||
env: { A_VAR: "a", Z_VAR: "z" },
|
env: { A_VAR: "a", Z_VAR: "z" },
|
||||||
@@ -471,7 +509,7 @@ describe("exec approval handlers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers systemRunPlanV2 canonical command/cwd when present", async () => {
|
it("prefers systemRunPlan canonical command/cwd when present", async () => {
|
||||||
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
const { handlers, broadcasts, respond, context } = createExecApprovalFixture();
|
||||||
await requestExecApproval({
|
await requestExecApproval({
|
||||||
handlers,
|
handlers,
|
||||||
@@ -481,8 +519,7 @@ describe("exec approval handlers", () => {
|
|||||||
command: "echo stale",
|
command: "echo stale",
|
||||||
commandArgv: ["echo", "stale"],
|
commandArgv: ["echo", "stale"],
|
||||||
cwd: "/tmp/link/sub",
|
cwd: "/tmp/link/sub",
|
||||||
systemRunPlanV2: {
|
systemRunPlan: {
|
||||||
version: 2,
|
|
||||||
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",
|
||||||
@@ -499,8 +536,7 @@ describe("exec approval handlers", () => {
|
|||||||
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["systemRunPlanV2"]).toEqual({
|
expect(request["systemRunPlan"]).toEqual({
|
||||||
version: 2,
|
|
||||||
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",
|
||||||
|
|||||||
@@ -80,6 +80,13 @@ async function requestAllowOnceApproval(
|
|||||||
id: approvalId,
|
id: approvalId,
|
||||||
command,
|
command,
|
||||||
commandArgv,
|
commandArgv,
|
||||||
|
systemRunPlan: {
|
||||||
|
argv: commandArgv,
|
||||||
|
cwd: null,
|
||||||
|
rawCommand: command,
|
||||||
|
agentId: null,
|
||||||
|
sessionKey: null,
|
||||||
|
},
|
||||||
nodeId,
|
nodeId,
|
||||||
cwd: null,
|
cwd: null,
|
||||||
host: "node",
|
host: "node",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 type { ExecApprovalRequestPayload } from "../infra/exec-approvals.js";
|
import type { ExecApprovalRequestPayload } from "../infra/exec-approvals.js";
|
||||||
import { buildSystemRunApprovalBindingV1 } from "../infra/system-run-approval-binding.js";
|
import { buildSystemRunApprovalBinding } from "../infra/system-run-approval-binding.js";
|
||||||
import { evaluateSystemRunApprovalMatch } from "./node-invoke-system-run-approval-match.js";
|
import { evaluateSystemRunApprovalMatch } from "./node-invoke-system-run-approval-match.js";
|
||||||
|
|
||||||
type FixtureCase = {
|
type FixtureCase = {
|
||||||
@@ -15,7 +15,7 @@ type FixtureCase = {
|
|||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
agentId?: string | null;
|
agentId?: string | null;
|
||||||
sessionKey?: string | null;
|
sessionKey?: string | null;
|
||||||
bindingV1?: {
|
binding?: {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
agentId?: string | null;
|
agentId?: string | null;
|
||||||
@@ -57,13 +57,13 @@ function buildRequestPayload(entry: FixtureCase): ExecApprovalRequestPayload {
|
|||||||
agentId: entry.request.agentId ?? null,
|
agentId: entry.request.agentId ?? null,
|
||||||
sessionKey: entry.request.sessionKey ?? null,
|
sessionKey: entry.request.sessionKey ?? null,
|
||||||
};
|
};
|
||||||
if (entry.request.bindingV1) {
|
if (entry.request.binding) {
|
||||||
payload.systemRunBindingV1 = buildSystemRunApprovalBindingV1({
|
payload.systemRunBinding = buildSystemRunApprovalBinding({
|
||||||
argv: entry.request.bindingV1.argv,
|
argv: entry.request.binding.argv,
|
||||||
cwd: entry.request.bindingV1.cwd,
|
cwd: entry.request.binding.cwd,
|
||||||
agentId: entry.request.bindingV1.agentId,
|
agentId: entry.request.binding.agentId,
|
||||||
sessionKey: entry.request.bindingV1.sessionKey,
|
sessionKey: entry.request.binding.sessionKey,
|
||||||
env: entry.request.bindingV1.env,
|
env: entry.request.binding.env,
|
||||||
}).binding;
|
}).binding;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildSystemRunApprovalBindingV1,
|
buildSystemRunApprovalBinding,
|
||||||
buildSystemRunApprovalEnvBinding,
|
buildSystemRunApprovalEnvBinding,
|
||||||
matchSystemRunApprovalBindingV1,
|
matchSystemRunApprovalBinding,
|
||||||
matchSystemRunApprovalEnvHash,
|
matchSystemRunApprovalEnvHash,
|
||||||
toSystemRunApprovalMismatchError,
|
toSystemRunApprovalMismatchError,
|
||||||
} from "../infra/system-run-approval-binding.js";
|
} from "../infra/system-run-approval-binding.js";
|
||||||
@@ -48,16 +48,16 @@ describe("matchSystemRunApprovalEnvHash", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("matchSystemRunApprovalBindingV1", () => {
|
describe("matchSystemRunApprovalBinding", () => {
|
||||||
test("accepts matching binding with reordered env keys", () => {
|
test("accepts matching binding with reordered env keys", () => {
|
||||||
const expected = buildSystemRunApprovalBindingV1({
|
const expected = buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
env: { SAFE_A: "1", SAFE_B: "2" },
|
env: { SAFE_A: "1", SAFE_B: "2" },
|
||||||
});
|
});
|
||||||
const actual = buildSystemRunApprovalBindingV1({
|
const actual = buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
@@ -65,7 +65,7 @@ describe("matchSystemRunApprovalBindingV1", () => {
|
|||||||
env: { SAFE_B: "2", SAFE_A: "1" },
|
env: { SAFE_B: "2", SAFE_A: "1" },
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
matchSystemRunApprovalBindingV1({
|
matchSystemRunApprovalBinding({
|
||||||
expected: expected.binding,
|
expected: expected.binding,
|
||||||
actual: actual.binding,
|
actual: actual.binding,
|
||||||
actualEnvKeys: actual.envKeys,
|
actualEnvKeys: actual.envKeys,
|
||||||
@@ -74,21 +74,21 @@ describe("matchSystemRunApprovalBindingV1", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("rejects env mismatch", () => {
|
test("rejects env mismatch", () => {
|
||||||
const expected = buildSystemRunApprovalBindingV1({
|
const expected = buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
env: { SAFE: "1" },
|
env: { SAFE: "1" },
|
||||||
});
|
});
|
||||||
const actual = buildSystemRunApprovalBindingV1({
|
const actual = buildSystemRunApprovalBinding({
|
||||||
argv: ["git", "diff"],
|
argv: ["git", "diff"],
|
||||||
cwd: null,
|
cwd: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
sessionKey: null,
|
sessionKey: null,
|
||||||
env: { SAFE: "2" },
|
env: { SAFE: "2" },
|
||||||
});
|
});
|
||||||
const result = matchSystemRunApprovalBindingV1({
|
const result = matchSystemRunApprovalBinding({
|
||||||
expected: expected.binding,
|
expected: expected.binding,
|
||||||
actual: actual.binding,
|
actual: actual.binding,
|
||||||
actualEnvKeys: actual.envKeys,
|
actualEnvKeys: actual.envKeys,
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ function renderQuotedArgv(argv: string[]): string {
|
|||||||
return argv.map((token) => shellEscapeSingleArg(token)).join(" ");
|
return argv.map((token) => shellEscapeSingleArg(token)).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePlannedSegmentArgv(segment: ExecCommandSegment): string[] | null {
|
export function resolvePlannedSegmentArgv(segment: ExecCommandSegment): string[] | null {
|
||||||
if (segment.resolution?.policyBlocked === true) {
|
if (segment.resolution?.policyBlocked === true) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -638,7 +638,8 @@ function resolvePlannedSegmentArgv(segment: ExecCommandSegment): string[] | null
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const argv = [...baseArgv];
|
const argv = [...baseArgv];
|
||||||
const resolvedExecutable = segment.resolution?.resolvedPath?.trim() ?? "";
|
const resolvedExecutable =
|
||||||
|
segment.resolution?.resolvedRealPath?.trim() ?? segment.resolution?.resolvedPath?.trim() ?? "";
|
||||||
if (resolvedExecutable) {
|
if (resolvedExecutable) {
|
||||||
argv[0] = resolvedExecutable;
|
argv[0] = resolvedExecutable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ export type ExecHost = "sandbox" | "gateway" | "node";
|
|||||||
export type ExecSecurity = "deny" | "allowlist" | "full";
|
export type ExecSecurity = "deny" | "allowlist" | "full";
|
||||||
export type ExecAsk = "off" | "on-miss" | "always";
|
export type ExecAsk = "off" | "on-miss" | "always";
|
||||||
|
|
||||||
export type SystemRunApprovalBindingV1 = {
|
export type SystemRunApprovalBinding = {
|
||||||
version: 1;
|
|
||||||
argv: string[];
|
argv: string[];
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
@@ -20,8 +19,7 @@ export type SystemRunApprovalBindingV1 = {
|
|||||||
envHash: string | null;
|
envHash: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SystemRunApprovalPlanV2 = {
|
export type SystemRunApprovalPlan = {
|
||||||
version: 2;
|
|
||||||
argv: string[];
|
argv: string[];
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
rawCommand: string | null;
|
rawCommand: string | null;
|
||||||
@@ -34,8 +32,8 @@ export type ExecApprovalRequestPayload = {
|
|||||||
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[];
|
||||||
systemRunBindingV1?: SystemRunApprovalBindingV1 | null;
|
systemRunBinding?: SystemRunApprovalBinding | null;
|
||||||
systemRunPlanV2?: SystemRunApprovalPlanV2 | null;
|
systemRunPlan?: SystemRunApprovalPlan | null;
|
||||||
cwd?: string | null;
|
cwd?: string | null;
|
||||||
nodeId?: string | null;
|
nodeId?: string | null;
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const DEFAULT_SAFE_BINS = ["jq", "cut", "uniq", "head", "tail", "tr", "wc
|
|||||||
export type CommandResolution = {
|
export type CommandResolution = {
|
||||||
rawExecutable: string;
|
rawExecutable: string;
|
||||||
resolvedPath?: string;
|
resolvedPath?: string;
|
||||||
|
resolvedRealPath?: string;
|
||||||
executableName: string;
|
executableName: string;
|
||||||
effectiveArgv?: string[];
|
effectiveArgv?: string[];
|
||||||
wrapperChain?: string[];
|
wrapperChain?: string[];
|
||||||
@@ -86,6 +87,17 @@ function resolveExecutablePath(rawExecutable: string, cwd?: string, env?: NodeJS
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryResolveRealpath(filePath: string | undefined): string | undefined {
|
||||||
|
if (!filePath) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return fs.realpathSync(filePath);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveCommandResolution(
|
export function resolveCommandResolution(
|
||||||
command: string,
|
command: string,
|
||||||
cwd?: string,
|
cwd?: string,
|
||||||
@@ -96,10 +108,12 @@ export function resolveCommandResolution(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
||||||
|
const resolvedRealPath = tryResolveRealpath(resolvedPath);
|
||||||
const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable;
|
const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable;
|
||||||
return {
|
return {
|
||||||
rawExecutable,
|
rawExecutable,
|
||||||
resolvedPath,
|
resolvedPath,
|
||||||
|
resolvedRealPath,
|
||||||
executableName,
|
executableName,
|
||||||
effectiveArgv: [rawExecutable],
|
effectiveArgv: [rawExecutable],
|
||||||
wrapperChain: [],
|
wrapperChain: [],
|
||||||
@@ -119,10 +133,12 @@ export function resolveCommandResolutionFromArgv(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
const resolvedPath = resolveExecutablePath(rawExecutable, cwd, env);
|
||||||
|
const resolvedRealPath = tryResolveRealpath(resolvedPath);
|
||||||
const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable;
|
const executableName = resolvedPath ? path.basename(resolvedPath) : rawExecutable;
|
||||||
return {
|
return {
|
||||||
rawExecutable,
|
rawExecutable,
|
||||||
resolvedPath,
|
resolvedPath,
|
||||||
|
resolvedRealPath,
|
||||||
executableName,
|
executableName,
|
||||||
effectiveArgv,
|
effectiveArgv,
|
||||||
wrapperChain: plan.wrappers,
|
wrapperChain: plan.wrappers,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import type { SystemRunApprovalBindingV1, SystemRunApprovalPlanV2 } from "./exec-approvals.js";
|
import type { SystemRunApprovalBinding, SystemRunApprovalPlan } from "./exec-approvals.js";
|
||||||
import { normalizeEnvVarKey } from "./host-env-security.js";
|
import { normalizeEnvVarKey } from "./host-env-security.js";
|
||||||
|
|
||||||
type NormalizedSystemRunEnvEntry = [key: string, value: string];
|
type NormalizedSystemRunEnvEntry = [key: string, value: string];
|
||||||
@@ -16,20 +16,16 @@ function normalizeStringArray(value: unknown): string[] {
|
|||||||
return Array.isArray(value) ? value.map((entry) => String(entry)) : [];
|
return Array.isArray(value) ? value.map((entry) => String(entry)) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeSystemRunApprovalPlanV2(value: unknown): SystemRunApprovalPlanV2 | null {
|
export function normalizeSystemRunApprovalPlan(value: unknown): SystemRunApprovalPlan | null {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const candidate = value as Record<string, unknown>;
|
const candidate = value as Record<string, unknown>;
|
||||||
if (candidate.version !== 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const argv = normalizeStringArray(candidate.argv);
|
const argv = normalizeStringArray(candidate.argv);
|
||||||
if (argv.length === 0) {
|
if (argv.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
version: 2,
|
|
||||||
argv,
|
argv,
|
||||||
cwd: normalizeString(candidate.cwd),
|
cwd: normalizeString(candidate.cwd),
|
||||||
rawCommand: normalizeString(candidate.rawCommand),
|
rawCommand: normalizeString(candidate.rawCommand),
|
||||||
@@ -75,17 +71,16 @@ export function buildSystemRunApprovalEnvBinding(env: unknown): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSystemRunApprovalBindingV1(params: {
|
export function buildSystemRunApprovalBinding(params: {
|
||||||
argv: unknown;
|
argv: unknown;
|
||||||
cwd?: unknown;
|
cwd?: unknown;
|
||||||
agentId?: unknown;
|
agentId?: unknown;
|
||||||
sessionKey?: unknown;
|
sessionKey?: unknown;
|
||||||
env?: unknown;
|
env?: unknown;
|
||||||
}): { binding: SystemRunApprovalBindingV1; envKeys: string[] } {
|
}): { binding: SystemRunApprovalBinding; envKeys: string[] } {
|
||||||
const envBinding = buildSystemRunApprovalEnvBinding(params.env);
|
const envBinding = buildSystemRunApprovalEnvBinding(params.env);
|
||||||
return {
|
return {
|
||||||
binding: {
|
binding: {
|
||||||
version: 1,
|
|
||||||
argv: normalizeStringArray(params.argv),
|
argv: normalizeStringArray(params.argv),
|
||||||
cwd: normalizeString(params.cwd),
|
cwd: normalizeString(params.cwd),
|
||||||
agentId: normalizeString(params.agentId),
|
agentId: normalizeString(params.agentId),
|
||||||
@@ -161,17 +156,11 @@ export function matchSystemRunApprovalEnvHash(params: {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchSystemRunApprovalBindingV1(params: {
|
export function matchSystemRunApprovalBinding(params: {
|
||||||
expected: SystemRunApprovalBindingV1;
|
expected: SystemRunApprovalBinding;
|
||||||
actual: SystemRunApprovalBindingV1;
|
actual: SystemRunApprovalBinding;
|
||||||
actualEnvKeys: string[];
|
actualEnvKeys: string[];
|
||||||
}): SystemRunApprovalMatchResult {
|
}): SystemRunApprovalMatchResult {
|
||||||
if (params.expected.version !== 1 || params.actual.version !== 1) {
|
|
||||||
return requestMismatch({
|
|
||||||
expectedVersion: params.expected.version,
|
|
||||||
actualVersion: params.actual.version,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!argvMatches(params.expected.argv, params.actual.argv)) {
|
if (!argvMatches(params.expected.argv, params.actual.argv)) {
|
||||||
return requestMismatch();
|
return requestMismatch();
|
||||||
}
|
}
|
||||||
@@ -191,11 +180,10 @@ export function matchSystemRunApprovalBindingV1(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function missingSystemRunApprovalBindingV1(params: {
|
export function missingSystemRunApprovalBinding(params: {
|
||||||
actualEnvKeys: string[];
|
actualEnvKeys: string[];
|
||||||
}): SystemRunApprovalMatchResult {
|
}): SystemRunApprovalMatchResult {
|
||||||
return requestMismatch({
|
return requestMismatch({
|
||||||
requiredBindingVersion: 1,
|
|
||||||
envKeys: params.actualEnvKeys,
|
envKeys: params.actualEnvKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { SystemRunApprovalPlanV2 } from "./exec-approvals.js";
|
import type { SystemRunApprovalPlan } from "./exec-approvals.js";
|
||||||
import { normalizeSystemRunApprovalPlanV2 } from "./system-run-approval-binding.js";
|
import { normalizeSystemRunApprovalPlan } from "./system-run-approval-binding.js";
|
||||||
import { formatExecCommand, resolveSystemRunCommand } from "./system-run-command.js";
|
import { formatExecCommand, resolveSystemRunCommand } from "./system-run-command.js";
|
||||||
|
|
||||||
type PreparedRunPayload = {
|
type PreparedRunPayload = {
|
||||||
cmdText: string;
|
cmdText: string;
|
||||||
plan: SystemRunApprovalPlanV2;
|
plan: SystemRunApprovalPlan;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SystemRunApprovalRequestContext = {
|
type SystemRunApprovalRequestContext = {
|
||||||
planV2: SystemRunApprovalPlanV2 | null;
|
plan: SystemRunApprovalPlan | null;
|
||||||
commandArgv: string[] | undefined;
|
commandArgv: string[] | undefined;
|
||||||
commandText: string;
|
commandText: string;
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
@@ -19,7 +19,7 @@ type SystemRunApprovalRequestContext = {
|
|||||||
type SystemRunApprovalRuntimeContext =
|
type SystemRunApprovalRuntimeContext =
|
||||||
| {
|
| {
|
||||||
ok: true;
|
ok: true;
|
||||||
planV2: SystemRunApprovalPlanV2 | null;
|
plan: SystemRunApprovalPlan | null;
|
||||||
argv: string[];
|
argv: string[];
|
||||||
cwd: string | null;
|
cwd: string | null;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
@@ -54,7 +54,7 @@ export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayl
|
|||||||
}
|
}
|
||||||
const raw = payload as { cmdText?: unknown; plan?: unknown };
|
const raw = payload as { cmdText?: unknown; plan?: unknown };
|
||||||
const cmdText = normalizeString(raw.cmdText);
|
const cmdText = normalizeString(raw.cmdText);
|
||||||
const plan = normalizeSystemRunApprovalPlanV2(raw.plan);
|
const plan = normalizeSystemRunApprovalPlan(raw.plan);
|
||||||
if (!cmdText || !plan) {
|
if (!cmdText || !plan) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -65,38 +65,38 @@ export function resolveSystemRunApprovalRequestContext(params: {
|
|||||||
host?: unknown;
|
host?: unknown;
|
||||||
command?: unknown;
|
command?: unknown;
|
||||||
commandArgv?: unknown;
|
commandArgv?: unknown;
|
||||||
systemRunPlanV2?: unknown;
|
systemRunPlan?: unknown;
|
||||||
cwd?: unknown;
|
cwd?: unknown;
|
||||||
agentId?: unknown;
|
agentId?: unknown;
|
||||||
sessionKey?: unknown;
|
sessionKey?: unknown;
|
||||||
}): SystemRunApprovalRequestContext {
|
}): SystemRunApprovalRequestContext {
|
||||||
const host = normalizeString(params.host) ?? "";
|
const host = normalizeString(params.host) ?? "";
|
||||||
const planV2 = host === "node" ? normalizeSystemRunApprovalPlanV2(params.systemRunPlanV2) : 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);
|
||||||
return {
|
return {
|
||||||
planV2,
|
plan,
|
||||||
commandArgv: planV2?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
|
commandArgv: plan?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
|
||||||
commandText: planV2 ? (planV2.rawCommand ?? formatExecCommand(planV2.argv)) : fallbackCommand,
|
commandText: plan ? (plan.rawCommand ?? formatExecCommand(plan.argv)) : fallbackCommand,
|
||||||
cwd: planV2?.cwd ?? normalizeString(params.cwd),
|
cwd: plan?.cwd ?? normalizeString(params.cwd),
|
||||||
agentId: planV2?.agentId ?? normalizeString(params.agentId),
|
agentId: plan?.agentId ?? normalizeString(params.agentId),
|
||||||
sessionKey: planV2?.sessionKey ?? normalizeString(params.sessionKey),
|
sessionKey: plan?.sessionKey ?? normalizeString(params.sessionKey),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSystemRunApprovalRuntimeContext(params: {
|
export function resolveSystemRunApprovalRuntimeContext(params: {
|
||||||
planV2?: unknown;
|
plan?: unknown;
|
||||||
command?: unknown;
|
command?: unknown;
|
||||||
rawCommand?: unknown;
|
rawCommand?: unknown;
|
||||||
cwd?: unknown;
|
cwd?: unknown;
|
||||||
agentId?: unknown;
|
agentId?: unknown;
|
||||||
sessionKey?: unknown;
|
sessionKey?: unknown;
|
||||||
}): SystemRunApprovalRuntimeContext {
|
}): SystemRunApprovalRuntimeContext {
|
||||||
const normalizedPlan = normalizeSystemRunApprovalPlanV2(params.planV2 ?? null);
|
const normalizedPlan = normalizeSystemRunApprovalPlan(params.plan ?? null);
|
||||||
if (normalizedPlan) {
|
if (normalizedPlan) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
planV2: normalizedPlan,
|
plan: normalizedPlan,
|
||||||
argv: [...normalizedPlan.argv],
|
argv: [...normalizedPlan.argv],
|
||||||
cwd: normalizedPlan.cwd,
|
cwd: normalizedPlan.cwd,
|
||||||
agentId: normalizedPlan.agentId,
|
agentId: normalizedPlan.agentId,
|
||||||
@@ -113,7 +113,7 @@ export function resolveSystemRunApprovalRuntimeContext(params: {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
planV2: null,
|
plan: null,
|
||||||
argv: command.argv,
|
argv: command.argv,
|
||||||
cwd: normalizeString(params.cwd),
|
cwd: normalizeString(params.cwd),
|
||||||
agentId: normalizeString(params.agentId),
|
agentId: normalizeString(params.agentId),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
analyzeArgvCommand,
|
analyzeArgvCommand,
|
||||||
evaluateExecAllowlist,
|
evaluateExecAllowlist,
|
||||||
evaluateShellAllowlist,
|
evaluateShellAllowlist,
|
||||||
|
resolvePlannedSegmentArgv,
|
||||||
resolveExecApprovals,
|
resolveExecApprovals,
|
||||||
type ExecAllowlistEntry,
|
type ExecAllowlistEntry,
|
||||||
type ExecCommandSegment,
|
type ExecCommandSegment,
|
||||||
@@ -95,7 +96,7 @@ export function resolvePlannedAllowlistArgv(params: {
|
|||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const plannedAllowlistArgv = params.segments[0]?.resolution?.effectiveArgv;
|
const plannedAllowlistArgv = resolvePlannedSegmentArgv(params.segments[0]);
|
||||||
return plannedAllowlistArgv && plannedAllowlistArgv.length > 0 ? plannedAllowlistArgv : null;
|
return plannedAllowlistArgv && plannedAllowlistArgv.length > 0 ? plannedAllowlistArgv : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { SystemRunApprovalPlanV2 } from "../infra/exec-approvals.js";
|
import type { SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
||||||
|
import { resolveCommandResolutionFromArgv } from "../infra/exec-command-resolution.js";
|
||||||
import { sameFileIdentity } from "../infra/file-identity.js";
|
import { sameFileIdentity } from "../infra/file-identity.js";
|
||||||
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
|
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||||
|
|
||||||
@@ -12,22 +13,6 @@ function normalizeString(value: unknown): string | null {
|
|||||||
return trimmed ? trimmed : null;
|
return trimmed ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPathLikeExecutableToken(value: string): boolean {
|
|
||||||
if (!value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (value.startsWith(".") || value.startsWith("/") || value.startsWith("\\")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (value.includes("/") || value.includes("\\")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (process.platform === "win32" && /^[a-zA-Z]:[\\/]/.test(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pathComponentsFromRootSync(targetPath: string): string[] {
|
function pathComponentsFromRootSync(targetPath: string): string[] {
|
||||||
const absolute = path.resolve(targetPath);
|
const absolute = path.resolve(targetPath);
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -71,7 +56,6 @@ function hasMutableSymlinkPathComponentSync(targetPath: string): boolean {
|
|||||||
export function hardenApprovedExecutionPaths(params: {
|
export function hardenApprovedExecutionPaths(params: {
|
||||||
approvedByAsk: boolean;
|
approvedByAsk: boolean;
|
||||||
argv: string[];
|
argv: string[];
|
||||||
shellCommand: string | null;
|
|
||||||
cwd: string | undefined;
|
cwd: string | undefined;
|
||||||
}): { ok: true; argv: string[]; cwd: string | undefined } | { ok: false; message: string } {
|
}): { ok: true; argv: string[]; cwd: string | undefined } | { ok: false; message: string } {
|
||||||
if (!params.approvedByAsk) {
|
if (!params.approvedByAsk) {
|
||||||
@@ -127,38 +111,31 @@ export function hardenApprovedExecutionPaths(params: {
|
|||||||
hardenedCwd = cwdReal;
|
hardenedCwd = cwdReal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.shellCommand !== null || params.argv.length === 0) {
|
if (params.argv.length === 0) {
|
||||||
return { ok: true, argv: params.argv, cwd: hardenedCwd };
|
return { ok: true, argv: params.argv, cwd: hardenedCwd };
|
||||||
}
|
}
|
||||||
|
|
||||||
const argv = [...params.argv];
|
const resolution = resolveCommandResolutionFromArgv(params.argv, hardenedCwd);
|
||||||
const rawExecutable = argv[0] ?? "";
|
const pinnedExecutable = resolution?.resolvedRealPath ?? resolution?.resolvedPath;
|
||||||
if (!isPathLikeExecutableToken(rawExecutable)) {
|
if (!pinnedExecutable) {
|
||||||
return { ok: true, argv, cwd: hardenedCwd };
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = hardenedCwd ?? process.cwd();
|
|
||||||
const candidate = path.isAbsolute(rawExecutable)
|
|
||||||
? rawExecutable
|
|
||||||
: path.resolve(base, rawExecutable);
|
|
||||||
try {
|
|
||||||
argv[0] = fs.realpathSync(candidate);
|
|
||||||
} catch {
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message: "SYSTEM_RUN_DENIED: approval requires a stable executable path",
|
message: "SYSTEM_RUN_DENIED: approval requires a stable executable path",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const argv = [...params.argv];
|
||||||
|
argv[0] = pinnedExecutable;
|
||||||
return { ok: true, argv, cwd: hardenedCwd };
|
return { ok: true, argv, cwd: hardenedCwd };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSystemRunApprovalPlanV2(params: {
|
export function buildSystemRunApprovalPlan(params: {
|
||||||
command?: unknown;
|
command?: unknown;
|
||||||
rawCommand?: unknown;
|
rawCommand?: unknown;
|
||||||
cwd?: unknown;
|
cwd?: unknown;
|
||||||
agentId?: unknown;
|
agentId?: unknown;
|
||||||
sessionKey?: unknown;
|
sessionKey?: unknown;
|
||||||
}): { ok: true; plan: SystemRunApprovalPlanV2; cmdText: string } | { ok: false; message: string } {
|
}): { ok: true; plan: SystemRunApprovalPlan; cmdText: string } | { ok: false; message: string } {
|
||||||
const command = resolveSystemRunCommand({
|
const command = resolveSystemRunCommand({
|
||||||
command: params.command,
|
command: params.command,
|
||||||
rawCommand: params.rawCommand,
|
rawCommand: params.rawCommand,
|
||||||
@@ -172,7 +149,6 @@ export function buildSystemRunApprovalPlanV2(params: {
|
|||||||
const hardening = hardenApprovedExecutionPaths({
|
const hardening = hardenApprovedExecutionPaths({
|
||||||
approvedByAsk: true,
|
approvedByAsk: true,
|
||||||
argv: command.argv,
|
argv: command.argv,
|
||||||
shellCommand: command.shellCommand,
|
|
||||||
cwd: normalizeString(params.cwd) ?? undefined,
|
cwd: normalizeString(params.cwd) ?? undefined,
|
||||||
});
|
});
|
||||||
if (!hardening.ok) {
|
if (!hardening.ok) {
|
||||||
@@ -181,7 +157,6 @@ export function buildSystemRunApprovalPlanV2(params: {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
plan: {
|
plan: {
|
||||||
version: 2,
|
|
||||||
argv: hardening.argv,
|
argv: hardening.argv,
|
||||||
cwd: hardening.cwd ?? null,
|
cwd: hardening.cwd ?? null,
|
||||||
rawCommand: command.cmdText.trim() || null,
|
rawCommand: command.cmdText.trim() || null,
|
||||||
|
|||||||
@@ -54,15 +54,22 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
|||||||
ask?: "off" | "on-miss" | "always";
|
ask?: "off" | "on-miss" | "always";
|
||||||
approved?: boolean;
|
approved?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const runCommand = vi.fn(async () => ({
|
const runCommand = vi.fn(
|
||||||
success: true,
|
async (
|
||||||
stdout: "local-ok",
|
_command: string[],
|
||||||
stderr: "",
|
_cwd?: string,
|
||||||
timedOut: false,
|
_env?: Record<string, string>,
|
||||||
truncated: false,
|
_timeoutMs?: number,
|
||||||
exitCode: 0,
|
) => ({
|
||||||
error: null,
|
success: true,
|
||||||
}));
|
stdout: "local-ok",
|
||||||
|
stderr: "",
|
||||||
|
timedOut: false,
|
||||||
|
truncated: false,
|
||||||
|
exitCode: 0,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const runViaMacAppExecHost = vi.fn(async () => params.runViaResponse ?? null);
|
const runViaMacAppExecHost = vi.fn(async () => params.runViaResponse ?? null);
|
||||||
const sendInvokeResult = vi.fn(async () => {});
|
const sendInvokeResult = vi.fn(async () => {});
|
||||||
const sendExecFinishedEvent = vi.fn(async () => {});
|
const sendExecFinishedEvent = vi.fn(async () => {});
|
||||||
@@ -192,7 +199,10 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(runCommand).toHaveBeenCalledWith(["tr", "a", "b"], undefined, undefined, undefined);
|
const runArgs = vi.mocked(runCommand).mock.calls[0]?.[0] as string[] | undefined;
|
||||||
|
expect(runArgs).toBeDefined();
|
||||||
|
expect(runArgs?.[0]).toMatch(/(^|[/\\])tr$/);
|
||||||
|
expect(runArgs?.slice(1)).toEqual(["a", "b"]);
|
||||||
expect(sendInvokeResult).toHaveBeenCalledWith(
|
expect(sendInvokeResult).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -217,6 +227,132 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.runIf(process.platform !== "win32")(
|
||||||
|
"pins PATH-token executable to canonical path for approval-based runs",
|
||||||
|
async () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-path-pin-"));
|
||||||
|
const binDir = path.join(tmp, "bin");
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const link = path.join(binDir, "poccmd");
|
||||||
|
fs.symlinkSync("/bin/echo", link);
|
||||||
|
const expected = fs.realpathSync(link);
|
||||||
|
const oldPath = process.env.PATH;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||||
|
try {
|
||||||
|
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||||
|
preferMacAppExecHost: false,
|
||||||
|
command: ["poccmd", "-n", "SAFE"],
|
||||||
|
approved: true,
|
||||||
|
security: "full",
|
||||||
|
ask: "off",
|
||||||
|
});
|
||||||
|
expect(runCommand).toHaveBeenCalledWith(
|
||||||
|
[expected, "-n", "SAFE"],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(sendInvokeResult).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
ok: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (oldPath === undefined) {
|
||||||
|
delete process.env.PATH;
|
||||||
|
} else {
|
||||||
|
process.env.PATH = oldPath;
|
||||||
|
}
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.runIf(process.platform !== "win32")(
|
||||||
|
"pins PATH-token executable to canonical path for allowlist runs",
|
||||||
|
async () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-allowlist-path-pin-"));
|
||||||
|
const binDir = path.join(tmp, "bin");
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
const link = path.join(binDir, "poccmd");
|
||||||
|
fs.symlinkSync("/bin/echo", link);
|
||||||
|
const expected = fs.realpathSync(link);
|
||||||
|
const oldPath = process.env.PATH;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||||
|
const runCommand = vi.fn(async () => ({
|
||||||
|
success: true,
|
||||||
|
stdout: "local-ok",
|
||||||
|
stderr: "",
|
||||||
|
timedOut: false,
|
||||||
|
truncated: false,
|
||||||
|
exitCode: 0,
|
||||||
|
error: null,
|
||||||
|
}));
|
||||||
|
const sendInvokeResult = vi.fn(async () => {});
|
||||||
|
const sendNodeEvent = vi.fn(async () => {});
|
||||||
|
try {
|
||||||
|
await withTempApprovalsHome({
|
||||||
|
approvals: {
|
||||||
|
version: 1,
|
||||||
|
defaults: {
|
||||||
|
security: "allowlist",
|
||||||
|
ask: "off",
|
||||||
|
askFallback: "deny",
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
main: {
|
||||||
|
allowlist: [{ pattern: link }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
run: async () => {
|
||||||
|
await handleSystemRunInvoke({
|
||||||
|
client: {} as never,
|
||||||
|
params: {
|
||||||
|
command: ["poccmd", "-n", "SAFE"],
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
},
|
||||||
|
skillBins: {
|
||||||
|
current: async () => [],
|
||||||
|
},
|
||||||
|
execHostEnforced: false,
|
||||||
|
execHostFallbackAllowed: true,
|
||||||
|
resolveExecSecurity: () => "allowlist",
|
||||||
|
resolveExecAsk: () => "off",
|
||||||
|
isCmdExeInvocation: () => false,
|
||||||
|
sanitizeEnv: () => undefined,
|
||||||
|
runCommand,
|
||||||
|
runViaMacAppExecHost: vi.fn(async () => null),
|
||||||
|
sendNodeEvent,
|
||||||
|
buildExecEventPayload: (payload) => payload,
|
||||||
|
sendInvokeResult,
|
||||||
|
sendExecFinishedEvent: vi.fn(async () => {}),
|
||||||
|
preferMacAppExecHost: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(runCommand).toHaveBeenCalledWith(
|
||||||
|
[expected, "-n", "SAFE"],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(sendInvokeResult).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
ok: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (oldPath === undefined) {
|
||||||
|
delete process.env.PATH;
|
||||||
|
} else {
|
||||||
|
process.env.PATH = oldPath;
|
||||||
|
}
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.runIf(process.platform !== "win32")(
|
it.runIf(process.platform !== "win32")(
|
||||||
"denies approval-based execution when cwd is a symlink",
|
"denies approval-based execution when cwd is a symlink",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ async function sendSystemRunDenied(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { formatSystemRunAllowlistMissMessage } from "./exec-policy.js";
|
export { formatSystemRunAllowlistMissMessage } from "./exec-policy.js";
|
||||||
export { buildSystemRunApprovalPlanV2 } from "./invoke-system-run-plan.js";
|
export { buildSystemRunApprovalPlan } from "./invoke-system-run-plan.js";
|
||||||
|
|
||||||
async function parseSystemRunPhase(
|
async function parseSystemRunPhase(
|
||||||
opts: HandleSystemRunInvokeOptions,
|
opts: HandleSystemRunInvokeOptions,
|
||||||
@@ -300,7 +300,6 @@ async function evaluateSystemRunPolicyPhase(
|
|||||||
const hardenedPaths = hardenApprovedExecutionPaths({
|
const hardenedPaths = hardenApprovedExecutionPaths({
|
||||||
approvedByAsk: policy.approvedByAsk,
|
approvedByAsk: policy.approvedByAsk,
|
||||||
argv: parsed.argv,
|
argv: parsed.argv,
|
||||||
shellCommand: parsed.shellCommand,
|
|
||||||
cwd: parsed.cwd,
|
cwd: parsed.cwd,
|
||||||
});
|
});
|
||||||
if (!hardenedPaths.ok) {
|
if (!hardenedPaths.ok) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "../infra/exec-host.js";
|
} from "../infra/exec-host.js";
|
||||||
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
|
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
|
||||||
import { runBrowserProxyCommand } from "./invoke-browser.js";
|
import { runBrowserProxyCommand } from "./invoke-browser.js";
|
||||||
import { buildSystemRunApprovalPlanV2, handleSystemRunInvoke } from "./invoke-system-run.js";
|
import { buildSystemRunApprovalPlan, handleSystemRunInvoke } from "./invoke-system-run.js";
|
||||||
import type {
|
import type {
|
||||||
ExecEventPayload,
|
ExecEventPayload,
|
||||||
RunResult,
|
RunResult,
|
||||||
@@ -429,7 +429,7 @@ export async function handleInvoke(
|
|||||||
agentId?: unknown;
|
agentId?: unknown;
|
||||||
sessionKey?: unknown;
|
sessionKey?: unknown;
|
||||||
}>(frame.paramsJSON);
|
}>(frame.paramsJSON);
|
||||||
const prepared = buildSystemRunApprovalPlanV2(params);
|
const prepared = buildSystemRunApprovalPlan(params);
|
||||||
if (!prepared.ok) {
|
if (!prepared.ok) {
|
||||||
await sendErrorResult(client, frame, "INVALID_REQUEST", prepared.message);
|
await sendErrorResult(client, frame, "INVALID_REQUEST", prepared.message);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"cases": [
|
"cases": [
|
||||||
{
|
{
|
||||||
"name": "v1 matches when env key order changes",
|
"name": "binding matches when env key order changes",
|
||||||
"request": {
|
"request": {
|
||||||
"host": "node",
|
"host": "node",
|
||||||
"command": "git diff",
|
"command": "git diff",
|
||||||
"bindingV1": {
|
"binding": {
|
||||||
"argv": ["git", "diff"],
|
"argv": ["git", "diff"],
|
||||||
"cwd": null,
|
"cwd": null,
|
||||||
"agentId": null,
|
"agentId": null,
|
||||||
@@ -25,11 +25,11 @@
|
|||||||
"expected": { "ok": true }
|
"expected": { "ok": true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "v1 rejects env mismatch",
|
"name": "binding rejects env mismatch",
|
||||||
"request": {
|
"request": {
|
||||||
"host": "node",
|
"host": "node",
|
||||||
"command": "git diff",
|
"command": "git diff",
|
||||||
"bindingV1": {
|
"binding": {
|
||||||
"argv": ["git", "diff"],
|
"argv": ["git", "diff"],
|
||||||
"cwd": null,
|
"cwd": null,
|
||||||
"agentId": null,
|
"agentId": null,
|
||||||
@@ -49,11 +49,11 @@
|
|||||||
"expected": { "ok": false, "code": "APPROVAL_ENV_MISMATCH" }
|
"expected": { "ok": false, "code": "APPROVAL_ENV_MISMATCH" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "v1 rejects unbound env overrides",
|
"name": "binding rejects unbound env overrides",
|
||||||
"request": {
|
"request": {
|
||||||
"host": "node",
|
"host": "node",
|
||||||
"command": "git diff",
|
"command": "git diff",
|
||||||
"bindingV1": {
|
"binding": {
|
||||||
"argv": ["git", "diff"],
|
"argv": ["git", "diff"],
|
||||||
"cwd": null,
|
"cwd": null,
|
||||||
"agentId": null,
|
"agentId": null,
|
||||||
@@ -89,12 +89,12 @@
|
|||||||
"expected": { "ok": false, "code": "APPROVAL_REQUEST_MISMATCH" }
|
"expected": { "ok": false, "code": "APPROVAL_REQUEST_MISMATCH" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "v1 stays authoritative when legacy command text diverges",
|
"name": "binding stays authoritative when legacy command text diverges",
|
||||||
"request": {
|
"request": {
|
||||||
"host": "node",
|
"host": "node",
|
||||||
"command": "echo STALE",
|
"command": "echo STALE",
|
||||||
"commandArgv": ["echo", "STALE"],
|
"commandArgv": ["echo", "STALE"],
|
||||||
"bindingV1": {
|
"binding": {
|
||||||
"argv": ["echo", "SAFE"],
|
"argv": ["echo", "SAFE"],
|
||||||
"cwd": null,
|
"cwd": null,
|
||||||
"agentId": null,
|
"agentId": null,
|
||||||
|
|||||||
Reference in New Issue
Block a user