



























@@ -44,6 +44,20 @@ const evaluateShellAllowlistMock = vi.hoisted(() =>
4444segmentAllowlistEntries: [{ pattern: "/usr/bin/echo", source: "allow-always" }],
4545})),
4646);
47+const analyzeShellCommandMock = vi.hoisted(() =>
48+vi.fn((params: { command: string }) => ({
49+ok: true,
50+segments: params.command
51+.split(";")
52+.map((part) => part.trim())
53+.filter(Boolean)
54+.map((part) => ({
55+raw: part,
56+resolution: null,
57+argv: part.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, "")),
58+})),
59+})),
60+);
4761const hasDurableExecApprovalMock = vi.hoisted(() => vi.fn(() => true));
4862const buildEnforcedShellCommandMock = vi.hoisted(() =>
4963vi.fn((): { ok: boolean; reason?: string; command?: string } => ({
@@ -86,6 +100,7 @@ const detectInterpreterInlineEvalArgvMock = vi.hoisted(() =>
8610087101vi.mock("../infra/exec-approvals.js", () => ({
88102evaluateShellAllowlist: evaluateShellAllowlistMock,
103+analyzeShellCommand: analyzeShellCommandMock,
89104hasDurableExecApproval: hasDurableExecApprovalMock,
90105buildEnforcedShellCommand: buildEnforcedShellCommandMock,
91106requiresExecApproval: vi.fn(() => false),
@@ -198,6 +213,19 @@ describe("processGatewayAllowlist", () => {
198213segments: [{ resolution: null, argv: ["echo", "ok"] }],
199214segmentAllowlistEntries: [{ pattern: "/usr/bin/echo", source: "allow-always" }],
200215});
216+analyzeShellCommandMock.mockReset();
217+analyzeShellCommandMock.mockImplementation((params: { command: string }) => ({
218+ok: true,
219+segments: params.command
220+.split(";")
221+.map((part) => part.trim())
222+.filter(Boolean)
223+.map((part) => ({
224+raw: part,
225+resolution: null,
226+argv: part.split(/\s+/).map((token) => token.replace(/^['"]|['"]$/g, "")),
227+})),
228+}));
201229hasDurableExecApprovalMock.mockReset();
202230hasDurableExecApprovalMock.mockReturnValue(true);
203231buildEnforcedShellCommandMock.mockReset();
@@ -325,6 +353,203 @@ describe("processGatewayAllowlist", () => {
325353expect(result).toEqual({ execCommandOverride: undefined });
326354});
327355356+it("requires approval for security audit suppression edits unless yolo mode is active", async () => {
357+resolveExecHostApprovalContextMock.mockReturnValue({
358+approvals: { allowlist: [], file: { version: 1, agents: {} } },
359+hostSecurity: "full",
360+hostAsk: "on-miss",
361+askFallback: "deny",
362+});
363+364+const result = await runGatewayAllowlist({
365+command: "openclaw config set security.audit.suppressions '[]'",
366+security: "full",
367+ask: "on-miss",
368+});
369+370+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
371+expect(result.pendingResult?.details.status).toBe("approval-pending");
372+});
373+374+it("does not require approval for security audit suppression edits in yolo mode", async () => {
375+resolveExecHostApprovalContextMock.mockReturnValue({
376+approvals: { allowlist: [], file: { version: 1, agents: {} } },
377+hostSecurity: "full",
378+hostAsk: "off",
379+askFallback: "deny",
380+});
381+382+await runGatewayAllowlist({
383+command: "openclaw config set security.audit.suppressions '[]'",
384+security: "full",
385+ask: "off",
386+});
387+388+expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
389+});
390+391+it("does not require suppression edit approval for read-only suppression inspection", async () => {
392+evaluateShellAllowlistMock.mockReturnValue({
393+allowlistMatches: [],
394+analysisOk: true,
395+allowlistSatisfied: true,
396+segments: [
397+{ resolution: null, argv: ["openclaw", "config", "get", "security.audit.suppressions"] },
398+],
399+segmentAllowlistEntries: [],
400+});
401+resolveExecHostApprovalContextMock.mockReturnValue({
402+approvals: { allowlist: [], file: { version: 1, agents: {} } },
403+hostSecurity: "full",
404+hostAsk: "on-miss",
405+askFallback: "deny",
406+});
407+408+await runGatewayAllowlist({
409+command: "openclaw config get security.audit.suppressions",
410+security: "full",
411+ask: "on-miss",
412+});
413+414+expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
415+});
416+417+it("does not require suppression edit approval for profile-scoped read-only inspection", async () => {
418+evaluateShellAllowlistMock.mockReturnValue({
419+allowlistMatches: [],
420+analysisOk: true,
421+allowlistSatisfied: true,
422+segments: [
423+{
424+resolution: null,
425+argv: ["openclaw", "--profile", "rescue", "config", "get", "security.audit.suppressions"],
426+},
427+],
428+segmentAllowlistEntries: [],
429+});
430+resolveExecHostApprovalContextMock.mockReturnValue({
431+approvals: { allowlist: [], file: { version: 1, agents: {} } },
432+hostSecurity: "full",
433+hostAsk: "on-miss",
434+askFallback: "deny",
435+});
436+437+await runGatewayAllowlist({
438+command: "openclaw --profile rescue config get security.audit.suppressions",
439+security: "full",
440+ask: "on-miss",
441+});
442+443+expect(createAndRegisterDefaultExecApprovalRequestMock).not.toHaveBeenCalled();
444+});
445+446+it("requires suppression edit approval when a mutating segment follows read-only inspection", async () => {
447+evaluateShellAllowlistMock.mockReturnValue({
448+allowlistMatches: [],
449+analysisOk: true,
450+allowlistSatisfied: true,
451+segments: [
452+{ resolution: null, argv: ["openclaw", "config", "get", "security.audit.suppressions"] },
453+{
454+resolution: null,
455+argv: ["openclaw", "config", "set", "security.audit.suppressions", "[]"],
456+},
457+],
458+segmentAllowlistEntries: [],
459+});
460+resolveExecHostApprovalContextMock.mockReturnValue({
461+approvals: { allowlist: [], file: { version: 1, agents: {} } },
462+hostSecurity: "full",
463+hostAsk: "on-miss",
464+askFallback: "deny",
465+});
466+467+const result = await runGatewayAllowlist({
468+command:
469+"openclaw config get security.audit.suppressions; openclaw config set security.audit.suppressions '[]'",
470+security: "full",
471+ask: "on-miss",
472+});
473+474+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
475+expect(result.pendingResult?.details.status).toBe("approval-pending");
476+});
477+478+it("requires suppression edit approval when allowlist analysis only returns a read-only prefix", async () => {
479+evaluateShellAllowlistMock.mockReturnValue({
480+allowlistMatches: [],
481+analysisOk: true,
482+allowlistSatisfied: false,
483+segments: [
484+{ resolution: null, argv: ["openclaw", "config", "get", "security.audit.suppressions"] },
485+],
486+segmentAllowlistEntries: [],
487+});
488+resolveExecHostApprovalContextMock.mockReturnValue({
489+approvals: { allowlist: [], file: { version: 1, agents: {} } },
490+hostSecurity: "full",
491+hostAsk: "on-miss",
492+askFallback: "deny",
493+});
494+495+const result = await runGatewayAllowlist({
496+command:
497+"openclaw config get security.audit.suppressions; openclaw config set security.audit.suppressions '[]'",
498+security: "full",
499+ask: "on-miss",
500+});
501+502+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
503+expect(result.pendingResult?.details.status).toBe("approval-pending");
504+});
505+506+it("requires suppression edit approval when a heredoc patch follows read-only inspection", async () => {
507+evaluateShellAllowlistMock.mockReturnValue({
508+allowlistMatches: [],
509+analysisOk: true,
510+allowlistSatisfied: false,
511+segments: [
512+{
513+resolution: null,
514+argv: ["openclaw", "config", "get", "security.audit.suppressions"],
515+},
516+],
517+segmentAllowlistEntries: [],
518+});
519+analyzeShellCommandMock.mockReturnValueOnce({
520+ok: true,
521+segments: [
522+{
523+raw: "openclaw config get security.audit.suppressions",
524+resolution: null,
525+argv: ["openclaw", "config", "get", "security.audit.suppressions"],
526+},
527+{
528+raw: "openclaw config patch --stdin <<'EOF'",
529+resolution: null,
530+argv: ["openclaw", "config", "patch", "--stdin"],
531+},
532+],
533+});
534+resolveExecHostApprovalContextMock.mockReturnValue({
535+approvals: { allowlist: [], file: { version: 1, agents: {} } },
536+hostSecurity: "full",
537+hostAsk: "on-miss",
538+askFallback: "deny",
539+});
540+541+const result = await runGatewayAllowlist({
542+command: `openclaw config get security.audit.suppressions; openclaw config patch --stdin <<'EOF'
543+{"security":{"audit":{"suppressions":[]}}}
544+EOF`,
545+security: "full",
546+ask: "on-miss",
547+});
548+549+expect(createAndRegisterDefaultExecApprovalRequestMock).toHaveBeenCalledTimes(1);
550+expect(result.pendingResult?.details.status).toBe("approval-pending");
551+});
552+328553it("keeps denying allowlist misses when durable trust does not match", async () => {
329554evaluateShellAllowlistMock.mockReturnValue({
330555allowlistMatches: [],
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。