Skip to content
Draft
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
3 changes: 2 additions & 1 deletion docs/runner-isolation-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) | 막지 못함 | 코드 — 라이브 미검증 |

Expand Down
8 changes: 5 additions & 3 deletions src/runners.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 공유)

Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/runners/codex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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로 사상한다.
Expand Down
24 changes: 23 additions & 1 deletion test/runner-gate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 오탐)
Expand Down
Loading