Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 78 additions & 2 deletions packages/cli/src/commands/review/lib/sandboxed-exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
// happens when there is no runtime at all.

import { describe, it, expect, afterEach, vi } from 'vitest';
import { join, sep } from 'node:path';
import { dirname, join, sep } from 'node:path';
import {
existsSync,
readFileSync,
mkdirSync,
writeFileSync,
mkdtempSync,
Expand All @@ -22,6 +23,8 @@ import {
symlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { CLI_VERSION } from '../../../generated/git-commit.js';
import { fileURLToPath } from 'node:url';
import type { spawnSync } from 'node:child_process';
import { isolateOperatorReviewSettings } from './test-utils.js';
import * as environment from '../../../config/environment.js';
Expand Down Expand Up @@ -917,6 +920,79 @@ describe('reviewSandboxImage', () => {
expect(reviewSandboxImage({ QWEN_REVIEW_SANDBOX_IMAGE: 'mine:1' })).toBe(
'mine:1',
);
expect(reviewSandboxImage({})).toContain('sandbox');
// The operator's own sandbox image, if they configured one for
// `qwen --sandbox`, rather than ignoring it and pulling a second one.
expect(reviewSandboxImage({ QWEN_SANDBOX_IMAGE: 'theirs:2' })).toBe(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test sets two override variables at once, so any reordering of the chain survives the whole suite. Verified: with QWEN_REVIEW_SANDBOX_IMAGE=mine:1 and QWEN_SANDBOX_IMAGE=theirs:2 both set, clean code returns mine:1; moving the pick('QWEN_SANDBOX_IMAGE') line above the review-specific overrides returns theirs:2 (expected mine:1, received theirs:2) while every existing test still passes. If such a reorder ships, an operator's QWEN_SANDBOX_IMAGE silently outranks a repository's QWEN_REVIEW_SANDBOX_IMAGE toolchain override, and the review runs in an image missing the toolchain the repository declared it needs, breaking or skewing the reviewed build. Add one assertion with both set (and, if cheap, the QWEN_CODE_CUSTOM_SANDBOX_IMAGE pair):

expect(
  reviewSandboxImage({
    QWEN_REVIEW_SANDBOX_IMAGE: 'mine:1',
    QWEN_SANDBOX_IMAGE: 'theirs:2',
  }),
).toBe('mine:1');
中文说明

没有任何测试同时设置两个覆盖变量,因此链上的任意重排都能通过整套测试。已验证:同时设置 QWEN_REVIEW_SANDBOX_IMAGE=mine:1QWEN_SANDBOX_IMAGE=theirs:2 时,干净代码返回 mine:1;把 pick('QWEN_SANDBOX_IMAGE') 这一行移到两个审查专用覆盖之上会返回 theirs:2(期望 mine:1,实际收到 theirs:2),而现有测试全部通过。如果这种重排被合入,操作者的 QWEN_SANDBOX_IMAGE 会静默压过仓库声明的 QWEN_REVIEW_SANDBOX_IMAGE 工具链覆盖,审查将运行在缺少仓库所需工具链的镜像里,导致被审查的构建失败或结果偏差。建议补一条同时设置两个变量的断言(代码见上方英文部分;成本允许的话把 QWEN_CODE_CUSTOM_SANDBOX_IMAGE 的配对也加上)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

'theirs:2',
);
// ORDER, with both set at once — the only way a reordering of the chain
// can be seen. Reviewed-repository-specific beats the operator's general
// one, because the review override exists for a toolchain the repository
// declared it needs; the other way round runs the build in an image
// missing it.
expect(
reviewSandboxImage({
QWEN_REVIEW_SANDBOX_IMAGE: 'mine:1',
QWEN_SANDBOX_IMAGE: 'theirs:2',
}),
).toBe('mine:1');
expect(
reviewSandboxImage({
QWEN_CODE_CUSTOM_SANDBOX_IMAGE: 'custom:3',
QWEN_SANDBOX_IMAGE: 'theirs:2',
}),
).toBe('custom:3');
expect(
reviewSandboxImage({
QWEN_REVIEW_SANDBOX_IMAGE: 'mine:1',
QWEN_CODE_CUSTOM_SANDBOX_IMAGE: 'custom:3',
}),
).toBe('mine:1');
});

it('consults the manifest, and falls back to a name that exists', () => {
// These two cannot be told apart by their VALUE: `DEFAULT_IMAGE`'s tag is
// `CLI_VERSION`, generated from the same manifest version, so today the
// fallback string-equals the manifest field. Deleting the manifest lookup
// entirely therefore leaves the pin below green. Injecting the reader is
// what makes the mechanism visible at all.
expect(reviewSandboxImage({}, () => 'manifest-image:test')).toBe(
'manifest-image:test',
);
// ...and when the manifest cannot be found — the unusual install layout
// the literal exists for — the argv still names something that resolves.
// This is the branch the defect this PR fixes lived in: with no coverage,
// reverting it to an unpullable name ships green.
const fallback = reviewSandboxImage({}, () => undefined);
expect(fallback).toBe(`ghcr.io/qwenlm/qwen-code:${CLI_VERSION}`);
expect(fallback).not.toContain('/sandbox');
expect(fallback).not.toContain(':latest');
});

it('defaults to the image this CLI actually ships with', () => {
// Pinned against the MANIFEST, not against a spelling. The assertion this
// replaces was `toContain('sandbox')`, which is how a default of
// `ghcr.io/qwenlm/qwen-code/sandbox:latest` shipped: it contains the word,
// it is not the CLI's image, and it does not resolve at all — an anonymous
// manifest request answers 403 where the real one answers 200, so every
// command of an opted-in review failed at image pull. Nineteen rounds of
// argv-level review could not see it because no container was ever
// started. The real image happens NOT to contain "sandbox", so the old
// assertion would fail on the correct value and pass on the broken one.
const manifest = JSON.parse(
readFileSync(
join(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'..',
'package.json',
),
'utf8',
),
) as { config?: { sandboxImageUri?: string } };
expect(manifest.config?.sandboxImageUri).toBeTruthy();
expect(reviewSandboxImage({})).toBe(manifest.config?.sandboxImageUri);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This pin cannot detect the removal or disablement of cliSandboxImage() — the core mechanism this PR adds — because the fallback DEFAULT_IMAGE currently evaluates to the exact same string as the manifest field (ghcr.io/qwenlm/qwen-code:0.22.0; CLI_VERSION is generated from the root package version). Deleting cliSandboxImage() || from the chain, stubbing it to return undefined, or reading a wrong field key all keep the suite green: the result falls through to DEFAULT_IMAGE, which string-equals the manifest. Verified by probe: mocking read-package-up to return manifest-image:test, the assertion passes on clean code and fails on the deletion mutant (expected manifest-image:test, received ghcr.io/qwenlm/qwen-code:0.22.0) with the existing suite still green. The two literals are maintained separately — once they diverge (a version bump landing around a field edit, an image repo rename), review silently runs a different image than qwen --sandbox. Add a mocked variant whose value the fallback can never equal, keeping this integration-style test alongside (mind the module-level manifestImage cache — use vi.resetModules() + dynamic import):

vi.mock('read-package-up', () => ({
  readPackageUpSync: () => ({
    packageJson: { config: { sandboxImageUri: 'manifest-image:test' } },
  }),
}));
// with module isolation:
expect(reviewSandboxImage({})).toBe('manifest-image:test');
中文说明

这条钉住语句无法检测 cliSandboxImage()(本 PR 的核心机制)被删除或禁用:兜底值 DEFAULT_IMAGE 目前的求值结果与 manifest 字段的字符串完全相同(ghcr.io/qwenlm/qwen-code:0.22.0CLI_VERSION 由根包版本生成)。因此,把 cliSandboxImage() || 从链上删掉、让它返回 undefined、或读错字段名,整套测试仍然是绿的:结果会落到 DEFAULT_IMAGE,与 manifest 字符串相等。探针验证:把 read-package-up mock 成返回 manifest-image:test 后,该断言在干净代码上通过,在删除变异体上失败(期望 manifest-image:test,实际收到 ghcr.io/qwenlm/qwen-code:0.22.0),而现有套件保持绿色。这两个字面量是分开维护的——一旦将来不一致(版本升级与字段修改交错落地、镜像仓库改名),审查就会静默运行与 qwen --sandbox 不同的镜像。建议增加一个 mock 变体,其值是兜底字面量永远不可能等于的,同时保留现有这条集成式测试(注意 manifestImage 是模块级缓存,需要 vi.resetModules() + 动态 import 隔离;代码见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

});
});
90 changes: 80 additions & 10 deletions packages/cli/src/commands/review/lib/sandboxed-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,26 @@
import { spawnSync } from 'node:child_process';
import { realpathSync } from 'node:fs';
import { basename, dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { operatorReviewSettings } from './review-settings.js';
import { REVIEW_TMP_DIR } from './paths.js';
import { redirectedAncestor } from './worktree.js';
import { CUSTOM_SANDBOX_IMAGE_ENV_VAR } from '../../../utils/processUtils.js';
import { isFileSourcedEnvKey } from '../../../config/environment.js';
import { readPackageUpSync } from 'read-package-up';
import { CLI_VERSION } from '../../../generated/git-commit.js';

/**
* The fallback when neither override names an image: the published sandbox
* image for this CLI line. Pinned by tag rather than digest on purpose — a
* digest would go stale in a file nobody updates, and the override exists for
* anyone who needs reproducibility.
* Last resort only: the real default is the CLI's own `config.sandboxImageUri`
* — see `cliSandboxImage`. This literal covers the case where the package
* manifest cannot be found at all (an unusual install layout), so the argv
* still names something that exists.
*
* Versioned rather than floating on `:latest`, and by tag rather than digest —
* a digest would go stale in a file nobody updates, and the overrides exist
* for anyone who needs reproducibility.
*/
const DEFAULT_IMAGE = 'ghcr.io/qwenlm/qwen-code/sandbox:latest';
const DEFAULT_IMAGE = `ghcr.io/qwenlm/qwen-code:${CLI_VERSION}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The last-resort literal this diff changes has zero test coverage: no test makes cliSandboxImage() fail (it reads the real filesystem via readPackageUpSync with no injection seam), and the manifest always resolves in the test layout, so this branch never executes. A verified mutant shows the hole: reverting DEFAULT_IMAGE to the broken ghcr.io/qwenlm/qwen-code/sandbox:latest leaves the entire new suite green — only a probe forcing the fallback (mocking read-package-up to return undefined) catches it (expected ghcr.io/qwenlm/qwen-code:0.22.0, received ghcr.io/qwenlm/qwen-code/sandbox:latest). If a future edit reintroduces an unresolvable name here — the exact bug class this PR fixes — the suite stays green, and on an install where the manifest genuinely cannot be found (the unusual install layout this literal exists for), every command of an opted-in review fails at image pull again. Add a seam and pin the fallback:

// mock read-package-up → undefined (or add a test-only reset of manifestImage), then:
expect(reviewSandboxImage({})).toBe(`ghcr.io/qwenlm/qwen-code:${CLI_VERSION}`);
中文说明

这个 diff 修改的兜底字面量没有任何测试覆盖:没有任何测试能让 cliSandboxImage() 失败(它通过 readPackageUpSync 读取真实文件系统,没有注入缝隙),而测试布局中 manifest 总能解析成功,所以兜底分支从不执行。验证过的变异体展示了这个空洞:把 DEFAULT_IMAGE 退回坏值 ghcr.io/qwenlm/qwen-code/sandbox:latest,新套件依旧全绿——只有强制走兜底的探针(mock read-package-up 使其返回 undefined)才能抓住它(期望 ghcr.io/qwenlm/qwen-code:0.22.0,实际收到 ghcr.io/qwenlm/qwen-code/sandbox:latest)。如果未来某次修改在这里重新引入一个无法解析的名字——正是本 PR 修复的那类缺陷——套件会保持绿色,而在 manifest 确实找不到的安装布局(这个字面量为之存在的非常规安装布局)上,所有开启沙箱的审查命令会再次在拉镜像时失败。建议加一个测试缝隙并钉住兜底值(代码见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)


/** Container runtimes this module knows how to drive, in preference order. */
const RUNTIMES = ['docker', 'podman'] as const;
Expand Down Expand Up @@ -688,18 +695,76 @@ export function containerCommand(
return { file: opts.runtime, args };
}

/**
* The image `qwen --sandbox` itself runs, read from the package manifest that
* ships with this CLI (`config.sandboxImageUri`) — the same field
* `sandboxConfig.ts` reads, so the two cannot drift.
*
* This is a correction, not a refinement. The first cut hardcoded
* `ghcr.io/qwenlm/qwen-code/sandbox:latest` and its comment claimed that was
* "the same image `qwen --sandbox` uses". It is not: the CLI's image is
* `ghcr.io/qwenlm/qwen-code:<version>`, a different repository path and a
* pinned tag, and the hardcoded name does not resolve at all — an anonymous
* manifest request answers 403 where the real one answers 200. Every command
* of an opted-in review therefore failed at image pull, which nineteen rounds
* of argv-level review could not see because no container was ever started
* from that argv.
*
* Read once and cached: this is on the per-command path.
*/
function cliSandboxImage(): string | undefined {
if (manifestImage !== undefined) return manifestImage ?? undefined;
try {
const found = readPackageUpSync({
cwd: dirname(fileURLToPath(import.meta.url)),
});
const uri = (
found?.packageJson as
| { config?: { sandboxImageUri?: string } }
| undefined
)?.config?.sandboxImageUri;
manifestImage = typeof uri === 'string' && uri.trim() ? uri.trim() : null;
} catch {
manifestImage = null;
}
return manifestImage ?? undefined;
}

let manifestImage: string | null | undefined;

/**
* The image the reviewed repository's commands run in.
*
* Defaults to the CLI's own sandbox image, which already carries a Node
* toolchain — the same image `qwen --sandbox` uses, so a repository that
* builds under one builds under the other. `QWEN_REVIEW_SANDBOX_IMAGE`
* overrides it for a repository whose toolchain needs more (a JDK, a Python,
* a specific Node major), which is the case this default cannot cover and
* should not pretend to.
* toolchain — literally the same image `qwen --sandbox` resolves, read from
* the same manifest field, so a repository that builds under one builds under
* the other. That sentence was here before `cliSandboxImage` existed, when a
* hardcoded name made it false; it is now a description of the code rather
* than an intention about it.
*
* The parity is over the DEFAULT, not over every channel `--sandbox` reads.
* An operator who set `QWEN_SANDBOX_IMAGE` in a user-level `~/.qwen/.env` or
* in `settings.env` loses it here and falls back to that default: the loader
* records every key it applies from a file without distinguishing the user's
* own from the reviewed repository's, and this side cannot afford to guess
* wrong about which one it is holding. Exporting it in the shell keeps
* parity. Widening that would mean teaching the loader to carry the
* home-scoped classification it already computes — a change to the loader,
* not to this pick.
*
* `QWEN_REVIEW_SANDBOX_IMAGE` overrides it for a repository whose toolchain
* needs more (a JDK, a Python, a specific Node major), which is the case this
* default cannot cover and should not pretend to.
*/
export function reviewSandboxImage(
env: NodeJS.ProcessEnv = process.env,
// Injected so a test can tell the manifest apart from the fallback. It
// cannot otherwise: `DEFAULT_IMAGE`'s tag comes from `CLI_VERSION`, which is
// generated from the same manifest version, so the two currently produce the
// SAME string — deleting the manifest lookup falls through to a literal that
// string-equals it and the suite stays green. That is the mechanism this
// change exists to add, pinned by nothing until the two can be told apart.
manifest: () => string | undefined = cliSandboxImage,
): string {
// File-sourced overrides are ignored for the same reason the policy ignores
// them, and this one is sharper: the image IS the code the reviewed
Expand All @@ -711,6 +776,11 @@ export function reviewSandboxImage(
return (
pick('QWEN_REVIEW_SANDBOX_IMAGE') ||
pick(CUSTOM_SANDBOX_IMAGE_ENV_VAR) ||
// The operator's own sandbox image, if they configured one for

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The parity this comment promises does not hold when the operator configured their image in a user-level dotenv file — ~/.qwen/.env, ~/.env, or user settings.env — channels qwen --sandbox honours. loadEnvironment marks every applied key as file-sourced with no home-scoped exemption (the tracking sets never carry the homeScoped classification the loader already computes), so pick discards the value and review silently falls back to the manifest default image. Verified by a four-arm probe on unmodified code: in the user-dotenv and user-settings-env arms, qwen --sandbox resolves registry.example/qwen-custom:1 while review resolves ghcr.io/qwenlm/qwen-code:0.22.0; the shell-export arm keeps parity; the repo-dotenv arm is correctly rejected. Not a blocker — the fallback is the CLI's own shipped image, and exporting in the shell is a tested workaround — but the divergence is silent while the comment asserts the opposite. The cheaper fix is narrowing the comment; recording home-scoped provenance separately in environment.ts and exempting it for this key fixes the dotenv arm without weakening the repo guard, though the settings.env arm does not flip until the settings merge carries per-scope provenance:

// The operator's own sandbox image, if they exported it in their shell.
// File-loaded values — including the operator's own `~/.qwen/.env` — are
// dropped by the provenance gate below; export the variable to keep review
// and `qwen --sandbox` on the same image.
中文说明

当操作者把镜像配置在用户级 dotenv 文件(~/.qwen/.env~/.env 或用户 settings.env)里时,这条注释承诺的对齐并不成立——而这些都是 qwen --sandbox 尊重的配置渠道。loadEnvironment 会把每个生效的键都标记为文件来源,且没有家目录作用域的豁免(跟踪集合从不携带加载器已经计算出的 homeScoped 分类),所以 pick 会丢弃该值,审查静默退回 manifest 默认镜像。在未修改代码上做的四臂探针验证:用户级 dotenv 与用户级 settings.env 两臂中,qwen --sandbox 解析出 registry.example/qwen-custom:1,而审查解析出 ghcr.io/qwenlm/qwen-code:0.22.0;shell 导出渠道对齐正常;仓库级 .env 被正确拒绝。这不是阻塞项——兜底是 CLI 自带的镜像,shell 导出是已测试的变通办法——但分歧是静默的,而注释断言的恰恰相反。更便宜的修法是收窄注释;在 environment.ts 中单独记录家目录作用域的来源并对本键豁免,可以在不削弱仓库守卫的前提下修复 dotenv 渠道——不过 settings.env 渠道在设置合并支持按作用域记录来源之前不会翻转(代码见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// `qwen --sandbox`. Through `pick`, so a repository shipping it in its
// `.qwen/.env` cannot choose the image its own code runs in.
pick('QWEN_SANDBOX_IMAGE') ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The file-sourced provenance guard on this line has no test pinning it for the newly honoured key. The sibling key QWEN_REVIEW_SANDBOX_IMAGE has a dedicated rejection test ('ignores a repo-shipped image override — the image IS the code') that spies isFileSourcedEnvKey and asserts the attacker image is ignored, but QWEN_SANDBOX_IMAGE's reject path has no twin. A verified mutant shows the cost: replacing pick('QWEN_SANDBOX_IMAGE') with a plain env['QWEN_SANDBOX_IMAGE']?.trim() fails the guard probe (expected the result not to contain attacker.example, received attacker.example/rogue:1) while the entire existing suite stays green — 36 passed | 1 skipped, identical to baseline. If such a regression ships, a reviewed repository commits QWEN_SANDBOX_IMAGE=attacker.example/rogue:1 in its .qwen/.env and the review container pulls and runs the attacker's image. Mirror the sibling test:

it('ignores a repo-shipped QWEN_SANDBOX_IMAGE too', () => {
  vi.stubEnv('QWEN_SANDBOX_IMAGE', 'attacker.example/rogue:1');
  const spy = vi
    .spyOn(environment, 'isFileSourcedEnvKey')
    .mockImplementation((k) => k === 'QWEN_SANDBOX_IMAGE');
  try {
    expect(reviewSandboxImage()).not.toContain('attacker.example');
  } finally {
    spy.mockRestore();
    vi.unstubAllEnvs();
  }
});
中文说明

这一行上的文件来源守卫对新引入的键没有任何测试来钉住。兄弟键 QWEN_REVIEW_SANDBOX_IMAGE 有专门的拒绝测试('ignores a repo-shipped image override — the image IS the code'):spy isFileSourcedEnvKey 并断言攻击者镜像被忽略;但 QWEN_SANDBOX_IMAGE 的拒绝路径没有孪生测试。验证过的变异体展示了代价:把 pick('QWEN_SANDBOX_IMAGE') 换成普通的 env['QWEN_SANDBOX_IMAGE']?.trim(),守卫探针会失败(期望结果不包含 attacker.example,实际收到 attacker.example/rogue:1),而现有整套测试依旧全绿——36 通过 | 1 跳过,与基线完全一致。如果这样的回归被合入,被审查的仓库就可以在其 .qwen/.env 中提交 QWEN_SANDBOX_IMAGE=attacker.example/rogue:1,审查容器将拉取并运行攻击者的镜像。建议仿照兄弟测试补一条(代码见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +779 to +782

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-1: The file-sourced rejection of the newly honoured QWEN_SANDBOX_IMAGE channel still has no test pinning it — this round-1 finding still stands (the round-1 response addressed the other four suggestions, not this one). The sibling key QWEN_REVIEW_SANDBOX_IMAGE has a dedicated rejection test ('ignores a repo-shipped image override — the image IS the code') that spies isFileSourcedEnvKey; QWEN_SANDBOX_IMAGE appears in the suite only in honour-path and ordering tests (sandboxed-exec.test.ts lines 925, 936, 942). The guard itself works — the gap is that nothing fails if it goes away: replacing pick('QWEN_SANDBOX_IMAGE') with a plain env['QWEN_SANDBOX_IMAGE']?.trim() ships green (measured this round: 37 passed | 1 skipped, identical to baseline), while an attack probe — stubbing QWEN_SANDBOX_IMAGE=attacker.example/rogue:1 with isFileSourcedEnvKey mocked true for that key — fails under the mutation (expected 'attacker.example/rogue:1' not to contain 'attacker.example') and passes on the restored code. If such a regression ships, a reviewed repository that commits QWEN_SANDBOX_IMAGE=attacker.example/rogue:1 in its .qwen/.env chooses the image its own review code executes in. Mirror the sibling test inside describe('values a repository must not be able to set'):

it('ignores a repo-shipped QWEN_SANDBOX_IMAGE too', () => {
  vi.stubEnv('QWEN_SANDBOX_IMAGE', 'attacker.example/rogue:1');
  const spy = vi
    .spyOn(environment, 'isFileSourcedEnvKey')
    .mockImplementation((k) => k === 'QWEN_SANDBOX_IMAGE');
  try {
    expect(reviewSandboxImage()).not.toContain('attacker.example');
  } finally {
    spy.mockRestore();
    vi.unstubAllEnvs();
  }
});
中文说明

新引入的 QWEN_SANDBOX_IMAGE 渠道的「文件来源值拒绝」仍然没有任何测试钉住——这是第 1 轮就已提出的发现(R1-1),至今依然成立(第 1 轮的回复处理了其余四条建议,没有处理这一条)。兄弟键 QWEN_REVIEW_SANDBOX_IMAGE 有专门的拒绝测试('ignores a repo-shipped image override — the image IS the code'):spy isFileSourcedEnvKey 并断言攻击者镜像被忽略;而 QWEN_SANDBOX_IMAGE 在整套测试里只出现在生效路径与顺序断言中(sandboxed-exec.test.ts 第 925、936、942 行)。守卫本身是正确的——缺口在于:它即使失效也不会有任何测试变红。把 pick('QWEN_SANDBOX_IMAGE') 换成普通的 env['QWEN_SANDBOX_IMAGE']?.trim(),整套测试依旧全绿(本轮实测:37 通过 | 1 跳过,与基线完全一致);而攻击探针——把 QWEN_SANDBOX_IMAGE stub 成 attacker.example/rogue:1 并让 isFileSourcedEnvKey 对该键返回 true——在变异体上失败(期望不包含 attacker.example,实际收到 attacker.example/rogue:1),在还原后的代码上通过。如果这样的回归被合入,被审查的仓库就可以在其 .qwen/.env 中提交 QWEN_SANDBOX_IMAGE=attacker.example/rogue:1,从而选择审查代码实际运行的镜像。建议在 describe('values a repository must not be able to set') 中仿照兄弟测试补一条(代码见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

manifest() ||
DEFAULT_IMAGE
);
}
Expand Down
Loading