惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
量子位
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
爱范儿
爱范儿
V
Visual Studio Blog
雷峰网
雷峰网
T
Tailwind CSS Blog
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
有赞技术团队
有赞技术团队
博客园 - 叶小钗
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
U
Unit 42
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
Y
Y Combinator Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(exec): bind node auto-review to prepared plans · open...
steipete · 2026-05-30 · via Recent Commits to openclaw:main

@@ -6,6 +6,7 @@ import {

66

import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js";

77

import {

88

type ExecApprovalsFile,

9+

type ExecAllowlistEntry,

910

type ExecAsk,

1011

type ExecSecurity,

1112

type SystemRunApprovalPlan,

@@ -19,7 +20,11 @@ import {

1920

parsePreparedSystemRunPayload,

2021

type PreparedRunExecPolicy,

2122

} from "../infra/system-run-approval-context.js";

22-

import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";

23+

import {

24+

extractShellCommandFromArgv,

25+

formatExecCommand,

26+

resolveSystemRunCommandRequest,

27+

} from "../infra/system-run-command.js";

2328

import { normalizeNullableString } from "../shared/string-coerce.js";

2429

import { resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js";

2530

import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js";

@@ -85,6 +90,47 @@ function resolveNodeRunTimeoutMs(runTimeoutSec: number): number {

8590

: 0;

8691

}

879293+

type NodePolicyCommandEval = {

94+

command: string;

95+

cwd: string | undefined;

96+

allowlistEval: ReturnType<typeof evaluateShellAllowlist>;

97+

};

98+99+

function hasExactCommandDurableApproval(params: {

100+

allowlist: readonly ExecAllowlistEntry[];

101+

commandText: string;

102+

}): boolean {

103+

const normalizedCommand = params.commandText.trim();

104+

if (!normalizedCommand) {

105+

return false;

106+

}

107+

const commandPattern = `=command:${crypto

108+

.createHash("sha256")

109+

.update(normalizedCommand)

110+

.digest("hex")

111+

.slice(0, 16)}`;

112+

return params.allowlist.some(

113+

(entry) =>

114+

entry.source === "allow-always" &&

115+

(entry.pattern === commandPattern ||

116+

(typeof entry.commandText === "string" && entry.commandText.trim() === normalizedCommand)),

117+

);

118+

}

119+120+

function extractPreparedNodeShellPayload(argv: readonly string[]): string | null {

121+

const extracted = extractShellCommandFromArgv([...argv]);

122+

if (extracted) {

123+

return extracted;

124+

}

125+

const executable = argv[0]?.split(/[\\/]/).pop()?.toLowerCase();

126+

const flag = argv[1]?.trim();

127+

const payload = argv[2]?.trim();

128+

if (argv.length === 3 && executable === "sh" && flag === "-lc" && payload) {

129+

return payload;

130+

}

131+

return null;

132+

}

133+88134

export function shouldSkipNodeApprovalPrepare(params: {

89135

hostSecurity: ExecSecurity;

90136

hostAsk: ExecAsk;

@@ -332,22 +378,76 @@ export async function analyzeNodeApprovalRequirement(params: {

332378

hostSecurity: ExecSecurity;

333379

hostAsk: ExecAsk;

334380

}): Promise<NodeApprovalAnalysis> {

381+

const approvalCommand = params.prepared.rawCommand;

382+

const approvalCwd = params.prepared.cwd ?? params.request.workdir;

335383

const baseAllowlistEval = evaluateShellAllowlist({

336-

command: params.request.command,

384+

command: approvalCommand,

337385

allowlist: [],

338386

safeBins: new Set(),

339-

cwd: params.request.workdir,

387+

cwd: approvalCwd,

340388

env: params.request.env,

341389

platform: params.target.platform,

342390

trustedSafeBinDirs: params.request.trustedSafeBinDirs,

343391

});

392+

const bindingCommandEvals: NodePolicyCommandEval[] = [

393+

{

394+

command: approvalCommand,

395+

cwd: approvalCwd,

396+

allowlistEval: baseAllowlistEval,

397+

},

398+

];

399+

const addCommandEval = (

400+

entries: NodePolicyCommandEval[],

401+

command: string | null | undefined,

402+

cwd: string | undefined,

403+

) => {

404+

const normalizedCommand = command?.trim();

405+

if (!normalizedCommand) {

406+

return;

407+

}

408+

if (entries.some((entry) => entry.command.trim() === normalizedCommand && entry.cwd === cwd)) {

409+

return;

410+

}

411+

entries.push({

412+

command: normalizedCommand,

413+

cwd,

414+

allowlistEval: evaluateShellAllowlist({

415+

command: normalizedCommand,

416+

allowlist: [],

417+

safeBins: new Set(),

418+

cwd,

419+

env: params.request.env,

420+

platform: params.target.platform,

421+

trustedSafeBinDirs: params.request.trustedSafeBinDirs,

422+

}),

423+

});

424+

};

425+

const preparedCommand = resolveSystemRunCommandRequest({

426+

command: params.prepared.argv,

427+

rawCommand: params.prepared.rawCommand,

428+

});

429+

const preparedShellPayload =

430+

extractPreparedNodeShellPayload(params.prepared.argv) ??

431+

(preparedCommand.ok ? preparedCommand.shellPayload : null);

432+

addCommandEval(bindingCommandEvals, preparedShellPayload, approvalCwd);

433+

const autoReviewBindingCommand = preparedShellPayload?.trim() || approvalCommand;

434+

const autoReviewBindingEval =

435+

bindingCommandEvals.find(

436+

(entry) =>

437+

entry.command.trim() === autoReviewBindingCommand.trim() && entry.cwd === approvalCwd,

438+

)?.allowlistEval ?? baseAllowlistEval;

439+

const policyCommandEvals = [...bindingCommandEvals];

440+

addCommandEval(policyCommandEvals, params.prepared.plan.commandPreview, approvalCwd);

441+

addCommandEval(policyCommandEvals, params.request.command, params.request.workdir);

344442

let analysisOk = baseAllowlistEval.analysisOk;

345443

let allowlistSatisfied = false;

346444

let durableApprovalSatisfied = false;

347445

let nodeApprovalsFileKnown = false;

348446

const inlineEvalHit =

349447

params.request.strictInlineEval === true

350-

? detectPolicyInlineEval(baseAllowlistEval.segments)

448+

? (policyCommandEvals

449+

.map((entry) => detectPolicyInlineEval(entry.allowlistEval.segments))

450+

.find((hit) => hit !== null) ?? null)

351451

: null;

352452

if (inlineEvalHit) {

353453

params.request.warnings.push(

@@ -356,13 +456,21 @@ export async function analyzeNodeApprovalRequirement(params: {

356456

)}.`,

357457

);

358458

}

459+

const suppressionCommandEvals =

460+

preparedShellPayload && preparedShellPayload.trim().length > 0

461+

? policyCommandEvals.filter(

462+

(entry) => entry.command.trim() !== approvalCommand.trim() || entry.cwd !== approvalCwd,

463+

)

464+

: policyCommandEvals;

359465

const requiresSecurityAuditSuppressionApproval =

360-

commandRequiresSecurityAuditSuppressionApproval({

361-

command: params.request.command,

362-

cwd: params.request.workdir,

363-

env: params.request.env,

364-

segments: baseAllowlistEval.segments,

365-

}) && !(params.hostSecurity === "full" && params.hostAsk === "off");

466+

suppressionCommandEvals.some((entry) =>

467+

commandRequiresSecurityAuditSuppressionApproval({

468+

command: entry.command,

469+

cwd: entry.cwd,

470+

env: params.request.env,

471+

segments: entry.allowlistEval.segments,

472+

}),

473+

) && !(params.hostSecurity === "full" && params.hostAsk === "off");

366474

if (

367475

(params.hostAsk === "always" ||

368476

params.hostSecurity === "allowlist" ||

@@ -383,27 +491,48 @@ export async function analyzeNodeApprovalRequirement(params: {

383491

nodeApprovalsFileKnown = true;

384492

const resolved = resolveExecApprovalsFromFile({

385493

file: approvalsFile as ExecApprovalsFile,

386-

agentId: params.request.agentId,

494+

agentId: params.prepared.agentId,

387495

overrides: { security: "full" },

388496

});

389497

// Allowlist-only precheck; safe bins are node-local and may diverge.

390-

const allowlistEval = evaluateShellAllowlist({

391-

command: params.request.command,

392-

allowlist: resolved.allowlist,

393-

safeBins: new Set(),

394-

cwd: params.request.workdir,

395-

env: params.request.env,

396-

platform: params.target.platform,

397-

trustedSafeBinDirs: params.request.trustedSafeBinDirs,

398-

});

399-

durableApprovalSatisfied = hasDurableExecApproval({

400-

analysisOk: allowlistEval.analysisOk,

401-

segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,

402-

allowlist: resolved.allowlist,

403-

commandText: params.prepared.rawCommand,

498+

// POSIX node transport wraps commands, so mirror node policy by

499+

// accepting either the prepared wrapper or its semantic inner command.

500+

const allowlistEvals = bindingCommandEvals.map((entry) => {

501+

const allowlistEval = evaluateShellAllowlist({

502+

command: entry.command,

503+

allowlist: resolved.allowlist,

504+

safeBins: new Set(),

505+

cwd: entry.cwd,

506+

env: params.request.env,

507+

platform: params.target.platform,

508+

trustedSafeBinDirs: params.request.trustedSafeBinDirs,

509+

});

510+

return {

511+

command: entry.command,

512+

allowlistEligible:

513+

!preparedShellPayload || entry.command.trim() === preparedShellPayload.trim(),

514+

exactDurableApprovalSatisfied: hasExactCommandDurableApproval({

515+

allowlist: resolved.allowlist,

516+

commandText: entry.command,

517+

}),

518+

allowlistEval,

519+

durableApprovalSatisfied: hasDurableExecApproval({

520+

analysisOk: allowlistEval.analysisOk,

521+

segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,

522+

allowlist: resolved.allowlist,

523+

commandText: entry.command,

524+

}),

525+

};

404526

});

405-

allowlistSatisfied = allowlistEval.allowlistSatisfied;

406-

analysisOk = allowlistEval.analysisOk;

527+

durableApprovalSatisfied = allowlistEvals.some(

528+

(entry) =>

529+

entry.durableApprovalSatisfied &&

530+

(entry.allowlistEligible || entry.exactDurableApprovalSatisfied),

531+

);

532+

allowlistSatisfied = allowlistEvals.some(

533+

(entry) => entry.allowlistEligible && entry.allowlistEval.allowlistSatisfied,

534+

);

535+

analysisOk = allowlistEvals.some((entry) => entry.allowlistEval.analysisOk);

407536

}

408537

} catch {

409538

// Fall back to requiring approval if node approvals cannot be fetched.

@@ -419,10 +548,10 @@ export async function analyzeNodeApprovalRequirement(params: {

419548

inlineEvalHit,

420549

requiresSecurityAuditSuppressionApproval,

421550

autoReviewArgv:

422-

baseAllowlistEval.segments.length === 1 &&

423-

(baseAllowlistEval.segments[0]?.raw === undefined ||

424-

baseAllowlistEval.segments[0].raw.trim() === params.request.command.trim())

425-

? baseAllowlistEval.segments[0].argv

551+

autoReviewBindingEval.segments.length === 1 &&

552+

(autoReviewBindingEval.segments[0]?.raw === undefined ||

553+

autoReviewBindingEval.segments[0].raw.trim() === autoReviewBindingCommand.trim())

554+

? autoReviewBindingEval.segments[0].argv

426555

: undefined,

427556

};

428557

}