






















@@ -6,6 +6,7 @@ import {
66import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js";
77import {
88type ExecApprovalsFile,
9+type ExecAllowlistEntry,
910type ExecAsk,
1011type ExecSecurity,
1112type SystemRunApprovalPlan,
@@ -19,7 +20,11 @@ import {
1920parsePreparedSystemRunPayload,
2021type 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";
2328import { normalizeNullableString } from "../shared/string-coerce.js";
2429import { resolveSafeTimeoutDelayMs } from "../utils/timer-delay.js";
2530import 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+88134export function shouldSkipNodeApprovalPrepare(params: {
89135hostSecurity: ExecSecurity;
90136hostAsk: ExecAsk;
@@ -332,22 +378,76 @@ export async function analyzeNodeApprovalRequirement(params: {
332378hostSecurity: ExecSecurity;
333379hostAsk: ExecAsk;
334380}): Promise<NodeApprovalAnalysis> {
381+const approvalCommand = params.prepared.rawCommand;
382+const approvalCwd = params.prepared.cwd ?? params.request.workdir;
335383const baseAllowlistEval = evaluateShellAllowlist({
336-command: params.request.command,
384+command: approvalCommand,
337385allowlist: [],
338386safeBins: new Set(),
339-cwd: params.request.workdir,
387+cwd: approvalCwd,
340388env: params.request.env,
341389platform: params.target.platform,
342390trustedSafeBinDirs: 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);
344442let analysisOk = baseAllowlistEval.analysisOk;
345443let allowlistSatisfied = false;
346444let durableApprovalSatisfied = false;
347445let nodeApprovalsFileKnown = false;
348446const inlineEvalHit =
349447params.request.strictInlineEval === true
350- ? detectPolicyInlineEval(baseAllowlistEval.segments)
448+ ? (policyCommandEvals
449+.map((entry) => detectPolicyInlineEval(entry.allowlistEval.segments))
450+.find((hit) => hit !== null) ?? null)
351451 : null;
352452if (inlineEvalHit) {
353453params.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;
359465const 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");
366474if (
367475(params.hostAsk === "always" ||
368476params.hostSecurity === "allowlist" ||
@@ -383,27 +491,48 @@ export async function analyzeNodeApprovalRequirement(params: {
383491nodeApprovalsFileKnown = true;
384492const resolved = resolveExecApprovalsFromFile({
385493file: approvalsFile as ExecApprovalsFile,
386-agentId: params.request.agentId,
494+agentId: params.prepared.agentId,
387495overrides: { 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,
421550autoReviewArgv:
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}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。