From ddac6f73e549a217ac82afe834871df842d99081 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 01:46:30 +0100 Subject: [PATCH] fix(approvals): accept allowlist metadata --- CHANGELOG.md | 1 + docs/tools/exec-approvals.md | 2 + .../exec-approvals-validators.test.ts | 75 +++++++++++++++++++ src/gateway/protocol/schema/exec-approvals.ts | 2 + ui/src/ui/controllers/exec-approvals.ts | 3 + 5 files changed, 83 insertions(+) create mode 100644 src/gateway/protocol/exec-approvals-validators.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 133530152ee..795bf46c3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Fixes - macOS Gateway: write launchd services with a state-dir `WorkingDirectory`, use a durable state-dir temp path instead of freezing macOS session `TMPDIR`, create that temp directory before bootstrap, and label abort-shaped launchd exits as `SIGABRT/abort` in status output. Fixes #53679 and #70223; refs #71848. Thanks @dlturock, @stammi922, and @palladius. +- Exec approvals: accept runtime-owned `source: "allow-always"` and `commandText` allowlist metadata in gateway and node approval-set payloads so Control UI round-trips no longer fail with `unexpected property 'source'`. Fixes #60000; carries forward #60064. Thanks @sd1471123, @sharkqwy, and @luoyanglang. - Exec/node: skip approval-plan preparation for full-trust `host=node` runs so interpreter and script commands no longer fail with `SYSTEM_RUN_DENIED: approval cannot safely bind` when effective policy is `security=full` and `ask=off`. Fixes #48457 and duplicate #69251. Thanks @ajtran303, @jaserNo1, @Blakeshannon, @lesliefag, and @AvIsBeastMC. - Exec/node: synthesize a local approval plan when a paired node advertises `system.run` without `system.run.prepare`, unblocking approval-required `host=node` exec on current macOS companion nodes while preserving remote prepare for node hosts that support it. Fixes #37591 and duplicate #66839; carries forward #69725. Thanks @soloclz. - Memory/QMD: prefer QMD's `--mask` collection pattern flag so root memory indexing stays scoped to `MEMORY.md` instead of widening to every markdown file in the workspace. Thanks @codex. diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index e26a80cee99..2354be48130 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -99,6 +99,8 @@ Example schema: { "id": "B0C8C0B3-2C2D-4F8A-9A3C-5A4B3C2D1E0F", "pattern": "~/Projects/**/bin/rg", + "source": "allow-always", + "commandText": "rg -n TODO", "lastUsedAt": 1737150000000, "lastUsedCommand": "rg -n TODO", "lastResolvedPath": "/Users/user/Projects/.../bin/rg" diff --git a/src/gateway/protocol/exec-approvals-validators.test.ts b/src/gateway/protocol/exec-approvals-validators.test.ts new file mode 100644 index 00000000000..13bb01ded9f --- /dev/null +++ b/src/gateway/protocol/exec-approvals-validators.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; +import { validateExecApprovalsNodeSetParams, validateExecApprovalsSetParams } from "./index.js"; + +describe("exec approvals protocol validators", () => { + it("accepts runtime-owned allowlist metadata on gateway and node set payloads", () => { + const file = { + version: 1 as const, + agents: { + main: { + allowlist: [ + { + id: "entry-1", + pattern: "cmd:allow-always:abcdef", + source: "allow-always" as const, + commandText: "python3 -c 'print(123)'", + argPattern: "-c *", + lastUsedAt: 1775154056736, + lastUsedCommand: "python3 -c 'print(123)'", + lastResolvedPath: "/usr/bin/python3", + }, + ], + }, + }, + }; + + expect(validateExecApprovalsSetParams({ file, baseHash: "abc123" })).toBe(true); + expect( + validateExecApprovalsNodeSetParams({ + nodeId: "node-1", + file, + baseHash: "abc123", + }), + ).toBe(true); + }); + + it("rejects unknown allowlist metadata", () => { + expect( + validateExecApprovalsSetParams({ + file: { + version: 1, + agents: { + main: { + allowlist: [ + { + pattern: "/usr/bin/python3", + source: "unknown-source", + }, + ], + }, + }, + }, + baseHash: "abc123", + }), + ).toBe(false); + + expect( + validateExecApprovalsSetParams({ + file: { + version: 1, + agents: { + main: { + allowlist: [ + { + pattern: "/usr/bin/python3", + randomMetadata: true, + }, + ], + }, + }, + }, + baseHash: "abc123", + }), + ).toBe(false); + }); +}); diff --git a/src/gateway/protocol/schema/exec-approvals.ts b/src/gateway/protocol/schema/exec-approvals.ts index 7806c0e92ca..b94d674aac9 100644 --- a/src/gateway/protocol/schema/exec-approvals.ts +++ b/src/gateway/protocol/schema/exec-approvals.ts @@ -5,6 +5,8 @@ export const ExecApprovalsAllowlistEntrySchema = Type.Object( { id: Type.Optional(NonEmptyString), pattern: Type.String(), + source: Type.Optional(Type.Literal("allow-always")), + commandText: Type.Optional(Type.String()), argPattern: Type.Optional(Type.String()), lastUsedAt: Type.Optional(Type.Integer({ minimum: 0 })), lastUsedCommand: Type.Optional(Type.String()), diff --git a/ui/src/ui/controllers/exec-approvals.ts b/ui/src/ui/controllers/exec-approvals.ts index 104035f9ce8..c69f02e3a3a 100644 --- a/ui/src/ui/controllers/exec-approvals.ts +++ b/ui/src/ui/controllers/exec-approvals.ts @@ -11,6 +11,9 @@ export type ExecApprovalsDefaults = { export type ExecApprovalsAllowlistEntry = { id?: string; pattern: string; + source?: "allow-always"; + commandText?: string; + argPattern?: string; lastUsedAt?: number; lastUsedCommand?: string; lastResolvedPath?: string;