diff --git a/docs/runner-isolation-limits.md b/docs/runner-isolation-limits.md index 5b0580a0..c8c019c1 100644 --- a/docs/runner-isolation-limits.md +++ b/docs/runner-isolation-limits.md @@ -13,7 +13,8 @@ |---|---|---|---| | claude / glm / kimi / openrouter — shell OFF | SDK `query({canUseTool})` | **하드 차단** | 격리 재현·회귀 테스트 실측 | | 〃 — shell ON | 같은 게이트 + Bash 리터럴 스캔 | **1차 방어만** (아래 참고) | 실측 | -| codex | `codex exec --sandbox workspace-write`, cwd=워크스페이스 | 막지 못함 | 코드(`runners.mjs`) — 라이브 미검증 | +| codex (맥·리눅스) | `codex exec --sandbox workspace-write`, cwd=워크스페이스 | 막지 못함 | 코드(`runners.mjs`) — 라이브 미검증 | +| codex (윈도우, 2026-08-06~) | `codex exec --dangerously-bypass-approvals-and-sandbox` | 막지 못함 + **OS 샌드박스 자체가 없음** | 윈도우 codex는 샌드박스 기전 부재로 workspace-write가 read-only로 떨어져 쓰기 전멸(신고 4건 전부 윈도우 + openai/codex#6374). 전권 원칙에 따라 우회 — 근거·되돌림 조건은 `src/runners/codex.mjs` `codexSandboxModeArgs` 주석 | | gemini | `--approval-mode auto_edit` | 막지 못함 | 코드 — 라이브 미검증 | | antigravity | `--mode accept-edits` (셸 OFF면 `--sandbox`) | 막지 못함 | 코드 — 라이브 미검증 | diff --git a/src/runners.mjs b/src/runners.mjs index 5b265871..8388a2d5 100644 --- a/src/runners.mjs +++ b/src/runners.mjs @@ -14,7 +14,7 @@ import { join } from 'node:path'; import { monthCostByRunner } from './usage.mjs'; // usage는 workspace만 의존 — 순환 없음 import { exec, exists, scrubServerSecrets } from './runners/shared.mjs'; import { RUNNERS, RUNNER_AUTH, hostOptInAllowed, isCliRunner, pickRunner, oauthFormatError } from './runners/catalog.mjs'; -import { codexHome, codexCmd, importCodexAuth, recoverCodexAuth, writeCodexTurnConfig, codexEffortArgs, codexSandboxArgs } from './runners/codex.mjs'; +import { codexHome, codexCmd, importCodexAuth, recoverCodexAuth, writeCodexTurnConfig, codexEffortArgs, codexSandboxArgs, codexSandboxModeArgs } from './runners/codex.mjs'; import { geminiCmd, writeGeminiTurnSettings } from './runners/gemini.mjs'; import { openRoots } from './workroots.mjs'; // 파일 반경 단일 진실(codex·gemini·antigravity 공유) @@ -34,7 +34,7 @@ export { pickRunner, autoRunnerOf, oauthFormatError, excludeWith, } from './runners/catalog.mjs'; export { - provisionCodexCli, codexSandboxArgs, CODEX_EFFORTS, codexEffortArgs, + provisionCodexCli, codexSandboxArgs, codexSandboxModeArgs, CODEX_EFFORTS, codexEffortArgs, importCodexAuth, recoverCodexAuth, writeCodexTurnConfig, } from './runners/codex.mjs'; export { provisionGeminiCli, probeGeminiOAuth, probeGeminiHostOAuth } from './runners/gemini.mjs'; @@ -111,7 +111,9 @@ export async function externalExec({ runner, model, cwd, prompt, timeoutMs = 300 try { await exec(cmd.file, [ ...cmd.args, - 'exec', '--sandbox', 'workspace-write', '--skip-git-repo-check', + // 샌드박스 모드 — 맥·리눅스 workspace-write / 윈도우는 우회(윈도우 codex는 샌드박스 기전이 + // 없어 read-only로 떨어져 쓰기 전멸 — 근거·되돌림 조건은 codexSandboxModeArgs 주석) + 'exec', ...codexSandboxModeArgs(), '--skip-git-repo-check', ...codexEffortArgs(effort), // 크루별 추론 강도 — codex도 지원(실측 2026-07-26) ...codexSandboxArgs(caps, workRoots), // config.toml과 이중 — 신버전은 `-c`, 구버전은 config.toml이 받는다 '--output-last-message', out, diff --git a/src/runners/codex.mjs b/src/runners/codex.mjs index cfcda80d..368be1ec 100644 --- a/src/runners/codex.mjs +++ b/src/runners/codex.mjs @@ -120,6 +120,25 @@ export const codexSandboxArgs = (caps, workRoots = []) => { ]; }; +/** 샌드박스 모드 인자(순수) — 플랫폼별로 갈린다. + 맥·리눅스: `--sandbox workspace-write` — seatbelt/Landlock이 실제로 작동해 "워크스페이스+ + writable_roots만 쓰기"가 지켜진다(앱 본체 /Applications 보호가 여기 걸려 있다). + 윈도우: codex는 OS 샌드박스 기전이 없어 workspace-write를 못 지키고 **통째로 read-only로 + 떨어진다** — 실사용 신고 4건 전부 윈도우(ref 57b7599e·cff06983·63dc3339·0145d811, 2026-08-03~05 + "read-only sandbox로 파일 생성 차단") + 벤더 확인(openai/codex#6374: "Windows doesn't have an + off-the-shelf sandboxing mechanism", WSL 권장). 네이티브 윈도우 샌드박스는 실험 단계로 elevated + 셋업(UAC·로컬 사용자 생성·방화벽 규칙)이 선행돼야 하는데 Argo가 띄우는 `codex exec`는 그 셋업 + 없이 돈다. 크루가 일을 아예 못 하는 상태(QA 최다 클러스터 14건)라, 전권 원칙(capabilities.mjs — + 설치부터 전권, 유건 2026-07-30)에 따라 윈도우만 샌드박스를 우회한다. 맥의 "홈은 열되 앱 본체는 + 보호" 비대칭이 사라지는 건 아니다 — 윈도우 앱 본체(%LOCALAPPDATA%)는 홈 안이라 writable_roots=홈 + 시점부터 이미 샌드박스 밖 보호가 없었다. 2차 방어는 commonDirectives 금지 지시(기존과 동일). + ponytail: 벤더가 네이티브 윈도우 샌드박스를 정식화하면 win32 분기를 workspace-write로 되돌린다. + (export: 회귀 테스트용 — platform 주입으로 win32 분기를 어느 OS CI에서도 검증) */ +export const codexSandboxModeArgs = (platform = process.platform) => + platform === 'win32' + ? ['--dangerously-bypass-approvals-and-sandbox'] + : ['--sandbox', 'workspace-write']; + /** 크루별 추론 강도 → codex CLI 인자(순수). codex도 강도를 지원한다 — `-c model_reasoning_effort=…`가 인식되는 키임을 실측(2026-07-26, codex-cli 0.144.1: 미인식 키는 --strict-config에서 즉시 에러, 이 키는 통과하고 low·high·xhigh 모두 실턴 성공). 'max'는 Claude 전용 명칭이라 xhigh로 사상한다. diff --git a/test/runner-gate.test.mjs b/test/runner-gate.test.mjs index e3c9bcf4..e1469520 100644 --- a/test/runner-gate.test.mjs +++ b/test/runner-gate.test.mjs @@ -9,7 +9,29 @@ import { join } from 'node:path'; // 임시 ARGO_ROOT — WS_ROOT는 모듈 로드 시 고정되므로 import보다 먼저 심는다(실데이터 미접촉) process.env.ARGO_ROOT = await mkdtemp(join(tmpdir(), 'argo-gatetest-')); const { anyRunnerUsable, runnerNeedsReconnect } = await import('../app/runner-usable.mjs'); -const { codexSandboxArgs } = await import('../src/runners.mjs'); +const { codexSandboxArgs, codexSandboxModeArgs } = await import('../src/runners.mjs'); + +test('codexSandboxModeArgs: 윈도우=샌드박스 우회, 그 외=workspace-write (읽기전용 쓰기 전멸 신고 2026-08-03~05)', () => { + // 윈도우 codex는 OS 샌드박스 기전이 없어 workspace-write가 read-only로 떨어진다(openai/codex#6374). + // 전권 원칙(capabilities.mjs)에 따라 윈도우만 우회 — 맥·리눅스는 seatbelt/Landlock이 실제로 지켜 + // "앱 본체 보호"가 여기 걸려 있으므로 절대 우회로 바꾸지 않는다(2026-07-22 writable_roots="/" 크리티컬 계열). + assert.deepEqual(codexSandboxModeArgs('win32'), ['--dangerously-bypass-approvals-and-sandbox'], + '윈도우 = 우회(샌드박스가 쓰기 전멸을 만들던 자리)'); + assert.deepEqual(codexSandboxModeArgs('darwin'), ['--sandbox', 'workspace-write'], '맥 = 기존 유지'); + assert.deepEqual(codexSandboxModeArgs('linux'), ['--sandbox', 'workspace-write'], '리눅스 = 기존 유지'); + // 현재 플랫폼 기본값 = 위 둘 중 하나와 정확히 일치(주입 없는 실호출 경로 잠금) + assert.deepEqual(codexSandboxModeArgs(), + codexSandboxModeArgs(process.platform), '기본 인자 = process.platform'); +}); + +test('codexSandboxModeArgs 배선: externalExec가 하드코딩 --sandbox가 아니라 이 함수를 지난다', async () => { + // 순수 함수만 잠그면 호출부가 '--sandbox workspace-write' 하드코딩으로 롤백돼도 초록이다 — + // facade의 externalExec 앵커 테스트와 같은 패턴으로 배선 자체를 잠근다. + const src = await (await import('node:fs/promises')).readFile(new URL('../src/runners.mjs', import.meta.url), 'utf8'); + const codexBlock = src.split("if (runner === 'codex')")[1]?.split("if (runner === 'gemini')")[0] ?? ''; + assert.ok(codexBlock.includes('...codexSandboxModeArgs()'), 'codex exec 인자에 codexSandboxModeArgs 배선'); + assert.ok(!codexBlock.includes("'--sandbox', 'workspace-write'"), 'codex 블록에 하드코딩 샌드박스 인자 잔존 금지'); +}); test('anyRunnerUsable: codex/gemini 자격 연결이면 CLI 미감지여도 통과(실사고 재현)', () => { // 실사용 신고 상태 재현 — OAuth 웹 브리지로 자격 저장, hostInstalled=false(미설치 또는 GUI PATH 오탐)