mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(exec): require explicit safe-bin profiles
This commit is contained in:
@@ -124,6 +124,10 @@ are treated as allowlisted on nodes (macOS node or headless node host). This use
|
||||
`tools.exec.safeBins` defines a small list of **stdin-only** binaries (for example `jq`)
|
||||
that can run in allowlist mode **without** explicit allowlist entries. Safe bins reject
|
||||
positional file args and path-like tokens, so they can only operate on the incoming stream.
|
||||
Treat this as a narrow fast-path for stream filters, not a general trust list.
|
||||
Do **not** add interpreter or runtime binaries (for example `python3`, `node`, `ruby`, `bash`, `sh`, `zsh`) to `safeBins`.
|
||||
If a command can evaluate code, execute subcommands, or read files by design, prefer explicit allowlist entries and keep approval prompts enabled.
|
||||
Custom safe bins must define an explicit profile in `tools.exec.safeBinProfiles.<bin>`.
|
||||
Validation is deterministic from argv shape only (no host filesystem existence checks), which
|
||||
prevents file-existence oracle behavior from allow/deny differences.
|
||||
File-oriented options are denied for default safe bins (for example `sort -o`, `sort --output`,
|
||||
@@ -165,6 +169,42 @@ their non-stdin workflows.
|
||||
For `grep` in safe-bin mode, provide the pattern with `-e`/`--regexp`; positional pattern form is
|
||||
rejected so file operands cannot be smuggled as ambiguous positionals.
|
||||
|
||||
### Safe bins versus allowlist
|
||||
|
||||
| Topic | `tools.exec.safeBins` | Allowlist (`exec-approvals.json`) |
|
||||
| ---------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
|
||||
| Goal | Auto-allow narrow stdin filters | Explicitly trust specific executables |
|
||||
| Match type | Executable name + safe-bin argv policy | Resolved executable path glob pattern |
|
||||
| Argument scope | Restricted by safe-bin profile and literal-token rules | Path match only; arguments are otherwise your responsibility |
|
||||
| Typical examples | `jq`, `head`, `tail`, `wc` | `python3`, `node`, `ffmpeg`, custom CLIs |
|
||||
| Best use | Low-risk text transforms in pipelines | Any tool with broader behavior or side effects |
|
||||
|
||||
Configuration location:
|
||||
|
||||
- `safeBins` comes from config (`tools.exec.safeBins` or per-agent `agents.list[].tools.exec.safeBins`).
|
||||
- `safeBinProfiles` comes from config (`tools.exec.safeBinProfiles` or per-agent `agents.list[].tools.exec.safeBinProfiles`). Per-agent profile keys override global keys.
|
||||
- allowlist entries live in host-local `~/.openclaw/exec-approvals.json` under `agents.<id>.allowlist` (or via Control UI / `openclaw approvals allowlist ...`).
|
||||
|
||||
Custom profile example:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
exec: {
|
||||
safeBins: ["jq", "myfilter"],
|
||||
safeBinProfiles: {
|
||||
myfilter: {
|
||||
minPositional: 0,
|
||||
maxPositional: 0,
|
||||
allowedValueFlags: ["-n", "--limit"],
|
||||
deniedFlags: ["-f", "--file", "-c", "--command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Control UI editing
|
||||
|
||||
Use the **Control UI → Nodes → Exec approvals** card to edit defaults, per‑agent
|
||||
|
||||
@@ -55,6 +55,7 @@ Notes:
|
||||
- `tools.exec.node` (default: unset)
|
||||
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only).
|
||||
- `tools.exec.safeBins`: stdin-only safe binaries that can run without explicit allowlist entries. For behavior details, see [Safe bins](/tools/exec-approvals#safe-bins-stdin-only).
|
||||
- `tools.exec.safeBinProfiles`: optional custom argv policy per safe bin (`minPositional`, `maxPositional`, `allowedValueFlags`, `deniedFlags`).
|
||||
|
||||
Example:
|
||||
|
||||
@@ -126,6 +127,16 @@ allowlisted or a safe bin. Chaining (`;`, `&&`, `||`) and redirections are rejec
|
||||
allowlist mode unless every top-level segment satisfies the allowlist (including safe bins).
|
||||
Redirections remain unsupported.
|
||||
|
||||
Use the two controls for different jobs:
|
||||
|
||||
- `tools.exec.safeBins`: small, stdin-only stream filters.
|
||||
- `tools.exec.safeBinProfiles`: explicit argv policy for custom safe bins.
|
||||
- allowlist: explicit trust for executable paths.
|
||||
|
||||
Do not treat `safeBins` as a generic allowlist, and do not add interpreter/runtime binaries (for example `python3`, `node`, `ruby`, `bash`). If you need those, use explicit allowlist entries and keep approval prompts enabled.
|
||||
|
||||
For full policy details and examples, see [Exec approvals](/tools/exec-approvals#safe-bins-stdin-only) and [Safe bins versus allowlist](/tools/exec-approvals#safe-bins-versus-allowlist).
|
||||
|
||||
## Examples
|
||||
|
||||
Foreground:
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveAllowAlwaysPatterns,
|
||||
resolveExecApprovals,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
|
||||
import { markBackgrounded, tail } from "./bash-process-registry.js";
|
||||
import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ export type ProcessGatewayAllowlistParams = {
|
||||
security: ExecSecurity;
|
||||
ask: ExecAsk;
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
scopeKey?: string;
|
||||
@@ -69,6 +71,7 @@ export async function processGatewayAllowlist(
|
||||
command: params.command,
|
||||
allowlist: approvals.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.workdir,
|
||||
env: params.env,
|
||||
platform: process.platform,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ExecAsk, ExecHost, ExecSecurity } from "../infra/exec-approvals.js";
|
||||
import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js";
|
||||
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
||||
|
||||
export type ExecToolDefaults = {
|
||||
@@ -8,6 +9,7 @@ export type ExecToolDefaults = {
|
||||
node?: string;
|
||||
pathPrepend?: string[];
|
||||
safeBins?: string[];
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
agentId?: string;
|
||||
backgroundMs?: number;
|
||||
timeoutSec?: number;
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { type ExecHost, maxAsk, minSecurity, resolveSafeBins } from "../infra/exec-approvals.js";
|
||||
import { resolveSafeBinProfiles } from "../infra/exec-safe-bin-policy.js";
|
||||
import { getTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
|
||||
import {
|
||||
getShellPathFromLoginShell,
|
||||
@@ -164,6 +165,13 @@ export function createExecTool(
|
||||
: 1800;
|
||||
const defaultPathPrepend = normalizePathPrepend(defaults?.pathPrepend);
|
||||
const safeBins = resolveSafeBins(defaults?.safeBins);
|
||||
const safeBinProfiles = resolveSafeBinProfiles(defaults?.safeBinProfiles);
|
||||
const unprofiledSafeBins = Array.from(safeBins).filter((entry) => !safeBinProfiles[entry]);
|
||||
if (unprofiledSafeBins.length > 0) {
|
||||
logInfo(
|
||||
`exec: ignoring unprofiled safeBins entries (${unprofiledSafeBins.toSorted().join(", ")}); use allowlist or define tools.exec.safeBinProfiles.<bin>`,
|
||||
);
|
||||
}
|
||||
const trustedSafeBinDirs = getTrustedSafeBinDirs();
|
||||
const notifyOnExit = defaults?.notifyOnExit !== false;
|
||||
const notifyOnExitEmptySuccess = defaults?.notifyOnExitEmptySuccess === true;
|
||||
@@ -404,6 +412,7 @@ export function createExecTool(
|
||||
security,
|
||||
ask,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
agentId,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
scopeKey: defaults?.scopeKey,
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ExecApprovalsResolved } from "../infra/exec-approvals.js";
|
||||
import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
|
||||
const bundledPluginsDirSnapshot = captureEnv(["OPENCLAW_BUNDLED_PLUGINS_DIR"]);
|
||||
@@ -86,6 +87,7 @@ type ExecTool = {
|
||||
async function createSafeBinsExecTool(params: {
|
||||
tmpPrefix: string;
|
||||
safeBins: string[];
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
files?: Array<{ name: string; contents: string }>;
|
||||
}): Promise<{ tmpDir: string; execTool: ExecTool }> {
|
||||
const { createOpenClawCodingTools } = await import("./pi-tools.js");
|
||||
@@ -101,6 +103,7 @@ async function createSafeBinsExecTool(params: {
|
||||
security: "allowlist",
|
||||
ask: "off",
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -139,6 +142,9 @@ describe("createOpenClawCodingTools safeBins", () => {
|
||||
{
|
||||
tmpPrefix: "openclaw-safe-bins-",
|
||||
safeBins: ["echo"],
|
||||
safeBinProfiles: {
|
||||
echo: { maxPositional: 1 },
|
||||
},
|
||||
},
|
||||
async ({ tmpDir, execTool }) => {
|
||||
const marker = `safe-bins-${Date.now()}`;
|
||||
@@ -155,6 +161,23 @@ describe("createOpenClawCodingTools safeBins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects unprofiled custom safe-bin entries", async () => {
|
||||
await withSafeBinsExecTool(
|
||||
{
|
||||
tmpPrefix: "openclaw-safe-bins-unprofiled-",
|
||||
safeBins: ["echo"],
|
||||
},
|
||||
async ({ tmpDir, execTool }) => {
|
||||
await expect(
|
||||
execTool.execute("call1", {
|
||||
command: "echo hello",
|
||||
workdir: tmpDir,
|
||||
}),
|
||||
).rejects.toThrow("exec denied: allowlist miss");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not allow env var expansion to smuggle file args via safeBins", async () => {
|
||||
await withSafeBinsExecTool(
|
||||
{
|
||||
|
||||
@@ -97,6 +97,13 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
||||
const globalExec = cfg?.tools?.exec;
|
||||
const agentExec =
|
||||
cfg && params.agentId ? resolveAgentConfig(cfg, params.agentId)?.tools?.exec : undefined;
|
||||
const mergedSafeBinProfiles =
|
||||
globalExec?.safeBinProfiles || agentExec?.safeBinProfiles
|
||||
? {
|
||||
...globalExec?.safeBinProfiles,
|
||||
...agentExec?.safeBinProfiles,
|
||||
}
|
||||
: undefined;
|
||||
return {
|
||||
host: agentExec?.host ?? globalExec?.host,
|
||||
security: agentExec?.security ?? globalExec?.security,
|
||||
@@ -104,6 +111,7 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
||||
node: agentExec?.node ?? globalExec?.node,
|
||||
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
||||
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
||||
safeBinProfiles: mergedSafeBinProfiles,
|
||||
backgroundMs: agentExec?.backgroundMs ?? globalExec?.backgroundMs,
|
||||
timeoutSec: agentExec?.timeoutSec ?? globalExec?.timeoutSec,
|
||||
approvalRunningNoticeMs:
|
||||
@@ -361,6 +369,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
node: options?.exec?.node ?? execConfig.node,
|
||||
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
||||
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
||||
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
|
||||
agentId,
|
||||
cwd: workspaceRoot,
|
||||
allowBackground,
|
||||
|
||||
@@ -94,6 +94,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
|
||||
"tools.exec.safeBins":
|
||||
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
|
||||
"tools.exec.safeBinProfiles":
|
||||
"Optional per-binary safe-bin profiles (positional limits + allowed/denied flags).",
|
||||
"tools.fs.workspaceOnly":
|
||||
"Restrict filesystem tools (read/write/edit/apply_patch) to the workspace directory (default: false).",
|
||||
"tools.sessions.visibility":
|
||||
|
||||
@@ -92,6 +92,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.exec.node": "Exec Node Binding",
|
||||
"tools.exec.pathPrepend": "Exec PATH Prepend",
|
||||
"tools.exec.safeBins": "Exec Safe Bins",
|
||||
"tools.exec.safeBinProfiles": "Exec Safe Bin Profiles",
|
||||
"tools.message.allowCrossContextSend": "Allow Cross-Context Messaging",
|
||||
"tools.message.crossContext.allowWithinProvider": "Allow Cross-Context (Same Provider)",
|
||||
"tools.message.crossContext.allowAcrossProviders": "Allow Cross-Context (Across Providers)",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ChatType } from "../channels/chat-type.js";
|
||||
import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js";
|
||||
import type { AgentElevatedAllowFromConfig, SessionSendPolicyAction } from "./types.base.js";
|
||||
|
||||
export type MediaUnderstandingScopeMatch = {
|
||||
@@ -190,6 +191,8 @@ export type ExecToolConfig = {
|
||||
pathPrepend?: string[];
|
||||
/** Safe stdin-only binaries that can run without allowlist entries. */
|
||||
safeBins?: string[];
|
||||
/** Optional custom safe-bin profiles for entries in tools.exec.safeBins. */
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
/** Default time (ms) before an exec command auto-backgrounds. */
|
||||
backgroundMs?: number;
|
||||
/** Default timeout (seconds) before auto-killing exec commands. */
|
||||
|
||||
@@ -337,6 +337,15 @@ const ToolExecApplyPatchSchema = z
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
const ToolExecSafeBinProfileSchema = z
|
||||
.object({
|
||||
minPositional: z.number().int().nonnegative().optional(),
|
||||
maxPositional: z.number().int().nonnegative().optional(),
|
||||
allowedValueFlags: z.array(z.string()).optional(),
|
||||
deniedFlags: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const ToolExecBaseShape = {
|
||||
host: z.enum(["sandbox", "gateway", "node"]).optional(),
|
||||
security: z.enum(["deny", "allowlist", "full"]).optional(),
|
||||
@@ -344,6 +353,7 @@ const ToolExecBaseShape = {
|
||||
node: z.string().optional(),
|
||||
pathPrepend: z.array(z.string()).optional(),
|
||||
safeBins: z.array(z.string()).optional(),
|
||||
safeBinProfiles: z.record(z.string(), ToolExecSafeBinProfileSchema).optional(),
|
||||
backgroundMs: z.number().int().positive().optional(),
|
||||
timeoutSec: z.number().int().positive().optional(),
|
||||
cleanupMs: z.number().int().positive().optional(),
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "./exec-approvals-analysis.js";
|
||||
import type { ExecAllowlistEntry } from "./exec-approvals.js";
|
||||
import {
|
||||
SAFE_BIN_GENERIC_PROFILE,
|
||||
SAFE_BIN_PROFILES,
|
||||
type SafeBinProfile,
|
||||
validateSafeBinArgv,
|
||||
@@ -41,7 +40,6 @@ export function isSafeBinUsage(params: {
|
||||
platform?: string | null;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
safeBinGenericProfile?: SafeBinProfile;
|
||||
isTrustedSafeBinPathFn?: typeof isTrustedSafeBinPath;
|
||||
}): boolean {
|
||||
// Windows host exec uses PowerShell, which has different parsing/expansion rules.
|
||||
@@ -75,8 +73,10 @@ export function isSafeBinUsage(params: {
|
||||
}
|
||||
const argv = params.argv.slice(1);
|
||||
const safeBinProfiles = params.safeBinProfiles ?? SAFE_BIN_PROFILES;
|
||||
const genericSafeBinProfile = params.safeBinGenericProfile ?? SAFE_BIN_GENERIC_PROFILE;
|
||||
const profile = safeBinProfiles[execName] ?? genericSafeBinProfile;
|
||||
const profile = safeBinProfiles[execName];
|
||||
if (!profile) {
|
||||
return false;
|
||||
}
|
||||
return validateSafeBinArgv(argv, profile);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ function evaluateSegments(
|
||||
params: {
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
platform?: string | null;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -122,6 +123,7 @@ function evaluateSegments(
|
||||
argv: segment.argv,
|
||||
resolution: segment.resolution,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
});
|
||||
@@ -147,6 +149,7 @@ export function evaluateExecAllowlist(params: {
|
||||
analysis: ExecCommandAnalysis;
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
platform?: string | null;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -165,6 +168,7 @@ export function evaluateExecAllowlist(params: {
|
||||
const result = evaluateSegments(chainSegments, {
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -184,6 +188,7 @@ export function evaluateExecAllowlist(params: {
|
||||
const result = evaluateSegments(params.analysis.segments, {
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -354,6 +359,7 @@ export function evaluateShellAllowlist(params: {
|
||||
command: string;
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -384,6 +390,7 @@ export function evaluateShellAllowlist(params: {
|
||||
analysis,
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -419,6 +426,7 @@ export function evaluateShellAllowlist(params: {
|
||||
analysis,
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
type ExecAllowlistEntry,
|
||||
type ExecApprovalsFile,
|
||||
} from "./exec-approvals.js";
|
||||
import { SAFE_BIN_PROFILE_FIXTURES, SAFE_BIN_PROFILES } from "./exec-safe-bin-policy.js";
|
||||
import {
|
||||
SAFE_BIN_PROFILE_FIXTURES,
|
||||
SAFE_BIN_PROFILES,
|
||||
resolveSafeBinProfiles,
|
||||
} from "./exec-safe-bin-policy.js";
|
||||
|
||||
function makePathEnv(binDir: string): NodeJS.ProcessEnv {
|
||||
if (process.platform !== "win32") {
|
||||
@@ -798,6 +802,53 @@ describe("exec approvals safe bins", () => {
|
||||
expect(defaults.has("grep")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not auto-allow unprofiled safe-bin entries", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const result = evaluateShellAllowlist({
|
||||
command: "python3 -c \"print('owned')\"",
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["python3"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
expect(result.analysisOk).toBe(true);
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
});
|
||||
|
||||
it("allows caller-defined custom safe-bin profiles", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const safeBinProfiles = resolveSafeBinProfiles({
|
||||
echo: {
|
||||
maxPositional: 1,
|
||||
},
|
||||
});
|
||||
const allow = isSafeBinUsage({
|
||||
argv: ["echo", "hello"],
|
||||
resolution: {
|
||||
rawExecutable: "echo",
|
||||
resolvedPath: "/bin/echo",
|
||||
executableName: "echo",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["echo"]),
|
||||
safeBinProfiles,
|
||||
});
|
||||
const deny = isSafeBinUsage({
|
||||
argv: ["echo", "hello", "world"],
|
||||
resolution: {
|
||||
rawExecutable: "echo",
|
||||
resolvedPath: "/bin/echo",
|
||||
executableName: "echo",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["echo"]),
|
||||
safeBinProfiles,
|
||||
});
|
||||
expect(allow).toBe(true);
|
||||
expect(deny).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks sort output flags independent of file existence", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
@@ -37,6 +37,8 @@ export type SafeBinProfileFixture = {
|
||||
deniedFlags?: readonly string[];
|
||||
};
|
||||
|
||||
export type SafeBinProfileFixtures = Readonly<Record<string, SafeBinProfileFixture>>;
|
||||
|
||||
const NO_FLAGS: ReadonlySet<string> = new Set();
|
||||
|
||||
const toFlagSet = (flags?: readonly string[]): ReadonlySet<string> => {
|
||||
@@ -63,8 +65,6 @@ function compileSafeBinProfiles(
|
||||
) as Record<string, SafeBinProfile>;
|
||||
}
|
||||
|
||||
export const SAFE_BIN_GENERIC_PROFILE_FIXTURE: SafeBinProfileFixture = {};
|
||||
|
||||
export const SAFE_BIN_PROFILE_FIXTURES: Record<string, SafeBinProfileFixture> = {
|
||||
jq: {
|
||||
maxPositional: 1,
|
||||
@@ -184,11 +184,49 @@ export const SAFE_BIN_PROFILE_FIXTURES: Record<string, SafeBinProfileFixture> =
|
||||
},
|
||||
};
|
||||
|
||||
export const SAFE_BIN_GENERIC_PROFILE = compileSafeBinProfile(SAFE_BIN_GENERIC_PROFILE_FIXTURE);
|
||||
|
||||
export const SAFE_BIN_PROFILES: Record<string, SafeBinProfile> =
|
||||
compileSafeBinProfiles(SAFE_BIN_PROFILE_FIXTURES);
|
||||
|
||||
function normalizeSafeBinProfileName(raw: string): string | null {
|
||||
const name = raw.trim().toLowerCase();
|
||||
return name.length > 0 ? name : null;
|
||||
}
|
||||
|
||||
function normalizeSafeBinProfileFixtures(
|
||||
fixtures?: SafeBinProfileFixtures | null,
|
||||
): Record<string, SafeBinProfileFixture> {
|
||||
const normalized: Record<string, SafeBinProfileFixture> = {};
|
||||
if (!fixtures) {
|
||||
return normalized;
|
||||
}
|
||||
for (const [rawName, fixture] of Object.entries(fixtures)) {
|
||||
const name = normalizeSafeBinProfileName(rawName);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
normalized[name] = {
|
||||
minPositional: fixture.minPositional,
|
||||
maxPositional: fixture.maxPositional,
|
||||
allowedValueFlags: fixture.allowedValueFlags,
|
||||
deniedFlags: fixture.deniedFlags,
|
||||
};
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function resolveSafeBinProfiles(
|
||||
fixtures?: SafeBinProfileFixtures | null,
|
||||
): Record<string, SafeBinProfile> {
|
||||
const normalizedFixtures = normalizeSafeBinProfileFixtures(fixtures);
|
||||
if (Object.keys(normalizedFixtures).length === 0) {
|
||||
return SAFE_BIN_PROFILES;
|
||||
}
|
||||
return {
|
||||
...SAFE_BIN_PROFILES,
|
||||
...compileSafeBinProfiles(normalizedFixtures),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSafeBinDeniedFlags(
|
||||
fixtures: Readonly<Record<string, SafeBinProfileFixture>> = SAFE_BIN_PROFILE_FIXTURES,
|
||||
): Record<string, string[]> {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type ExecSecurity,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js";
|
||||
import { resolveSafeBinProfiles } from "../infra/exec-safe-bin-policy.js";
|
||||
import { getTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
|
||||
import { sanitizeSystemRunEnvOverrides } from "../infra/host-env-security.js";
|
||||
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||
@@ -116,6 +117,10 @@ export async function handleSystemRunInvoke(opts: {
|
||||
});
|
||||
const env = opts.sanitizeEnv(envOverrides);
|
||||
const safeBins = resolveSafeBins(agentExec?.safeBins ?? cfg.tools?.exec?.safeBins);
|
||||
const safeBinProfiles = resolveSafeBinProfiles({
|
||||
...cfg.tools?.exec?.safeBinProfiles,
|
||||
...agentExec?.safeBinProfiles,
|
||||
});
|
||||
const trustedSafeBinDirs = getTrustedSafeBinDirs();
|
||||
const bins = autoAllowSkills ? await opts.skillBins.current() : new Set<string>();
|
||||
let analysisOk = false;
|
||||
@@ -127,6 +132,7 @@ export async function handleSystemRunInvoke(opts: {
|
||||
command: shellCommand,
|
||||
allowlist: approvals.allowlist,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
cwd: opts.params.cwd ?? undefined,
|
||||
env,
|
||||
trustedSafeBinDirs,
|
||||
@@ -145,6 +151,7 @@ export async function handleSystemRunInvoke(opts: {
|
||||
analysis,
|
||||
allowlist: approvals.allowlist,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
cwd: opts.params.cwd ?? undefined,
|
||||
trustedSafeBinDirs,
|
||||
skillBins: bins,
|
||||
|
||||
Reference in New Issue
Block a user