Skip to content
treeman99Public
forked from stablyai/orca

About

Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and VPS.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

 
 

Latest commit

 

History

16,221 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Orca Orca — 사내 빌드 (Samsung DS)

Windows x64 GitHub Enterprise AWS Bedrock License: MIT

이 브랜치(enterprise/samsungds)는 오픈소스 stablyai/orca를 사내 환경에 맞춰 커스터마이즈한 포크입니다. 배포 대상은 Windows x64 단일 플랫폼이며, 사내 GitHub Enterprise(github.samsungds.net)와 AWS Bedrock 기반 Claude를 씁니다. 공개 배포본(자동 업데이트·텔레메트리가 켜진 .exe)과 달리, 이 빌드는 사내에서 직접 빌드하고 외부 phone-home을 관리자 정책으로 잠급니다.


Orca란

Orca는 여러 CLI 코딩 에이전트(Claude Code, Codex 등)를 각자의 git worktree에서 병렬로 실행하고 한 곳에서 관리하는 Electron 데스크톱 앱입니다. 주요 기능:

  • 병렬 Worktree — 하나의 프롬프트를 여러 에이전트에 나눠 실행하고 결과를 비교·병합
  • 터미널 분할 — WebGL 렌더링, 무한 분할, 재시작 후에도 유지되는 스크롤백
  • GitHub 네이티브 통합 — PR·이슈·체크를 앱 안에서 열람하고 worktree로 바로 진입 (이 브랜치는 사내 GHES 대응)
  • AI Diff 주석 / 파일 드래그 / 임베디드 브라우저 / Orca CLI 등

기능 사용법 자체는 원본 문서(onorca.dev/docs)를 참고하세요. 이 README는 사내 빌드·설정·배포·동기화에 집중하며, 문서 전체가 Windows x64 배포를 전제로 합니다.

Note

배포 대상이 Windows뿐이라는 사실이 코드를 Windows 전용으로 고쳐도 된다는 뜻은 아닙니다. AGENTS.md의 "Cross-Platform Support" 규칙은 macOS/Linux/WSL/SSH를 모두 지원하도록 요구하는 기여 규칙이고, 이 README가 좁히는 것은 빌드·배포 대상입니다. 플랫폼 분기를 "우리는 Windows만 쓰니까"라는 이유로 지우지 마세요.

지원 에이전트: 터미널에서 도는 CLI 에이전트는 모두 동작합니다. 이 환경의 1차 대상은 AWS Bedrock 기반 Claude Code입니다(§3).


0. 설정이 어디에 사는가 — 네 개의 버킷

이 배포에서 동작을 바꾸는 설정 값은 네 군데에 흩어져 있고, 각각 읽는 주체가 다릅니다. 어떤 값을 어디에 넣어야 하는지 헷갈리는 것이 이 배포의 가장 흔한 사고 원인이므로 먼저 정리합니다.

버킷 위치 읽는 주체 넣는 사람
A. OS 환경 변수 Windows 사용자/시스템 환경 변수 Orca 외부의 프로세스(git, gh, CLI, Node) 또는 Orca 시작 전에 존재해야 하는 값 IT / 사용자
B. 정책 파일 %ProgramData%\Orca\enterprise-policy.json Orca 자신(TypeScript 코드) 관리자 전용
C. Claude Code 설정 %USERPROFILE%\.claude\settings.json Bedrock 관련 키(env, awsAuthRefresh)는 Claude Code CLI만. 단 같은 파일의 hooks/statusLine 키는 Orca도 읽고 씁니다 — 아래 C 사용자
D. 빌드 셸 전용 빌드하는 PowerShell 세션 안에서만 electron-builder와 빌드 스크립트 (패키징 시점) 빌드 담당자

이 네 버킷은 설정 값만 다룹니다. 자격증명과 신뢰 저장소는 여기 없습니다 — gh auth status 인벤토리(§2), git config의 http.sslBackend/http.sslCAInfo(§2), gateway-cli가 자기 안에 들고 있는 게이트웨이 세션(§3.1), 은 각각 별도의 위치이며 해당 절에서 다룹니다.

핵심 원칙: Orca의 동작을 바꾸는 값은 환경 변수가 아니라 정책 파일(B)에 넣습니다. 이유는 Orca가 env에서 읽는 값이 Orca가 띄우는 모든 자식 프로세스(에이전트 CLI, gh, git, 릴레이)에 그대로 상속되고, setx로 심은 값은 같은 머신의 무관한 도구까지 오염시키기 때문입니다 — 근거: src/shared/enterprise-policy.ts:4-8, src/main/enterprise/enterprise-policy-file.ts:4-11.

A. OS 환경 변수 — 실제로 여기에 있어야 하는 것만

변수 읽는 주체 필수? 비고
HTTPS_PROXY / HTTP_PROXY / NO_PROXY OS 표준. Orca의 Node 계층(대소문자 6종 + NO_PROXY 2종을 직접 읽습니다 — src/shared/network-proxy.ts:13-21), git, gh, Claude Code CLI, gateway-cli가 각자 읽음 사내 프록시 환경이면 필수 앱 안의 프록시 설정(설정 → Advanced → Network)은 비워 두는 편이 안전합니다 — 아래 참고
NODE_EXTRA_CA_CERTS Node만. Orca 메인 프로세스와 Node 기반 CLI TLS 검사 프록시 환경이면 필요 git/gh 바이너리에는 아무 효과가 없습니다(§2). gateway-cli가 사내 CA를 어떻게 신뢰하는지는 그 CLI의 계약이며 미확인입니다 (이 행 전체가 Node/git 쪽 계약입니다 — 저장소에는 NODE_EXTRA_CA_CERTS 참조가 한 건도 없으므로 코드로 검증한 사실이 아닙니다)
GH_HOST gh CLI 고유 변수 선택 Orca는 정책의 githubEnterpriseHost가 비었을 때만 폴백으로 읽습니다(src/shared/enterprise-policy.ts:366). 정책에 호스트를 넣었다면 불필요
ORCA_ENTERPRISE_POLICY Orca만 불필요 — 아래 참고 정책 파일 경로만 지정하는 변수. 이 포크가 추가한 유일한 런타임 환경 변수

Important

설치 프로그램이 기본 정책을 이미 싣고 있으므로 ORCA_ENTERPRISE_POLICY는 전혀 필요하지 않습니다. 패키징 빌드에서 탐색 후보는 [머신 전역, 번들, ORCA_ENTERPRISE_POLICY 경로, 사용자별] 순서로 조립되고(enterprise-policy-file.ts:110-113), 머신 전역 Windows 경로는 코드에 하드코딩(:58-68), 번들 경로는 process.resourcesPath에서 해석됩니다(:236-244). 후보를 순서대로 읽다가 먼저 파싱에 성공한 파일에서 멈추므로(:161-215) 앞 후보가 있으면 환경 변수 후보는 도달조차 하지 않습니다. 플릿에는 이 변수를 배포하지 마세요. 개발·검증용입니다(§4.1).

선택적으로 유효한 OS 환경 변수 (이 포크가 만든 것이 아니라 upstream/커뮤니티 소유):

  • DO_NOT_TRACK, ORCA_TELEMETRY_DISABLED — upstream의 텔레메트리 킬 스위치(src/main/telemetry/consent.ts:79, :83). 정책 파일의 disableTelemetry가 같은 일을 하므로 중복입니다.
  • ORCA_DIAGNOSTICS_DISABLED — 로컬 NDJSON 진단 파일 기록까지 끕니다(src/main/observability/index.ts:102, :113-118). 정책 파일로는 로컬 파일을 끌 수 없습니다. 다만 이 변수를 켜면 §5.2의 정책 적용 확인 수단도 함께 사라집니다.
  • WSLENV — WSL 프로젝트에 Windows 환경 변수를 넘길 때만(§3.4).
  • AWS_REGION, CLAUDE_CODE_USE_BEDROCK 등 — 여기 두어도 동작하지만 버킷 C가 더 깔끔합니다(§3.2).

여기에 두면 안 되는 변수

  • AWS_PROFILE — 설정하지 마세요. 새 인증 방식에는 프로필 개념이 없습니다. 자격증명은 gateway-cli가 소유하며(§3.1), Orca 프로덕션 코드에는 이 변수 참조가 0건입니다.
  • CLAUDE_CONFIG_DIR — 설정하지 마세요. 값이 있으면 Orca가 그 값을 자식 환경에 재기입하고(src/main/claude-accounts/runtime-paths.ts:23), SSH 프로젝트에서는 로컬 Windows 경로가 원격 셸로 들어갑니다(§3.3).

Warning

setx로 OS 환경 변수를 바꾼 뒤 Orca만 재시작해도 에이전트 터미널에는 반영되지 않습니다. 터미널을 앱 재시작 이후까지 살려 두는 영속 데몬이 자기 fork 시점의 process.env를 계속 쓰고(src/main/daemon/daemon-init.ts:700-705, 재사용 판정 :574), 레지스트리에서 다시 읽어 병합하는 값은 PATH 하나뿐입니다(src/main/pty/windows-environment-path.ts:280-292). 반영시키려면 데몬을 교체(Manage Sessions → Restart)하거나 로그아웃/재로그인하세요.

프록시를 어디에 둘 것인가 — 앱의 프록시 설정(설정 → Advanced → Network, src/renderer/src/components/settings/AdvancedNetworkSettingsSection.tsx)이 비어 있으면 buildConfiguredProxyEnv가 {}를 반환해 상속된 프록시 환경 변수를 전혀 건드리지 않습니다(src/shared/network-proxy.ts:115-121). 값이 들어 있으면 HTTP_PROXY/HTTPS_PROXY/ALL_PROXY(대소문자 6종)를 덮어쓰고, NO_PROXY만은 상속값과 병합해 보존합니다(:124-137, 병합 소스 :83-88; 호출부 src/main/ipc/pty.ts:1696). 따라서 프록시는 OS 환경 변수에 두고 앱 설정은 비워 두는 것이 예측 가능합니다.

B. 정책 파일 — Orca 자신의 동작

전체 스키마·예제·롤아웃은 엔터프라이즈 정책 파일 레퍼런스 에 있습니다. 이 배포의 관리자가 실제로 쓰는 키는 다음 정도입니다.

// %ProgramData%\Orca\enterprise-policy.json
{
  "lockdown": true,
  "githubEnterpriseHost": "github.samsungds.net"
}
  • lockdown: true — 마스터 스위치. 개별 disable* 키를 쓰지 않아도 전부 상속됩니다(enterprise-policy.ts:157-175, :358-362).
  • githubEnterpriseHost — 허용목록 자동 추가 + GHES 로그인 대상 기본값 + GHES blob/commit URL 인식(§2).
  • lockdown을 쓰지 않을 거라면 disableManagedClaudeAccounts와 disableUsagePolling은 Bedrock 플릿에서 명시적으로 켜야 합니다(§3.3).
  • enforceNetworkAllowlist / allowedNetworkHosts — lockdown을 상속하지 않는 opt-in(§4.3).

전체 키 목록과 각 키가 실제로 무엇을 끄는지는 §4.2 표를 보세요.

C. Claude Code ~/.claude/settings.json — Bedrock 키는 Orca가 건드리지 않지만, 파일 자체는 Orca도 씁니다

Bedrock 모델/리전/플래그가 들어가는 곳입니다. 두 가지를 분리해서 알아야 합니다.

Orca가 건드리지 않는 것 — Bedrock 설정 그 자체. env·awsAuthRefresh 같은 키를 Orca는 읽지도 쓰지도 않고, 파일 경로를 바꾸지도 않습니다. CLAUDE_CONFIG_DIR은 이미 상속된 값이 있을 때만 재기입되고, 사내는 이 변수를 설정하지 않으므로 %USERPROFILE%\.claude가 그대로 쓰입니다(src/main/claude-accounts/runtime-paths.ts:15-24).

Orca가 건드리는 것 — 같은 파일의 hooks와 statusLine. 에이전트 상태 훅이 이 파일에 설치됩니다. 경로는 %USERPROFILE%\.claude\settings.json으로 정해져 있고(src/main/claude/hook-settings.ts:22-26, :71-73), 앱을 띄울 때마다 자동으로 설치됩니다(src/main/index.ts:2786-2799). 이 자동 설치를 막는 코드 게이트는 인자를 무시하고 항상 true를 돌려주므로(src/main/startup/configure-process.ts:200-203) 유일한 off 스위치는 설정 → Agents의 에이전트 상태 훅 토글입니다(src/renderer/src/components/settings/AgentsPane.tsx:1058-1066; 시작 시 이 값을 확인하는 곳은 src/main/index.ts:2788). 설치는 파일을 읽고(src/main/claude/hook-service.ts:193) hooks만 갈아끼운 뒤(src/main/claude/hook-settings.ts:160-178 — { ...config, hooks }로 나머지 키를 보존) 다시 씁니다(src/main/claude/hook-service.ts:218). 실무상 의미는 세 가지입니다:

  • Bedrock 블록은 살아남습니다. hooks/statusLine 밖의 키는 JSON.parse → JSON.stringify 왕복으로 보존됩니다(src/main/agent-hooks/hooks-json-read.ts:14-39, src/main/agent-hooks/installer-utils.ts:328).
  • 파일이 재포맷되고 .bak이 생깁니다. 2-스페이스 인덴트로 다시 써지며, 직전 내용은 settings.json.bak 하나로 롤링 백업됩니다(installer-utils.ts:351-353).
  • 주석이나 후행 쉼표를 넣지 마세요. 이 파일은 엄격한 JSON으로 파싱되므로(src/main/agent-hooks/hooks-json-read.ts:17-22) 깨져 있으면 훅 설치가 error 상태로 끝나고 쓰기를 포기합니다(src/main/claude/hook-service.ts:194-202). JSONC를 허용하는 것은 정책 파일(버킷 B)뿐이고 이 파일은 아닙니다.

읽기 전용으로 이 파일을 보는 곳이 하나 더 있습니다 — 스킬/플러그인 탐색이 활성 플러그인 목록을 확인할 때입니다(src/main/skills/claude-plugin-skill-sources.ts:37).

Bedrock 설정 자체의 내용과 주의사항은 §3.

C.1 터미널에서 claude를 bypass permissions로 실행하기

권한 확인 프롬프트 없이 돌리는 방법입니다. 아래는 Claude Code 2.1.261 기준이며, 플래그는 claude --help로 언제든 재확인할 수 있습니다.

그때그때 한 번만 — 가장 흔한 방식입니다.

claude --dangerously-skip-permissions
# Windows PowerShell도 동일합니다
claude --dangerously-skip-permissions

같은 효과의 다른 표기 — --permission-mode는 다른 모드(plan, acceptEdits, manual, auto, dontAsk)로 바꿔 쓸 때 유용합니다.

claude --permission-mode bypassPermissions

켜 두되 기본값으로는 쓰지 않기 — 시작은 평소 모드로 하고, 세션 중에 bypass로 전환하는 것만 허용합니다. 사내에서 상시 bypass가 부담스러울 때 절충안입니다.

claude --allow-dangerously-skip-permissions

매번 치기 싫다면 — ~/.claude/settings.json에 영구 설정 (버킷 C, 위 §C의 그 파일입니다)

{
  "permissions": { "defaultMode": "bypassPermissions" }
}

Orca가 이 파일에 hooks/statusLine을 다시 쓰지만 나머지 키는 보존하므로(§C) 이 설정은 살아남습니다. 다만 주석이나 후행 쉼표는 넣지 마세요 — 엄격한 JSON으로 파싱됩니다.

Orca 터미널에서 띄우는 에이전트라면 손으로 칠 필요가 없습니다. 설정 → Agents의 권한 토글을 켜면 Orca가 에이전트마다 해당 플래그를 붙여 실행합니다(src/shared/tui-agent-permissions.ts) — claude는 --dangerously-skip-permissions(:7), codex는 --dangerously-bypass-approvals-and-sandbox(:10), gemini는 --yolo(:11) 식으로 27종이 매핑돼 있습니다. 일부만 켜져 있으면 mixed로 표시됩니다.

안 켜질 때 — 원인은 셋뿐입니다.

증상 원인
--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons root/sudo로 실행했습니다. 일반 사용자로 실행하세요
Bypass permissions mode was disabled by settings 설정에 permissions.disableBypassPermissionsMode가 켜져 있습니다. 관리형 설정으로 배포됐다면 사용자가 못 풉니다
경고 다이얼로그가 뜨고 멈춤 최초 1회 수락 절차입니다. 승인하면 이후 세션부터 안 뜹니다

이 포크의 정책 파일(버킷 B)은 권한 모드를 통제하지 않습니다. enterprise-policy.ts에 permission 관련 키가 없습니다 — allowedAgents는 어떤 에이전트를 띄울 수 있는가만 정하고 그 에이전트가 권한을 어떻게 묻는가는 Claude Code 자신의 설정 영역입니다. 사내에서 bypass를 막아야 한다면 Orca 정책이 아니라 Claude Code의 permissions.disableBypassPermissionsMode를 관리형 설정으로 배포하세요.

D. 빌드 셸 전용 — 사용자 환경에는 절대 넣지 않습니다

변수 역할 근거
ORCA_WIN_PUBLISHER_NAME 설치 프로그램의 Authenticode publisherName. 기본값은 SignPath Foundation config/electron-builder.config.cjs:330-332
ORCA_DISABLE_PUBLISH_TARGET=1 publish 타깃을 null로 만들어 업데이터 메타데이터를 아예 생성하지 않음 :543-551 (의도는 :540-542 주석)
WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD electron-builder 고유의 Windows 코드 서명 입력 electron-builder 계약
ORCA_MAC_RELEASE 반드시 비어 있어야 합니다. 1이면 forceCodeSigning이 최상위 설정으로 켜져(:450, 값 출처 :29-30) 서명 없는 Windows 빌드가 실패합니다 :29-30, :450
ORCA_STRICT_ELECTRON_INSTALL=1 pnpm install의 postinstall이 Electron 바이너리 설치 실패를 묵인하고 넘어가지 않게 합니다. §1의 빌드 순서가 이 값을 켭니다 config/scripts/rebuild-native-deps.mjs:282
GH_TOKEN / GITHUB_TOKEN / GITHUB_RELEASE_TOKEN electron-builder가 릴리스 업로드에 쓰는 자격증명. §1에서 지우는 대상입니다 — 남아 있으면 --publish never와 함께 쓰더라도 실수로 upstream에 올릴 여지를 남깁니다 electron-builder 계약

이 표의 변수들은 빌드·패키징 시점에만 읽힙니다. 설치된 Orca는 어느 것도 읽지 않으므로 setx로 심을 이유가 전혀 없습니다.

이전 문서에 있던 ORCA_ENTERPRISE_LOCKDOWN / ORCA_DISABLE_AUTO_UPDATE / ORCA_DISABLE_STAR_NAG / ORCA_DISABLE_TELEMETRY / ORCA_GITHUB_ENTERPRISE_HOST는 전부 삭제되었습니다. 코드 어디에도 없으므로 setx로 심어도 아무 일도 일어나지 않습니다.


1. 빌드 — Windows 설치 프로그램(.exe)

회사 Windows x64 머신에서 PowerShell로 빌드합니다. 전체 절차·서명·프록시·트러블슈팅은 Windows 사내 빌드 가이드 참고.

git checkout enterprise/samsungds
npm install -g pnpm   # corepack enable 은 하지 마십시오 — docs/reference/pnpm-12-corepack-install.md
Remove-Item Env:GH_TOKEN, Env:GITHUB_TOKEN, Env:GITHUB_RELEASE_TOKEN, Env:ORCA_MAC_RELEASE -ErrorAction SilentlyContinue
$env:ORCA_STRICT_ELECTRON_INSTALL = "1"
pnpm install --frozen-lockfile
pnpm build:release
node config/scripts/ensure-native-runtime.mjs --runtime=electron
pnpm exec electron-builder --config config/electron-builder.config.cjs --win --x64 --publish never
# 산출물: dist\orca-windows-setup.exe  (NSIS, per-user 설치, 기본 무서명)

전제 조건

  • Visual Studio 2022 Build Tools("C++를 사용한 데스크톱 개발" 워크로드) + Python 3 — 준비 부담의 대부분이 여기 있습니다. 매 빌드마다 네이티브 모듈을 소스에서 재컴파일합니다.
  • Node — 회사 표준 최신 버전으로도 빌드됩니다. package.json의 engines.node: "24"는 강제되지 않고(경고만), 네이티브 모듈은 호스트 Node가 아니라 Electron ABI로 재빌드됩니다. 첫 빌드 전 node config/scripts/ensure-native-runtime.mjs --check-only가 exit 0인지만 확인하세요(플래그는 config/scripts/ensure-native-runtime.mjs:19-21).
  • --publish never는 필수입니다. 빠지면 사내 CI(CI=true)에서 electron-builder가 github.com/stablyai/orca로 업로드를 시도합니다(config/electron-builder.config.cjs:543-551). 원커맨드 pnpm build:win에는 이 플래그가 없으므로 그대로 쓰지 마세요.

공개 배포본을 그대로 받아 쓰지 않는 이유: 공개 .exe는 자동 업데이트·텔레메트리가 켜진 빌드입니다. 사내에서는 이 브랜치를 직접 빌드해 외부 연동을 잠급니다(§4).

설치 프로그램을 만들지 않고 화면만 확인하려면 — 맥북에서 pnpm dev로 잠금이 적용된 UI를 그대로 볼 수 있습니다. 비패키징 인스턴스에서는 ORCA_ENTERPRISE_POLICY가 정책 탐색 1순위이므로 %ProgramData%도 관리자 권한도 필요하지 않습니다. 절차·확인 지점·dev로는 확인할 수 없는 항목은 macOS dev 빌드 UI 확인 가이드 를 보세요.

1.1 GitHub Actions로 빌드 — Windows + Linux

빌드 머신(특히 Linux)이 없을 때 씁니다. .github/workflows/enterprise-build.yml 은 위 §1 절차를 GitHub 러너에서 그대로 재현하는 포크 전용 레인입니다. upstream 의 release-cut.yml 에서 태그 컷, SignPath 서명, 텔레메트리 빌드 식별자, 벤더 릴리스 업로드를 걷어낸 것입니다.

  • 실행: Actions 탭 → enterprise build → Run workflow → 브랜치와 platforms(both/windows/linux) 선택. CLI 로는 gh workflow run enterprise-build.yml --ref enterprise/samsungds -f platforms=both. workflow_dispatch 전용입니다 — 푸시로는 돌지 않습니다.

  • 산출물(워크플로 아티팩트, 실행 페이지 하단에서 zip 으로 내려받습니다)

    아티팩트 내용
    orca-windows-x64-<ver> orca-windows-setup-<ver>.exe (NSIS, per-user, 무서명)
    orca-linux-x64-<ver> orca-linux-<ver>.AppImage, orca-ide_<ver>_amd64.deb, orca-ide-<ver>.x86_64.rpm

    <ver> 는 package.json 의 version 입니다. 버전은 워크플로가 패키징 뒤에 파일명에 붙이므로 로컬 빌드(§1)의 산출물 이름은 그대로 orca-windows-setup.exe / orca-linux.AppImage 입니다.

  • 정책은 세 OS 공통으로 실립니다. resources/enterprise-policy.json 은 commonExtraResources 로 들어가므로(§4) 리눅스 패키지도 잠금이 적용된 상태로 나옵니다. 워크플로가 패키징 직후 resources/enterprise-policy.json 존재를 확인합니다.

  • 업로드 차단은 이중입니다 — --publish never 에 더해 잡 환경에 ORCA_DISABLE_PUBLISH_TARGET=1 을 두어 publish 를 null 로 만듭니다(§5-1). 워크플로 권한도 contents: read 뿐이라 GITHUB_TOKEN 이 빌드 셸로 흘러들지 않습니다.

⚠️ origin(github.com/treeman99/orca)은 public 저장소입니다. 워크플로 아티팩트는 링크를 아는 사람이면 누구나 내려받을 수 있습니다. 설치본에 자격증명이 들어가지는 않지만(정책 파일은 잠금 설정값일 뿐입니다), 배포본 자체를 공개하고 싶지 않다면 저장소를 private 으로 바꾸거나 사내 GHES + self-hosted 러너로 옮겨야 합니다. private 으로 바꾸면 Actions 분(minutes)이 과금되며 Windows 러너는 2배로 계산됩니다.

Linux 산출물의 glibc 하한(Ubuntu 20.04 / glibc 2.31)은 패키징 훅이 강제하고, 워크플로는 그 위에 패키징된 node-pty 를 실제로 로드해 보는 스모크까지 돌립니다(upstream 이 v1.4.224 에서 docs/reference/ 를 통째로 지워 근거 문서는 v1.4.223 태그에만 남아 있습니다).


2. 사내 GitHub Enterprise (github.samsungds.net)

Orca의 GitHub 연동은 gh CLI를 통하며 GHES를 원본 단계에서 이미 지원합니다(github.com 하드코딩 아님).

# 사용자별 1회. 이 단계가 빠지면 GHES 리모트는 GitHub로 인식되지 않습니다.
gh auth login --hostname github.samsungds.net
gh auth status                      # github.samsungds.net 이 목록에 보여야 함

무엇이 "이 호스트는 GitHub다"를 결정하는가 — gh auth status입니다

사내 GHES 같은 커스텀 호스트를 GitHub로 판정하는 신호는 오직 gh auth status 인벤토리입니다. 정책 파일도, 환경 변수도 아닙니다. github.com은 호스트명만으로 바로 처리되고(src/main/github/github-api-repository.ts:97-98) 엔터프라이즈 판정 경로는 그 호스트에서 즉시 빠집니다 — v1.4.159부터는 SSH 별칭까지 해석한 실효 호스트 기준으로 판정합니다(src/main/github/github-enterprise-repository.ts:249-251, 별칭 해석은 :231-247).

  • github-enterprise-repository.ts:151-152가 gh auth status를 실행하고, :98-122(authenticatedHostFromInventory)가 리모트 호스트를 그 목록과 대조합니다. 목록에 없으면 :213-263가 null을 돌려주고 GitHub 경로는 그대로 포기합니다.
  • 호출 체인: src/main/source-control/forge-provider.ts:136-137 → src/main/github/client.ts:1725-1735(getRepoSlug) → src/main/github/github-api-repository.ts:114-132 → github-enterprise-repository.ts:266-270.
  • 코드 주석이 이 계약을 명시합니다 — "gh는 github.com / GitHub Enterprise 자격증명만 관리하므로, gh auth status가 로그인됐다고 보고하는 호스트는 확정적으로 GitHub 호스트다"(github-enterprise-repository.ts:24-25).

따라서 gh auth login --hostname github.samsungds.net은 선택이 아니라 필수입니다. PR·이슈가 안 보인다는 신고가 들어오면 정책 파일이 아니라 gh auth status부터 확인하세요. gh가 아예 없거나 spawn에 실패하면 판정이 "미확정"으로 남아 캐시되지 않습니다(:155-159).

정책의 githubEnterpriseHost가 하는 일

세 가지입니다. GitHub로 인식시키는 기능은 여기 없습니다(그건 위의 gh auth status입니다).

  1. 네트워크 허용목록 자동 추가 — enforceNetworkAllowlist를 켠 플릿에서 관리자가 GHES 호스트를 allowedNetworkHosts에 한 번 더 적지 않아도 되도록 자동으로 넣습니다(src/shared/enterprise-policy.ts:370-371).
  2. GHES 로그인 대상 기본값 — 설정 → GitHub Enterprise 팬에서 사용자가 호스트를 저장한 적이 없으면 이 값이 gh auth login --hostname <host>의 대상이 됩니다(src/main/ipc/github-enterprise.ts:83-86). 로그인 뒤 실제로 gh가 어느 호스트로 나가는지는 gh 자신의 설정이 정하고, 팬은 그 "실효 호스트"를 별도로 표시합니다(src/main/github/effective-github-host.ts).
  3. GHES 퍼머링크 인식 — GHES는 /owner/repo/blob/<ref>/<path>#L<n> 형태를 호스트 루트에 그대로 서비스하므로, 호스트를 알아보는 것만으로 blob/commit URL이 GitHub 프로바이더로 매핑됩니다(src/main/git/hosted-remote-url.ts:38-42). 같은 값이 disableVendorLinks의 예외로도 쓰여 사내 GHES 링크는 벤더 링크로 차단되지 않습니다(src/main/enterprise/enterprise-vendor-link-guard.ts:80-83).

예전 README가 첫 번째 역할로 적었던 "Gitea 폴백 오인 차단"은 이제 해당하지 않습니다 — Bitbucket·Azure DevOps·Gitea 연동을 커밋 4d58e5f21c에서 코드째 제거했고, provider 탐색은 GitLab → GitHub 둘뿐이라 어느 쪽도 아니면 unsupported로 끝납니다(잘못된 호스트로 요청이 나가지 않습니다).

// 정책 파일 (§4)
{ "githubEnterpriseHost": "github.samsungds.net" }

값이 없으면 gh의 GH_HOST → gh 자체 설정의 기본 호스트(gh auth login --hostname <ghes>가 쓰는 hosts.yml, 로그인 호스트가 정확히 하나일 때) 순으로 폴백합니다(src/shared/enterprise-policy.ts:364-367, src/main/github/gh-config-host.ts). 프로토콜·포트·경로·자격증명이 붙어 있어도 호스트명만 정규화해 씁니다(:227-240).

git 바이너리(clone/fetch/push·워크트리) 전제조건

PR/이슈 표시는 gh API를 타지만, 클론·페치·푸시, 그리고 워크트리 생성 시 base 브랜치 페치는 git 바이너리가 직접 origin(= 사내 GHES)로 나갑니다. git worktree add 자체는 로컬이지만 base 브랜치가 로컬에 없으면 생성 과정에서 git fetch origin이 일어나므로, 아래가 갖춰져야 워크트리가 막힘없이 만들어집니다.

  • git 자격증명: gh auth login만으로는 git HTTPS 인증이 자동 설정되지 않습니다. gh auth setup-git --hostname github.samsungds.net(gh를 git credential helper로 등록)이나 Windows 자격증명 관리자/SSH 키를 함께 설정하세요.
  • 사설 CA: NODE_EXTRA_CA_CERTS는 Orca의 Node 계층에만 적용되고 git/gh 바이너리 TLS엔 무관합니다. git은 TLS 백엔드에 따라 신뢰 저장소가 다릅니다:
    • http.sslBackend=schannel (Git for Windows 설치 기본값) — Windows 인증서 저장소를 씁니다. 사내 루트 CA를 (보통 GPO로) 저장소에 넣어야 하며, http.sslCAInfo는 schannel에서 무시됩니다.
    • http.sslBackend=openssl — 이때만 git config --global http.sslCAInfo C:\path\to\corp-root-ca.pem이 의미가 있습니다. 먼저 git config --global http.sslBackend openssl로 전환해야 합니다.
    • 현재 값 확인: git config --get http.sslBackend
  • 프록시: HTTPS_PROXY가 외부 프록시를 가리키면 내부 호스트를 NO_PROXY에 넣거나(setx NO_PROXY "github.samsungds.net,.samsungds.net") 프록시가 내부 라우팅을 하도록 하세요. git 서브프로세스는 이 env를 상속합니다.

3. AWS Bedrock으로 Claude 사용

Bedrock 인증은 Claude Code CLI 자체가 자격증명 체인으로 처리하고, 그 자격증명은 gateway-cli가 소유합니다. Orca는 이 흐름에 관여하지 않습니다.

3.1 결론부터 — 자격증명은 gateway-cli가 소유하고 Orca는 손대지 않습니다

사내 인증은 gateway-cli login(인자 없음)으로 통일됐습니다. OIDC 브라우저 로그인을 마치면 게이트웨이가 virtual key를 자동 발급하고, 그 키를 에이전트까지 전달하는 것도 gateway-cli의 몫입니다. 프로필이라는 개념 자체가 이 레인에서 사라졌으므로 사용자가 ~/.aws/config를 관리하거나 AWS_PROFILE을 지정할 일이 없습니다. 상태 확인은 gateway-cli verify입니다.

Orca의 책임 경계는 AWS SSO 시절과 동일합니다 — 로그인 명령을 실행하고 상태를 보여줄 뿐, 토큰·키를 읽지도 저장하지도 않고 환경변수도 주입하지 않습니다. 그 경계가 실제로 지켜지는지 코드로 확인한 항목입니다.

확인 항목 결과 근거
Orca가 virtual key/토큰을 읽거나 저장하는가 아니오. 이 레인이 다루는 것은 CLI 실행과 그 출력뿐입니다 src/shared/gateway-auth.ts 헤더 주석이 계약을 명시. src/main/gateway/에 자격증명 저장소가 없습니다
Orca가 자격증명 환경변수를 주입하는가 아니오. virtual key 전달은 gateway-cli가 알아서 합니다 buildGatewayCommandEnv()가 만드는 것은 CLI 자신을 스폰할 때 쓰는 env(= process.env + Windows에서 레지스트리 PATH 재병합)이고, 에이전트 PTY 환경이 아닙니다
PTY 환경에 화이트리스트가 있는가 아니오. process.env 전체를 상속한 뒤 소수의 명시적 delete만 적용 providers/local-pty-provider.ts·daemon/pty-subprocess.ts의 stripInheritedBuildModeEnv(process.env) 스프레드
Orca가 지우는 AWS_* 변수 AWS_BEARER_TOKEN_BEDROCK 단 하나 (CLAUDE_AUTH_ENV_VARS) src/main/claude-accounts/environment.ts. ⚠️ virtual key 방식이 이 변수를 쓰는지는 미확인 — 쓴다면 §3.3의 disableManagedClaudeAccounts가 "권장"이 아니라 동작 조건이 됩니다
HOME/USERPROFILE을 바꾸는가 아니오. 프로덕션 코드의 모든 참조가 읽기입니다 → gateway-cli가 자기 설정·캐시를 정상적으로 찾습니다 읽기 지점: providers/pty-default-cwd.ts, relay/pty-shell-utils.ts, relay/relay-command-env.ts. 재검증: grep -rn USERPROFILE src/main src/shared src/relay
CLAUDE_CONFIG_DIR을 리다이렉트하는가 상속값이 이미 있을 때만. 사내는 설정하지 않으므로 %USERPROFILE%\.claude가 그대로 쓰입니다 claude-accounts/runtime-paths.ts의 getRuntimePaths()
gateway-cli를 어디서 찾는가 PATH만 해석합니다 src/main/gateway/gateway-cli-command.ts의 resolveGatewayCommand(). 설치 경로가 미확인이라 경로 추측을 넣지 않았습니다

HOME/USERPROFILE을 건드리지 않는다는 마지막 두 항목이 여전히 핵심입니다 — gateway-cli가 로그인 상태를 어디에 두든, Orca가 그 위치를 옮기지 않기 때문에 앱 안에서 로그인하든 터미널에서 로그인하든 같은 세션이 됩니다.

앱 안에서 로그인하기 (이 포크가 추가) — 터미널을 열지 않아도 **설정 → AI 제공업체 계정 → "사내 게이트웨이 로그인"**에서 처리할 수 있습니다.

  • 버튼은 gateway-cli login을 인자 없이 실행합니다(runGatewayLogin()). 브라우저는 gateway-cli가 직접 띄웁니다. 고를 프로필이 없으므로 화면에도 선택 UI가 없습니다. 로그인 중 CLI가 사용자 코드나 인증 URL을 인쇄하면 그대로 화면에 올려 줍니다(GatewayLoginProgress) — 브라우저가 안 뜨는 환경을 위한 것이고, 그 값을 만들어 내는 것은 Orca가 아니라 CLI입니다.
  • 로그인 여부·만료·신원은 gateway-cli verify의 실행 결과로 판정합니다(runGatewayVerify()). AWS SSO 시절과 달라진 실질적인 변화입니다: 예전에는 AWS CLI 토큰 캐시의 expiresAt을 네트워크 없이 파일에서 읽었지만, 이제는 상태를 새로 고칠 때마다 CLI가 최대 두 번 실행됩니다 — 설치 감지용 gateway-cli --version과 gateway-cli verify. 그 실행이 네트워크를 타는지는 gateway-cli 구현에 달려 있어 미확인입니다.
  • verify의 출력 형식은 확정되지 않았습니다. 그래서 파서(src/shared/gateway-cli-output.ts)가 JSON → 텍스트 → 종료 코드 순으로 방어적으로 읽고, 알아보지 못한 항목은 null로 남깁니다. 배지에 만료가 안 보이면 "만료를 알 수 없음"이지 "만료됨"이 아닙니다. 화면에 올라가는 detail 문자열은 파서가 비밀 후보(키·토큰 형태의 대입문, 숫자를 포함한 20자 이상 불투명 문자열)를 ***로 가린 뒤 넘깁니다.
  • gateway-cli가 PATH에 없으면 로그인 버튼 대신 미설치 경고가 뜹니다(detectGatewayCli()).
  • 로그아웃 버튼은 없습니다. gateway-cli logout이 존재하는지 확인되지 않아, 없는 하위 명령을 발명하지 않았습니다.

로컬 전용입니다. 이 버튼은 이 PC의 gateway-cli를 로그인시킵니다. WSL 게스트와 SSH 원격 호스트는 각자 자기 세션을 쓰므로 그 안에서 따로 gateway-cli login을 실행해야 합니다(§3.4). disableManagedClaudeAccounts·enforceNetworkAllowlist 같은 정책 스위치는 이 경로에 관여하지 않습니다 — 자식 프로세스의 egress는 허용목록 밖입니다(감사 문서 §0.2 #25).

3.2 모델·리전·플래그는 Claude Code 설정 파일에 (버킷 C)

// %USERPROFILE%\.claude\settings.json  ← 아래 블록을 읽는 것은 Claude Code CLI입니다
// (Orca는 같은 파일의 hooks/statusLine 키만 씁니다 — §0.C)
{
  "env": {
    "CLAUDE_CODE_USE_BEDROCK": "1",
    "AWS_REGION": "us-east-1",
    "ANTHROPIC_MODEL": "<Bedrock inference profile ARN 또는 모델 ID>"
  }
}

awsAuthRefresh는 예시에서 뺐습니다. 예전 값이던 "aws sso login"은 더 이상 맞지 않고, 새 방식에서 이 키를 어떻게 채워야 하는지는 확인되지 않았습니다 — 아래 NOTE를 보세요.

Note

위 키 이름과 의미는 Claude Code CLI의 계약이며 이 저장소 코드로 검증할 수 없습니다. 정확한 스펙은 Claude Code 문서를 따르세요. 이 저장소에서 검증한 것은 "Orca가 이 키들(env, awsAuthRefresh)에 관여하지 않고 경로 리다이렉트도 하지 않는다"는 사실뿐입니다. 파일 자체는 Orca도 씁니다 — hooks/statusLine 키에 한정되며 나머지 키는 보존되지만, 그래서 이 파일에 주석이나 후행 쉼표를 넣으면 안 됩니다(§0.C).

awsAuthRefresh를 새 방식에서 어떻게 채워야 하는지는 미확인입니다. 이 키는 Claude Code CLI가 자격증명 만료 시 실행하는 명령이므로, 값으로 gateway-cli login을 넣을 수는 있지만 게이트웨이 방식에서 그 훅이 필요한지, 브라우저를 띄우는 명령을 여기 두는 것이 맞는지는 이 저장소로 판정할 수 없습니다. 사내 인증 담당의 안내를 따르고, 확인 전에는 키를 비워 두는 편이 안전합니다 — 세션 시작 전에 gateway-cli verify로 상태를 한 번 보는 것으로 대체할 수 있습니다.

3.3 반드시 켜야 하는 정책 키 두 개

  • disableManagedClaudeAccounts — 권장이 아니라 필수입니다. 이 기능은 자식 환경에서 ANTHROPIC_API_KEY·ANTHROPIC_AUTH_TOKEN·CLAUDE_CODE_OAUTH_TOKEN·**AWS_BEARER_TOKEN_BEDROCK**을, 그리고 인증 정보가 담긴 것으로 판정되면 ANTHROPIC_CUSTOM_HEADERS까지 제거합니다(claude-accounts/environment.ts:3-8, :22-29). Windows 호스트에서는 관리형 계정을 선택한 동안에만 제거되지만(claude-accounts/runtime-auth-service.ts:667), WSL 런타임을 고르면 관리형 계정이 하나도 없어도 제거가 켜지고(:647, :657 — upstream v1.4.155는 이 두 곳이 stripAuthEnv: true 하드코딩이었고 포크가 !managedAccountsDisabled로 바꿨습니다), 런치 환경에 해당 변수가 있으면 PTY 스폰이 This Claude launch defines explicit Anthropic auth environment variables.로 하드 실패합니다(src/main/ipc/pty.ts:4576-4580, :6164-6168). 스위치를 켜면 활성 계정이 null로 고정되고(runtime-auth-service.ts:613-616) 하드코딩 호출자까지 최후 방어선에서 막혀(environment.ts:22) 이 실패 조건이 사라지며, platform.claude.com으로 나가는 OAuth 토큰 회전도 함수 진입부에서 차단됩니다(claude-accounts/oauth-refresh.ts:131-133). ⚠️ 게이트웨이가 발급하는 virtual key가 AWS_BEARER_TOKEN_BEDROCK으로 전달되는지는 확인 필요입니다. 전달되지 않는다면 오늘 당장 깨지지 않고, 전달된다면 이 스위치는 권장이 아니라 WSL 런치의 동작 조건입니다. 어느 쪽이든 누군가 bearer token으로 우회하거나 ANTHROPIC_API_KEY를 병기하는 순간 WSL 런치가 즉시 실패합니다. SSH 경로도 같은 스위치로 함께 정리됩니다 — 스트립이 켜져 있으면 envToDelete가 릴레이까지 전송되어 원격 spawn env에서 해당 변수가 삭제되고(src/main/providers/ssh-pty-spawn-request.ts:21), claudeAuth.envPatch가 connectionId 유무와 무관하게 SSH env에 병합됩니다(pty.ts:4614-4616, :6174-6176). 후자는 사내에서 CLAUDE_CONFIG_DIR을 설정하지 않아 지금은 빈 객체이므로 실제 피해가 없지만, 누군가 이 변수를 심으면 로컬 Windows 경로가 원격 셸에 들어갑니다.

  • disableUsagePolling — Bedrock 전용 머신에서도 api.anthropic.com으로 나갑니다. Orca는 창이 보이고 포커스된 동안(src/main/rate-limits/service.ts:873-882 shouldBackgroundPoll) 15분 주기로(:82 DEFAULT_POLL_MS) https://api.anthropic.com/api/oauth/usage(src/main/rate-limits/claude-fetcher.ts:46)를 호출합니다. 이 호출은 Orca의 관리형 계정 등록 여부와 무관하고, 과거 OAuth 로그인 흔적(~/.claude/.credentials.json 등, claude-fetcher.ts:190-194)만 있으면 켜집니다. 게다가 이 경로에는 claude CLI를 숨겨서 스폰하는 PTY도 있어(src/main/rate-limits/claude-pty.ts:244-245, spawn env = {...process.env}) Bedrock 환경에서는 예상치 못한 Bedrock 호출·과금이 발생할 수 있습니다.

lockdown: true면 둘 다 자동으로 켜집니다(enterprise-policy.ts:157-175, :358-362).

3.4 함정 — 실제로 사람들이 밟는 것들

  • 설정 → Agents의 에이전트별 env는 OS 값을 덮어씁니다(shadow). 빈 값을 넣으면 변수가 삭제되는 게 아니라 빈 문자열로 덮어써지고 그 빈 문자열이 이깁니다. AWS_REGION= 한 줄이 {AWS_REGION: ''}로 파싱되고(src/renderer/src/components/settings/agent-default-env-draft.ts:24-32), 정규화가 빈 문자열을 보존하며(src/shared/tui-agent-launch-defaults.ts:62-67), 병합에서 override가 최종 승자입니다(src/shared/git-credential-prompt-env.ts:11). 지우려면 항목 자체를 삭제하세요.
  • WSL 게스트는 호스트와 별개로 로그인해야 합니다. 게스트의 게이트웨이 로그인 상태는 Windows 호스트의 것과 별개입니다 — virtual key가 어디에 보관되는지는 미확인이지만, 게스트는 자기 홈 디렉터리를 쓰고 Orca는 HOME/USERPROFILE을 다리로 놓지 않으므로(§3.1) 호스트 로그인이 게스트에 보일 길이 없습니다. AWS SSO 시절 토큰 캐시가 호스트와 게스트에서 다른 파일이었던 것과 같은 구조이고, 로그인 주체만 gateway-cli로 바뀌었습니다. 그리고 wsl.exe는 WSLENV에 등재된 변수만 가져오는데 Orca가 등재하는 것은 ORCA_* 계열(src/main/pty/wsl-orca-env.ts:77-102), CODEX_HOME/ORCA_CODEX_HOME/CLAUDE_CONFIG_DIR/Hermes·p10k 변수, git credential 가드 키(src/shared/git-credential-prompt-env.ts:112)뿐이라 자격증명 성격의 변수는 어느 등재 지점에도 없습니다. 따라서 WSL 프로젝트는 게스트 안에서 따로 구성해야 합니다:
    1. 게스트에 gateway-cli를 설치하고 게스트 안에서 gateway-cli login을 별도로 실행 — Windows 쪽 로그인은 게스트에 보이지 않습니다. (gateway-cli가 WSL 게스트에서 어떻게 동작하는지, 게스트에 설치본이 제공되는지는 미확인입니다. 사내 배포 담당에게 확인하세요.)
    2. 게스트에 Claude Code CLI를 설치하고 게스트의 ~/.claude/settings.json에 Bedrock 블록을 둡니다. Windows 쪽 파일은 읽히지 않습니다.
    3. 리전/플래그처럼 자격증명이 아닌 값만 Windows에서 넘기고 싶다면 setx WSLENV "AWS_REGION/u:CLAUDE_CODE_USE_BEDROCK/u"처럼 WSLENV를 직접 채우세요 — Orca는 기존 WSLENV를 보존하고 append만 하므로 이 값이 살아남습니다(src/shared/wsl-env.ts:5-16). 자격증명 자체는 이 방법으로 넘길 수 없습니다.
  • SSH 원격 호스트도 각자 로그인해야 합니다. 호스트 env를 조립하는 buildPtyHostEnv는 SSH 경로에서 아예 호출되지 않고(계약은 src/main/ipc/pty.ts:1684-1689 주석, 게이트는 :6125-6128의 !args.connectionId), 릴레이는 자기 자신의 process.env(SSH exec 채널이 준 환경)에 렌더러가 보낸 env만 얹어 PTY를 만듭니다(src/relay/pty-handler.ts:596-607). 따라서 원격에서 별도로 gateway-cli login을 수행하고, 원격의 ~/.claude/settings.json과 로그인 셸 프로필에 설정을 두어야 합니다. (원격 호스트에서의 gateway-cli 동작 — 특히 브라우저를 띄울 수 없는 헤드리스 호스트에서 OIDC 흐름이 어떻게 끝나는지 — 도 미확인입니다.)
  • enforceNetworkAllowlist는 Bedrock 호출과도, 게이트웨이 로그인과도 무관합니다. Electron session과 메인 프로세스 fetch만 감싸므로(src/main/enterprise/enterprise-network-guard.ts:128-136) 자식 프로세스(Claude Code CLI, gateway-cli, git, gh)의 egress에는 적용되지 않습니다. bedrock-runtime.<region>.amazonaws.com이나 사내 IdP·게이트웨이 호스트를 allowedNetworkHosts에 넣을 필요가 없고, 넣어도 CLI에는 아무 효과가 없습니다.
  • setx 후 데몬 staleness — §0의 경고 박스를 참고하세요. Bedrock 설정을 OS 환경 변수로 넣었을 때 "설정했는데 안 먹는다"의 1순위 원인입니다.

4. 외부 연동 잠금 — 관리자 소유 정책 파일

4.1 정책 파일을 어디에 두는가 — 먼저 파싱에 성공한 파일이 이깁니다

탐색 순서를 조립하는 곳은 enterprise-policy-file.ts:89-114이고, 그 후보를 순서대로 읽다가 처음 파싱에 성공한 파일에서 멈추는 곳은 :161-215입니다(설계 의도는 :1-17 주석). 따라서 사용자별 파일이 위 후보를 완화할 수 없습니다.

패키징 빌드 — 사용자 PC에 설치된 .exe. 플릿에서 유일하게 의미 있는 순서입니다.

순위 위치 비고
1 머신 전역 — %ProgramData%\Orca\enterprise-policy.json (:58-68) 중앙(GPO/Intune)에서 값을 덮어쓸 위치. 선택입니다
2 번들 — <설치폴더>\resources\enterprise-policy.json (:236-244) 설치 프로그램에 내장된 기본값. 저장소 원본은 resources/enterprise-policy.json
3 ORCA_ENTERPRISE_POLICY 환경 변수 명시 경로가 후보에 추가될 뿐. off/none/disabled/false/0은 무시됩니다(:41, :97)
4 사용자별 — <userData>\enterprise-policy.json 개인 테스트용

🧩 1순위 파일은 Windows 설치 프로그램이 없을 때만 만듭니다(§5.2). 그 파일이 생기면 2순위 번들은 그 파일 밑의 바닥선으로만 쓰입니다.

비패키징(pnpm dev·vitest)에서만 ORCA_ENTERPRISE_POLICY가 1순위를 가져가고, 무력화 값으로 탐색 전체를 끌 수 있습니다(:99-106; 테스트 스위트가 이 값을 씁니다). 번들 후보는 비패키징에서 최후순위로만 들어옵니다(:110-111 — 후보는 [머신 전역, 사용자별, 체크아웃의 resources/enterprise-policy.json], 경로 해석은 :258-272 devCheckoutPolicyPath, 설계 의도는 :80-87 주석). 그래서 pnpm dev가 별도 설정 없이도 플릿 UI를 보여 주고(macOS dev UI 점검), 무력화 값을 쓰는 vitest·E2E 격리는 그대로 유지됩니다.

📦 빌드하는 사람에게: 정책을 바꾸는 1차 경로는 resources/enterprise-policy.json을 고치고 다시 빌드하는 것입니다. 그 파일이 config/electron-builder.config.cjs의 commonExtraResources로 실려 위 2순위가 됩니다(3 OS 공통). 패키징 후 afterPack이 산출물에 실제로 들어갔는지, JSONC로 파싱되는지, lockdown: true인지 검사하고 아니면 빌드를 실패시킵니다(config/scripts/verify-packaged-enterprise-policy.cjs) — electron-builder는 extraResources 원본이 없어도 경고 한 줄만 남기고 넘어가기 때문입니다.

🔒 환경 변수는 사내 잠금을 끌 수 없습니다 — 이게 보안 속성입니다. Windows에서 표준 사용자는 관리자 권한 없이 자기 계정의 환경 변수를 만들 수 있습니다. setx ORCA_ENTERPRISE_POLICY off 한 줄로 잠금이 풀린다면 그건 정책이 아닙니다. 그래서 패키징 빌드에서는 이 변수가 후보를 추가만 하고, 머신 전역 파일과 번들 정책이 항상 먼저 탐색됩니다(enterprise-policy-file.ts:70-88 주석, 분기는 :110-113). 판정 신호는 app.isPackaged입니다 — 표준 사용자가 바꿀 수 없는 유일한 신호이기 때문입니다(:228-234). 같은 이유로 번들 후보는 사용자별(%APPDATA%) 후보보다 위입니다 — 아니면 %APPDATA%\Orca\enterprise-policy.json에 {} 하나로 풀립니다.

배포상의 결론: 설치만으로 잠기고, 값을 중앙에서 바꿔야 할 때만 1순위 머신 전역 경로에 파일을 놓고 사용자가 쓰지 못하도록 ACL을 거세요(§5.2). 환경 변수로 커스텀 경로를 지정하는 방식은 개발·검증용이지 플릿용이 아닙니다.

머신 전역 위치가 이 설계의 핵심입니다. setx는 사용자별 상태를 쓰기 때문에, 같은 PC의 다른 Windows 프로필·서비스 계정·앞으로 새로 만들어질 프로필은 전부 잠기지 않은 채로 남습니다(enterprise-policy-file.ts:9-11). 번들 정책도 같은 성질을 가집니다 — 설치 폴더의 파일 하나가 그 설치본을 쓰는 모든 실행에 적용됩니다. 다만 per-user 설치라 그 폴더는 사용자 소유입니다(외부 연동 감사 §0.2 #21).

정책은 프로세스당 한 번만 읽고 캐시합니다(:325-368). 파일을 바꾸면 Orca를 재시작해야 반영됩니다.

4.2 파일 형식

JSONC입니다 — // 주석과 후행 쉼표를 허용합니다(enterprise-policy-file.ts:184-186). UTF-8 BOM과 PowerShell 5.1의 UTF-16LE 출력도 자동으로 처리합니다(:166-171, :181-186).

// %ProgramData%\Orca\enterprise-policy.json
{
  "lockdown": true,
  "githubEnterpriseHost": "github.samsungds.net",

  // 개별 예외: lockdown 아래에서도 이 기능만 되살립니다
  "disableSpellcheck": false
}
키 기본값 무엇을 끄는가 (검증한 근거)
lockdown false 마스터 스위치. 추가로 Chromium의 DNS-over-HTTPS 자동 승격을 끄고 OS 리졸버로 고정합니다(src/main/enterprise/enterprise-secure-dns.ts:19-24, 배선 src/main/index.ts:2084), node:https 직접 다운로드를 거부합니다(enterprise-direct-download-guard.ts:17-25)
disableTelemetry lockdown 상속 PostHog 텔레메트리(telemetry/consent.ts:88) + 진단 번들 업로드 레인(observability/index.ts:103, :120-133). 로컬 NDJSON 로그는 계속 기록됩니다 — 네트워크만 막습니다. 앱 내 피드백·크래시 리포트 전송은 정책이 아니라 코드에서 제거되었습니다
disableAutoUpdate lockdown 상속 사내 GHES(githubEnterpriseHost)의 updateReleaseRepository(기본 daegun-kim/Orca_ds) 릴리스 태그를 읽어 "새 버전이 있습니다" 팝업을 띄우는 레인을 끕니다. 게이트는 app-update/app-update-check-service.ts의 check() 한 곳. ⚠️ 벤더 인앱 업데이터는 여전히 코드에 없습니다 — 이 키를 false로 둬도 다운로드·설치·자가교체는 일어나지 않고 electron-updater도 의존성이 아닙니다. 팝업의 유일한 동작은 사내 릴리스 페이지를 브라우저로 여는 것입니다
updateReleaseRepository null → daegun-kim/Orca_ds 위 릴리스 조회가 볼 저장소(OWNER/REPO). lockdown을 상속하지 않는 값입니다. URL은 받지 않습니다
disableSkillBundleUpdate lockdown 상속 사내 GHES(githubEnterpriseHost)의 daegun-kim/ORCA_skill main 브랜치를 읽어 "사내 스킬 번들 새 판" 알림을 띄우고, [업데이트] 시 번들 스킬 전부를 ~/.agents/skills·~/.claude/skills에서 폴더째 교체하는 레인을 끕니다. 게이트는 corporate-skills/corporate-skill-bundle-service.ts의 check()·update(). disableAutoUpdate와 따로인 이유: 이 레인은 사용자 홈에 씁니다. ⚠️ 머신 전역 파일을 배포한다면 이 키를 false로 직접 적어야 켜집니다(§5.2)
disableStarNag lockdown 상속 github.com SaaS로 가는 star 조회/쓰기 — github/client.ts:341(checkOrcaStarred), :527(starOrca). 게이트를 서비스가 아니라 클라이언트 함수에 뒀습니다 — 넛지 서비스(star-nag/service.ts:121) 말고도 star-nag/direct-star-attempt.ts:9, star-nag/agent-value-moment.ts:46, IPC 핸들러 ipc/github.ts:1210·:1213이 같은 함수로 들어옵니다
disableCloudRelay lockdown 상속 orca-profiles/profile-cloud-auth-config.ts:82이 "미구성"으로 응답 → Orca Cloud 로그인, 조직 멤버 조회(orca-profiles/profile-cloud-org-members-service.ts:119), 그리고 데스크톱↔모바일 페어링 릴레이(src/main/index.ts:3065-3068이 configured일 때만 DesktopRelayService를 만듭니다)가 한꺼번에 꺼집니다. 단일 초크포인트임을 :76-78 주석이 명시
disableUsagePolling lockdown 상속 AI 벤더 사용량/레이트리밋 폴링. 게이트는 rate-limits/service/service-state.ts:32, 진입점은 start()(:353), Codex 리셋 크레딧(:478), 계정 스위처 프리뷰 2종(:572, :652), fetchAll(:998), fetchCodexOnly(:1063), fetchClaudeOnly(:1125), fetchGrokOnly(:1190). 벤더 백엔드로 POST하는 경로는 예외를 던집니다
disableManagedClaudeAccounts lockdown 상속 Orca 관리형 Claude 계정 — platform.claude.com OAuth 토큰 회전(게이트 claude-accounts/oauth-refresh.ts:131-133)과 에이전트 PTY로 가는 환경에서 AWS Bedrock 자격증명을 지우는 동작(게이트 runtime-auth-service.ts:613-616 + environment.ts:22)을 함께 끕니다. Bedrock 플릿에서는 필수 — §3.3
disableSpellcheck lockdown 상속 Chromium 맞춤법 사전 CDN 다운로드. 자체 세션을 갖는 WebContents는 메인 창의 게이트를 상속하지 않으므로 여섯 곳에 개별로 걸려 있습니다: 메인 창(window/createMainWindow.ts:306), webview 게스트(:494), 임베디드 브라우저 팝업(browser/browser-manager-guest-popup-policy.ts:30), 오프스크린 브라우저(browser/offscreen-browser-backend.ts:45), 대시보드 팝아웃(window/dashboard-popout-window.ts:181), PDF 렌더러(lib/html-to-pdf.ts:46)
enforceNetworkAllowlist 항상 false (상속 안 함) 아래 4.3
allowedNetworkHosts [] 허용 호스트 목록. enforceNetworkAllowlist가 켜졌을 때만 의미가 있습니다

전체 스키마와 예제는 엔터프라이즈 정책 파일 레퍼런스 를 보세요.

값 해석 규칙 (enterprise-policy.ts:200-223)

  • JSON true / false가 정식입니다. 문자열 "true"/"yes"/"on"/"1"과 "false"/"no"/"off"/"0"도 받습니다(대소문자·공백 무관 — :190-191).
  • 알아볼 수 없는 값은 "꺼짐"이 아니라 "없음"으로 취급되어 lockdown을 상속하고, 경고를 남깁니다. 관리자 오타가 조용히 머신을 풀어버리는 사고를 막기 위한 설계입니다(:197-199).
  • 모르는 키도 경고합니다(:352-356). JSON 자체가 깨졌으면 일부만 적용하지 않고 파일 전체를 버리고 다음 후보로 넘어갑니다(enterprise-policy-file.ts:187-194) — 번들 정책이 있으면 그쪽이 적용되므로, 오타 하나가 플릿을 풀어버리지는 않습니다(§4.1).
  • 경고는 [enterprise-policy] 접두사로 stderr에 나가고 동시에 버퍼링되어 NDJSON 로그에 남습니다(enterprise-policy-file.ts:131-136). Start Menu로 띄운 Windows GUI 프로세스에는 콘솔이 없어 stderr가 사라지기 때문입니다(§5.2).

4.3 네트워크 허용 목록 (opt-in)

enforceNetworkAllowlist는 lockdown을 상속하지 않습니다. 잘못된 허용 목록은 기능 스위치와 달리 배포 전체를 깨뜨릴 수 있어서 관리자가 명시적으로 켜야 합니다(enterprise-policy.ts:386-388, enterprise-network-guard.ts:15-16).

켜면 두 곳을 막습니다(enterprise-network-guard.ts:79-97, :99-122):

  • session.defaultSession의 webRequest.onBeforeRequest — 이 세션을 지나는 요청. 렌더러 요청이 대표적입니다. 로그 라벨은 renderer request(:93) (세션 범위는 Electron 계약이지 이 저장소 코드의 사실은 아닙니다 — 실제 차단 여부는 배포 전 1대에서 확인하세요.)
  • 메인 프로세스의 global fetch 래퍼(:111, 라벨 main-process fetch) — Node의 global fetch를 쓰는 메인 클라이언트용. network/proxy-settings.ts의 사내 프록시는 Electron 세션에만 적용되어 global fetch를 못 보기 때문입니다(enterprise-network-guard.ts:3-7)

githubEnterpriseHost는 항상 허용 목록에 자동 추가되고(enterprise-policy.ts:369-372), loopback은 언제나 통과합니다(enterprise-network-guard.ts:47-55). 차단은 호스트당 한 줄씩 stderr로 보고됩니다(:36-45).

node:https로 직접 나가는 다운로드는 이 두 레인을 모두 우회하므로 별도 가드가 있습니다 — lockdown이면 무조건 거부, 아니면 allowedNetworkHosts 대조(enterprise-direct-download-guard.ts:17-32). 현재 유일한 적용 지점은 Android 에뮬레이터의 scrcpy.jar 다운로드입니다(src/main/emulator/android/scrcpy-server-download.ts:42).

4.4 잠금이 덮지 않는 것 (정직하게)

  • 에이전트 CLI 자체의 통신 — Claude Code, Codex 등이 어디로 나가는지는 Orca의 통제 밖입니다. Orca는 PTY만 띄웁니다. 허용 목록도 자식 프로세스에는 적용되지 않습니다(§3.4).
  • 임베디드 브라우저 — persist: 파티션에서 돌며 허용 목록에서 의도적으로 제외됩니다. 임의 사이트 탐색이 그 기능의 목적이고, 해당 파티션의 단 하나뿐인 onBeforeRequest 슬롯은 이미 인증서 게이트가 쓰고 있습니다(enterprise-network-guard.ts:9-13).
  • 패키징된 렌더러에 CSP가 없습니다 — 에이전트 카탈로그의 favicon/아바타/첨부 URL을 그대로 로드하기 때문입니다(enterprise-network-guard.ts:3-4, src/renderer/index.html:6). 이 구멍을 실제로 막는 유일한 수단이 enforceNetworkAllowlist입니다.
  • 로컬 진단 로그 — disableTelemetry는 egress만 끕니다. NDJSON 파일은 계속 기록됩니다(observability/index.ts:120-133). 로컬 파일까지 멈추려면 정책 키가 아니라 upstream의 ORCA_DIAGNOSTICS_DISABLED 환경 변수를 써야 하며(:113-118), 그러면 §5.2의 확인 수단도 사라집니다.

어떤 기능이 어떤 호스트로 나가는지 전체 목록은 외부 연동 감사 및 차단 계획 을 참고하세요.


5. 사내 배포(롤아웃)

5.1 산출물 — per-user NSIS 설치 프로그램

config/electron-builder.config.cjs:356-365의 nsis 블록은 oneClick과 perMachine을 설정하지 않습니다. 산출물 이름은 orca-windows-setup.exe로 고정입니다(:357). 따라서 electron-builder 기본값이 적용되어 원클릭·사용자별 설치가 되고, %LOCALAPPDATA%\Programs\ 아래에 설치되며 관리자 권한이 필요 없습니다. (여기서 코드로 검증한 것은 두 키가 설정되지 않았다는 사실까지입니다. oneClick: true/perMachine: false 기본값과 그에 따른 설치 위치·권한은 electron-builder/NSIS의 계약이므로 배포 전 1대에서 확인하세요.)

  • 무인 설치: orca-windows-setup.exe /S — NSIS 원클릭 설치 프로그램의 표준 동작입니다. (electron-builder/NSIS의 계약이지 이 저장소 코드의 사실은 아닙니다. 배포 전 1대에서 검증하세요.)
  • 관리자 권한이 필요 없으므로 사용자 단위 소프트웨어 배포 채널(Intune 사용자 대상 앱 등)로 밀 수 있습니다.
  • ⚠️ 그 대가: 설치 폴더가 사용자 소유이므로, 여기에 내장한 번들 정책(§5.2)은 표준 사용자가 지울 수 있습니다. 이걸 막으려면 %ProgramData% 배치(ACL 포함) 또는 perMachine 설치가 필요합니다 — 외부 연동 감사 §0.2 #21.
  • 설치·업데이트 때 %ProgramData%\Orca\enterprise-policy.json이 없으면 만듭니다(§5.2). 있으면 건드리지 않고, 제거해도 남깁니다.
  • 제거 시 %LOCALAPPDATA% 아래로 재배치된 터미널 데몬을 정리하는 NSIS 스크립트가 포함되어 있습니다(:361-364, 스크립트는 config/nsis/daemon-host-uninstall.nsh).

5.2 정책 파일 배포 — 이제 선택입니다

설치 프로그램이 기본 정책을 싣고 있습니다. resources/enterprise-policy.json이 extraResources로 설치 폴더에 들어가고 앱이 process.resourcesPath에서 읽으므로, .exe만 설치한 PC도 §4.2의 잠금이 걸린 상태로 뜹니다. 예전 판의 "설치 프로그램은 정책 파일을 만들지 않습니다"는 더 이상 사실이 아닙니다.

설치 프로그램은 1순위 머신 전역 파일도 만듭니다 — 그 경로에 파일이 없을 때만. 설치·업데이트 때 %ProgramData%\Orca\enterprise-policy.json이 없으면 config/nsis/enterprise-policy.machine-seed.json(아래 예시와 같은 내용)으로 만들고, 이미 있으면 내용과 상관없이 그대로 둡니다 — 관리자나 사용자가 바꿨을 수 있기 때문입니다. 제거해도 지우지 않습니다. 훅은 config/nsis/enterprise-policy-machine-seed.nsh이고, kernel32::CopyFileW(bFailIfExists=1)로 "없을 때만"과 쓰기를 한 번에 하며 실패해도 대화상자를 띄우지 않습니다 — ProgramData 쓰기를 막아 둔 PC에서는 조용히 건너뛰고 번들 정책이 그대로 적용됩니다.

  • 시드 내용은 번들 정책과 결과가 같아야 합니다. 머신 전역 파일이 생기면 그 파일이 이기고, 번들 정책은 그 파일의 빈칸을 true 쪽으로만 채웁니다. 그래서 번들 정책이 false로 켜 두는 키(disableAutoUpdate·disableSkillBundleUpdate·disableUnattendedAgentRuns)와 설정 키(githubEnterpriseHost)는 시드에 직접 적혀 있어야 합니다. src/main/enterprise/enterprise-policy-machine-seed.test.ts가 "시드 + 번들 바닥선 = 번들 단독"을 확인하므로, 번들 정책에 false 스위치를 새로 넣으면 시드에도 넣기 전까지 테스트가 빨갛습니다.
  • ⚠️ 이미 시드된 PC는 이후 판의 기본값을 받지 못합니다. 설치 프로그램은 있는 파일을 고치지 않으므로, 뒤에 false로 들어오는 새 스위치는 그 PC에서 lockdown을 상속해 꺼진 채로 남습니다. 그런 판을 배포할 때는 릴리스 노트에 그 키를 적고 파일에 추가하게 하세요.
  • ⚠️ per-user 설치 프로그램이 만든 파일은 그 사용자 소유입니다. 그 사용자는 관리자 권한 없이 이 파일을 고치거나 지울 수 있고, 1순위라 번들 정책보다 우선합니다. 같은 PC의 다른 계정은 읽기만 됩니다. 파일이 없을 때도 사용자가 직접 만들 수 있었으므로 새로 생긴 구멍은 아니지만, 이 파일을 정책으로 삼으려면 아래 ACL 고정이 여전히 필요합니다 — §6-1의 스크립트는 /setowner로 소유권을 가져오므로 시드된 파일에도 그대로 쓸 수 있습니다.

아래 배포는 번들 기본값을 중앙에서 덮어써야 할 때만 하세요 — 값을 바꿔야 하는데 재빌드·재배포를 하고 싶지 않을 때, 부서별로 값이 달라야 할 때, 또는 Intune 콘솔에서 준수 여부를 감시하고 싶을 때입니다.

  • 대상: %ProgramData%\Orca\enterprise-policy.json — 기본 머신 전역 경로를 쓰세요. 패키징 빌드에서 이 경로가 1순위이고 환경 변수도, 번들 정책도 이것을 밀어낼 수 없기 때문입니다(§4.1). ORCA_ENTERPRISE_POLICY는 배포하지 마세요 — 필요하지 않고, 개발·검증용입니다.

  • ⚠️ %ProgramData%\Orca\enterprise-policy.json을 배포한다면 "disableSkillBundleUpdate": false를 추가하세요. 없으면 그 파일의 lockdown: true를 상속해 사내 스킬 번들 갱신(알림·업데이트)이 꺼집니다. 설치 프로그램에 내장된 정책은 이 키를 false로 싣고 있지만, 머신 전역 파일이 채택되면 그 파일이 이깁니다 — 내장 정책은 채택된 파일의 빈칸을 조이는 방향(true)으로만 채우므로 내장된 false가 대신 켜 주지 않습니다(엔터프라이즈 정책 파일 레퍼런스 §2-1). 같은 이유로 "새 버전이 있습니다" 알림을 쓰려면 "disableAutoUpdate": false도 함께 적으세요.

    // %ProgramData%\Orca\enterprise-policy.json
    {
      "lockdown": true,
      "githubEnterpriseHost": "github.samsungds.net",
      "disableAutoUpdate": false, // 사내 GHES 릴리스 태그 → 새 버전 알림
      "disableSkillBundleUpdate": false, // 사내 GHES daegun-kim/ORCA_skill main → 스킬 번들 갱신
      "disableUnattendedAgentRuns": false // 예약 자동화 — 빼면 lockdown 을 상속해 꺼집니다
    }
  • ⚠️ 배포한 파일의 문법을 검증하세요. 깨진 파일은 무시되고 번들 정책으로 되돌아갑니다 — 잠금은 유지되지만 관리자가 의도한 예외가 조용히 사라집니다(§4.1).

  • ACL을 함께 고정하세요 — 사용자가 쓸 수 있는 정책 파일은 정책이 아니라 기본값입니다. 관리자가 넣어 둔 파일은 기본 ACL에서 표준 사용자가 수정·삭제할 수 없지만, %ProgramData% 루트는 표준 사용자도 새 폴더·파일을 만들 수 있으므로 파일이 아직 없는 머신에서는 사용자가 자기 소유의 정책 파일을 먼저 만들 수 있습니다. 배포 스크립트에서 폴더 상속을 끊고 Users를 읽기 전용으로 내리세요 — 구체적인 icacls 명령은 엔터프라이즈 정책 파일 레퍼런스 §6-1에 있습니다. (Windows ACL 동작이지 이 저장소 코드의 사실은 아닙니다.)

  • 수단: GPO 파일 기본 설정, Intune 구성 프로필/스크립트, SCCM 패키지 등 기존 구성 관리 채널. (운영 권고 — 코드가 강제하는 바가 아닙니다.)

  • 순서는 상관없습니다. 정책 파일이 앱보다 먼저 들어가도 되고 나중에 들어가도 되지만, 정책은 프로세스 시작 시 1회만 읽히므로(enterprise-policy-file.ts:325-368) 이미 실행 중인 Orca는 재시작해야 반영됩니다.

적용 확인 — NDJSON 로그의 enterprise.policy 스팬

Start Menu로 띄운 Windows GUI 프로세스에는 콘솔이 없어서 [enterprise-policy] stderr 진단이 그대로 사라집니다. 그래서 정책 해석 결과를 로그에 한 번 기록합니다(src/main/enterprise/enterprise-policy-trace.ts:1-12 주석이 이 이유를 명시).

  • 파일: %APPDATA%\Orca\logs\main.trace.ndjson (src/main/observability/logs-directory.ts:28, :33)
  • 스팬 이름 enterprise.policy. 속성에 실제 채택된 파일 경로(enterprise.policy.source_path, 없으면 (none found)), 탐색한 후보 목록(…searched_paths), lockdown, 각 disable* 값, GHES 호스트, 허용 목록, 그리고 모든 경고가 들어갑니다(enterprise-policy-trace.ts:28-62).
  • 기록 시점은 initObservability() 직후입니다(src/main/index.ts:2309-2312).
  • disableTelemetry/lockdown이 켜져도 로컬 NDJSON은 유지되므로 잠금 상태에서도 이 확인이 동작합니다(observability/index.ts:120-133). 단 ORCA_DIAGNOSTICS_DISABLED를 켜면 이 로그도 사라집니다(:113-118).
Select-String -Path "$env:APPDATA\Orca\logs\main.trace.ndjson" -Pattern "enterprise.policy" | Select-Object -Last 1

기능으로 확인하는 방법(예: ? 메뉴에 "피드백 보내기"가 없는지)도 여전히 유효합니다. 반대로 경고가 없다는 것은 파일을 찾았다는 증거가 아닙니다 — 정책 파일이 아예 없어도 코드는 조용히 지나갑니다(enterprise-policy-file.ts:172-178). 그래서 위 스팬의 enterprise.policy.source_path 속성을 보는 것이 유일하게 확정적인 확인 방법입니다.

5.3 잠긴 사내 빌드와 공개 빌드를 구분하는 법 — 내장 수단이 없습니다

이 포크는 버전 문자열도 앱 식별자도 바꾸지 않습니다. package.json의 version은 upstream 값(1.4.155) 그대로이고, appId는 com.stablyai.orca, productName은 Orca입니다(config/electron-builder.config.cjs:53, :118-119). 즉 앱 안의 버전·이름만으로는 사내 빌드와 공개 빌드를 구분할 수 없습니다.

실무 권고 (운영 관례이지 코드가 보장하는 것이 아닙니다):

  1. 정책 파일 존재 여부를 자산 관리로 감시 — %ProgramData%\Orca\enterprise-policy.json이 없는 머신은 사내 설치 프로그램을 거치지 않았거나 쓰기가 막힌 머신입니다. 다만 사내 설치 프로그램이 그 파일을 만들기 시작했으므로(§5.2) 있다는 것만으로는 관리자 배포인지 알 수 없습니다 — 소유자(icacls)와 lockdown 값까지 보세요.
  2. ORCA_DISABLE_PUBLISH_TARGET=1로 빌드 — publish 타깃이 null이 되어 업데이터 메타데이터가 아예 안 실립니다. 그러면 이 설치본은 upstream 릴리스 피드로 덮어써질 수 없습니다(config/electron-builder.config.cjs:543-551).
  3. 사내 인증서로 서명하고 ORCA_WIN_PUBLISHER_NAME을 그 주체로 지정 — 공개 배포본의 서명 주체와 달라지므로 공개 설치 프로그램이 사내 빌드를 갈아치우지 못합니다. 기본값을 그대로 두면(SignPath Foundation) 공개 빌드가 그대로 받아들여집니다(:326-332). (이 빌드 자체에는 인앱 업데이터가 없습니다.)
  4. 사내 자체 식별이 꼭 필요하면 릴리스 태그·파일명·설치 경로 규약을 사내에서 별도로 정하세요. 앱은 도와주지 않습니다.

6. 원본(upstream) 최신 반영 — fork 동기화

원본 stablyai/orca는 자주 릴리스되므로 주기적으로 최신 변경을 가져옵니다. 전략은 역할 분리입니다.

  • main — 원본 upstream/main의 깨끗한 미러로만 유지(사내 커밋을 올리지 않음). 항상 fast-forward로 갱신됩니다.
  • enterprise/samsungds — 사내 커스터마이즈. 새 릴리스가 나오면 그 위로 재배치(rebase) 합니다.

최초 1회 — upstream 원격 등록

git remote add upstream https://github.com/stablyai/orca.git
git remote -v   # origin=사내 fork, upstream=stablyai/orca 확인

주기적으로 — main 미러 갱신

main에는 사내 커밋이 없으므로 fast-forward만 하면 됩니다.

git fetch upstream --tags --prune
git checkout main
git merge --ff-only upstream/main
git push origin main

더 간단하게는 GitHub 웹의 fork 페이지 상단 "Sync fork" → "Update branch" 버튼으로 main을 원클릭 갱신할 수 있습니다.

사내 커스터마이즈를 새 릴리스 위로 올리기

현재 enterprise/samsungds에는 v1.4.237 가 병합되어 있습니다(git log --oneline --merges -3로 확인). v1.4.159부터 v1.4.184까지 매번 병합(merge) 으로 올렸습니다 — 강제 푸시가 필요 없고, 사내에서 이미 받아 간 커밋이 재작성되지 않습니다.

git fetch upstream --tags --prune
git checkout enterprise/samsungds
git merge v1.4.165                  # 예: 새 릴리스 태그
# 충돌 해결 후:
git add -A ; git commit
git push origin enterprise/samsungds
  • 릴리스 태그는 서로 조상 관계가 아닙니다. upstream은 릴리스마다 main에서 새로 분기해 픽스를 체리픽하므로 v1.4.159는 v1.4.162의 조상이 아니고, --ff-only로는 절대 올라가지 않습니다. 그래서 main은 upstream/main 미러로만 두고, 릴리스 태그는 이 브랜치에 병합합니다.
  • rebase로도 되지만 강제 푸시(--force-with-lease)가 필요하고, 위 이유로 재생 대상이 릴리스 태그일 때는 충돌이 더 큽니다.
  • exe는 항상 이 enterprise/samsungds 브랜치(또는 사내 릴리스 태그 v1.4.x-samsungds)에서 빌드합니다.
  • 병합 직후에는 문서의 file:line 인용이 전부 밀립니다. 이 README와 docs/reference/*.md의 인용을 재검증하는 것을 체크리스트에 넣으세요.
  • 병합 후 pnpm install을 반드시 다시 돌리세요. v1.4.162는 신규 런타임 의존성(emojibase-data)을 추가했고, 없으면 pnpm typecheck가 먼저 깨집니다.
  • upstream이 잠금 대상 파일을 리팩터링하면 게이트가 조용히 사라집니다. v1.4.163이 실제로 두 번 그랬습니다: runtime-environments.ts를 쪼개면서 assertRemoteOrcaServerAllowed()가 붙어 있던 핸들러를 옮겼고(+ verifyAndAddFromPairingCode라는 두 번째 페어링 레인 신설), 계정 등록에는 ipcMain을 지나지 않는 accounts.addClaudeFromConfigDir RPC를 열었습니다. 병합 후 git grep -n 'getEnterprisePolicy()' src/와 src/main/enterprise/*-guard.ts의 호출부를 이전 브랜치와 대조하고, upstream 신규 코드에 net.fetch/새 IPC 채널/새 RPC 메서드가 있는지 훑으세요.

충돌 예상 지점 — 솔직한 현황

사내 변경은 "신규 파일 몇 개"가 아닙니다. v1.4.163 기준으로 신규 파일 112개, 기존 upstream 파일 수정 157개, upstream 파일 삭제 5개(잠금으로 없앤 설정 팬)입니다. 최신 수치는 직접 확인하세요:

git diff --name-status v1.4.163..HEAD   # A=신규, M=upstream 파일 수정, D=삭제
성격 파일 리베이스 충돌
신규(포크 전용) src/shared/enterprise-policy.ts(+.test.ts), src/main/enterprise/** 24개(정책 파일 탐색·트레이스·네트워크 가드·직접 다운로드 가드·secure DNS·에뮬레이터/원격 서버/에이전트 허용목록 가드·픽스처·테스트), src/main/rate-limits/usage-polling-disabled-providers.ts(+.test.ts), src/main/observability/observability-consent.test.ts, src/main/claude-accounts/environment.test.ts, src/main/emulator/android/scrcpy-server-download.test.ts, config/vitest-enterprise-policy-isolation.ts, docs/reference/*.md 5개, .claude/harness/*.md 3개(규칙 원장 — 7절) 거의 없음
포크가 삭제한 표면 upstream 대비 123개 파일을 지웠습니다. 도메인별로 피드백 제출 12개(ipc/feedback*, sidebar/SidebarFeedback*, lib/feedback-image-attachments*, crash-reporting/crash-feedback-diagnostic-bundle.ts), 크래시 리포트 7개, local-builds 7개, orca-profiles 4개, artifacts 3개 등. 게이트를 다는 대신 표면 자체를 없앤 경우입니다 가장 위험한 범주입니다. upstream이 지운 파일을 수정하면 modify/delete 충돌이 나고, incoming을 수용하면 표면이 통째로 되살아납니다 — 파일이 통째로 돌아오므로 게이트 grep에도 타입체크에도 잡히지 않습니다. 동기화마다 comm -23 <(git ls-tree -r --name-only <옛태그>) <(git ls-tree -r --name-only HEAD) 로 삭제 목록을 뽑아 upstream 변경분과 교집합을 확인하세요
upstream 파일에 삽입한 게이트 메인: telemetry/consent.ts, observability/index.ts, github/client.ts, git/hosted-remote-url.ts, orca-profiles/profile-cloud-auth-config.ts, rate-limits/service.ts, rate-limits/claude-pty.ts, claude-accounts/{environment,oauth-refresh,runtime-auth-service}.ts, ipc/pty.ts, window/{createMainWindow,dashboard-popout-window}.ts, browser/{browser-manager,offscreen-browser-backend}.ts, lib/html-to-pdf.ts, emulator/android/scrcpy-server-download.ts, index.ts, src/shared/network-proxy.ts. 렌더러: src/renderer/index.html(CSP 주석), components/settings/PrivacyDiagnosticsSection.tsx. 빌드·테스트: config/electron-builder.config.cjs, config/vitest.config.ts, tests/e2e/helpers/electron-home-isolation.ts (+ 대응 테스트 파일들, i18n 카탈로그 5개, .gitignore 3줄). 문서: skill-guides/orchestration.md의 ## Project Rule Ledger 절 + ## Next Action의 원장 로드 한 구절 upstream이 같은 함수를 건드리면 발생. 게이트를 각 도메인의 단일 초크포인트에 넣어 둔 이유가 이것입니다. 오케스트레이션 가이드 절은 config/scripts/orchestration-skill-rule-ledger.test.mjs가 지키므로 유실 시 테스트가 먼저 붉어집니다
포크가 재작성해 소유한 문서 README.md(upstream 원문 268줄을 사내 문서로 전면 교체 — 남은 공통 문장이 거의 없어 자동 병합이 되지 않습니다), CLAUDE.md(upstream은 @AGENTS.md 한 줄짜리 11바이트 스텁 → 126줄로 확장) 둘 다 upstream에도 존재하므로 upstream이 손댈 때마다 반드시 충돌합니다. 리베이스에서 사내 버전을 남기려면 git checkout --theirs README.md CLAUDE.md — 리베이스에서는 --ours가 재배치 대상(upstream), --theirs가 재생 중인 사내 커밋이라 머지와 의미가 뒤집혀 있습니다. 그다음 upstream 변경분 중 필요한 것만 수동으로 반영하세요

자식 프로세스는 창구를 지난다 — runProcess/spawnProcess (v1.4.197부터 숫자로 강제)

upstream v1.4.197이 "node:child_process를 직접 import 하는 파일 수"에 숫자 백스톱을 붙였습니다 (src/shared/child-process/child-process-import-boundary.test.ts의 DIRECT_IMPORTER_PIN, windows-console-visibility.test.ts의 UNHIDDEN_SPAWNER_PIN). 허용목록 파일은 그 전부터 있었고, 이 포크는 자기 소유 8건을 거기 올려 두고 있었습니다.

v1.4.197 머지에서 8건을 전부 이관했습니다.

무엇 오르카에서 언제 도나 어디로
gateway/gateway-cli-availability.ts 설정 → Gateway 를 열 때 gateway-cli --version runProcess
gateway/gateway-verify.ts 같은 화면에서 로그인 상태 확인 gateway-cli verify runProcess
relay/fs-handler-git-search-submodules.ts SSH 원격에 ripgrep 이 없어 git grep 으로 대체 검색할 때 runProcess + spawnProcess
git/submodule-write-test-repo.ts 서브모듈 테스트 픽스처 runProcessSync
타입 전용 4건 스폰하지 않음 — 타입만 빌려 씀 ChildProcessHandle, ExecFileSync

왜 중요한가. 이관 전에도 이 파일들은 windowsHide: true 를 직접 달고 있어서 "검은 창 번쩍"은 막혀 있었습니다. 창구가 추가로 보장하는 것은 .cmd/.bat argv 인코딩(게이트웨이 CLI 가 .cmd 심으로 깔리는 경우 — 예전에는 cmd.exe /d /c 로 감싸 명령줄이 재파싱됐습니다), 프로세스 트리 종료, 출력 상한, 그리고 릴레이 쪽에는 없던 30초 데드라인입니다.

다음 동기화에서 할 일.

v1.4.218 에서 upstream 이 DIRECT_IMPORTER_PIN(직접 import 수 백스톱)을 테스트째 폐기했고(#23815), 포크도 따랐습니다 — 허용목록 검사는 남아 있어 새 직접 import 는 여전히 막힙니다. 남은 핀은 UNHIDDEN_SPAWNER_PIN 하나였습니다(포크 56, upstream 58, v1.4.221 기준).

v1.4.223 에서 upstream 이 남은 핀과 허용목록까지 전부 지웠습니다(#25791 "low-value test inventories" — windows-console-visibility.test.ts, 두 __fixtures__/*-allowlist.txt, gh-spawn-boundary.test.ts). 포크는 삭제를 따랐고 원장의 두 앵커(UNHIDDEN_SPAWNER_PIN = 56, GHES 로그인의 INTERACTIVE_LOGIN_EXEMPTIONS)도 함께 뺐습니다 — 지켜야 할 게이트가 사라졌으므로 예외도 필요 없습니다. 창구 사용 규칙 자체(AGENTS.md)는 그대로이고, 이제 숫자로 강제하는 장치가 없으니 리뷰가 지킵니다.

디렉터리 통째로 지운 것 — cloud/ (v1.4.197에서 제거)

upstream v1.4.197이 모바일 앱↔데스크톱 페어링 릴레이 서비스의 GCP 인프라 전체를 모노레포에 들여왔습니다 — cloud/ 331파일 4MB(자체 pnpm 워크스페이스 apps/relay·apps/relay-fence-broker· apps/relay-ops·packages/relay-contract, Terraform 루트), .github/workflows/cloud-*.yml 25개, .github/actions/cloud-sql-rollout-lease/. 이 포크는 전부 지웠습니다.

왜 지웠나. 제품 위험은 0이었습니다 — 데스크톱 빌드가 cloud/를 참조하지 않고, 클라이언트 쪽 레인은 이미 disableMobilePairing·disableCloudRelay가 막습니다. 문제는 저장소·CI 표면입니다: 25개 중 2개가 스케줄 트리거(cloud-monitor-relay-clock-skew.yml 매시, cloud-power-relay-staging.yml)라 사내 origin에서 Actions가 켜져 있으면 벤더 GCP 프로젝트로 매시 인증을 시도하고, cloud-verify.yml은 PR 트리거이며, 사내 보안 리뷰어가 우리 저장소에서 벤더 배포 파이프라인을 읽게 됩니다.

같이 지운 것 — 이걸 빠뜨리면 CI가 깨집니다.

무엇 어디 왜
/cloud/·cloud-*.yml·cloud-sql-rollout-lease/ 3행 .github/CODEOWNERS 존재하지 않는 경로를 가리키게 됩니다
passes the staging confirmation through the step environment 케이스 config/scripts/release-blocker-fixes.test.mjs cloud-prove-relay-asia-staging.yml을 직접 읽으므로 ENOENT로 죽습니다
tests/e2e/relay-region-{compatibility,correction}.unit.test.ts, src/main/runtime/push/push-host-proof-vector.test.ts (v1.4.201) 테스트 ../../cloud/… 를 import 하므로 수집 단계에서 죽습니다
desktop-relay.region-correction-idle-cutover 게이트 항목 (v1.4.201) config/reliability-gates.jsonc 위 테스트만 가리키므로 check:reliability-gates 가 없는 파일로 빨개집니다
Install relay integration dependencies 스텝 (v1.4.201) .github/workflows/unit-tests.yml working-directory: cloud 에서 @orca-cloud/relay 를 설치합니다
relay.control-activation-ownership 게이트 항목 (v1.4.215) config/reliability-gates.jsonc cloud/apps/relay 테스트 4개만 가리킵니다. 원장 absentSymbols 의 cloud/apps/ 가 다음 항목을 잡습니다
Cancel obsolete PR checks 목록의 cloud-verify.yml 행 (v1.4.229) .github/workflows/ci-closed-pr-caches.yml ci-pr-concurrency-keys.test.mjs 가 없는 워크플로의 동시성 키로 빨개집니다

다음 동기화에서 할 일 — 매번 반복됩니다.

  1. cloud/**는 대부분 신규 파일이라 충돌 없이 조용히 들어옵니다. 게이트가 먼저 말해 줍니다 — pnpm check:fork-feature-ledger의 absentPaths에 cloud·.github/workflows/cloud-*.yml· .github/actions/cloud-sql-rollout-lease 3건이 등재돼 있고, 워크플로는 계열 글롭으로 검사하므로 upstream이 cloud-26.yml을 새로 추가해도 잡힙니다.
  2. 빨개지면 git rm -rqf cloud .github/workflows/cloud-*.yml .github/actions/cloud-sql-rollout-lease.
  3. .github/CODEOWNERS와 release-blocker-fixes.test.mjs는 자동 병합됩니다 — 충돌 표시 없이 되살아나므로 직접 확인하세요. (release-blocker-fixes.test.mjs 는 v1.4.223 에서 upstream 이 지웠습니다.)
  4. cloud/ 를 import 하는 코드는 src/·tests/ 에도 들어옵니다(v1.4.201 에서 3건). 판별식은 git grep -n -E "cloud/(apps|packages)|@orca-cloud/" -- src tests config .github — 주석 외에는 비어야 합니다. upstream 이 벤더 클라우드에 기능을 붙일 때마다 데스크톱 쪽 클라이언트도 같이 오므로, 그 클라이언트가 ORCA_CLOUD_REMOVED 뒤에 있는지도 봐야 합니다 — v1.4.201 의 푸시 게이트웨이는 일부러 그 밖에 있었습니다 (감사 문서 §3.1).

디렉터리 통째로 지운 것 — docs/site (v1.4.195에서 제거)

upstream v1.4.195가 Next.js 문서 사이트(docs/site/, 115파일 + 자체 package.json·pnpm-lock.yaml)와 그것을 Vercel www.onorca.dev/docs로 배포하는 워크플로(.github/workflows/docs.yml)를 들여왔습니다. 이 포크는 둘 다 지웠습니다.

왜 지웠나. 데스크톱 번들에는 안 실리고(config/electron-builder.config.cjs의 '!docs{,/**/*}') 배포 잡도 release_gate의 github.repository == 'stablyai/orca'로 막혀 우리 저장소에서는 도달 불가지만, 저장소 안에 벤더 인프라를 향하는 배포 파이프라인과 별도 의존성 트리(next/fumadocs/vercel CLI) 가 남아 있을 이유가 없습니다. 사내 감사에서 설명할 표면을 하나 줄이는 쪽을 택했습니다.

같이 지운 것 — 이걸 빠뜨리면 CI가 깨집니다.

무엇 어디 왜
docs-production-dispatch 잡 .github/workflows/release-cut.yml gh workflow run docs.yml을 호출하므로 워크플로가 없으면 릴리스 컷에서 실패합니다
docs.yml#{check,production,release_gate} 3행 + release-cut.yml#docs-production-dispatch 1행 config/scripts/release-cut-token-permissions.test.mjs의 EXPECTED_MATRIX 이 게이트는 워크플로 잡 목록을 리터럴로 못 박습니다. 남겨 두면 "존재하지 않는 잡"으로 빨개집니다
.gitignore의 !docs/site/ · !docs/site/** 2행 .gitignore upstream이 추가한 허용목록 예외. 지워야 docs/** 무시가 다시 이 트리를 덮습니다
번역 README 6개의 ../site/public/docs/* 미디어 4종 (v1.4.203) docs/readme/README.*.md check:readme-local-links가 추적 안 된 경로로 빨개집니다. 같은 파일의 사본인 resources/onboarding/feature-wall/tile-{01,05,08}(*.recorded-at.json의 sourceGif가 원본을 적어 둡니다)로 바꿨습니다
Cancel obsolete PR checks 목록의 docs.yml 행 (v1.4.229) .github/workflows/ci-closed-pr-caches.yml ci-pr-concurrency-keys.test.mjs 가 없는 워크플로의 동시성 키로 빨개집니다. 목록이 자동 병합되므로 다음 동기화에서도 이 행과 cloud-verify.yml 행이 되살아났는지 보세요

다음 동기화에서 할 일 — 매번 반복됩니다.

  1. docs/site/**는 DU(deleted by us) 충돌로 뜹니다. 전부 git rm -r docs/site로 삭제 유지하세요. upstream이 문서를 계속 고치므로 파일 수는 릴리스마다 늘어납니다.
  2. .github/workflows/docs.yml도 같은 DU 충돌입니다. git rm 하세요.
  3. release-cut.yml은 자동 병합됩니다 — upstream이 docs-production-dispatch 잡을 되살려 놓아도 충돌 표시가 안 납니다. 병합 후 grep -n "docs.yml" .github/workflows/release-cut.yml로 직접 확인하고 되살아났으면 그 잡 블록을 다시 지우세요.
  4. release-cut-token-permissions.test.mjs의 EXPECTED_MATRIX도 같은 이유로 조용히 되살아납니다. 판정기는 그 테스트 자신입니다 — pnpm test config/scripts/release-cut-token-permissions.test.mjs가 초록이면 4행이 정확히 빠져 있는 것입니다.
  5. .gitignore의 3행(주석 포함)은 upstream 원문과 충돌합니다. 포크 주석을 남기세요.
  6. upstream이 README 미디어를 docs/site/public/로 다시 옮기면 번역 README가 자동 병합되고 pnpm lint의 check:readme-local-links가 경로를 그대로 알려 줍니다. 같은 GIF의 사본을 resources/onboarding/feature-wall/*.recorded-at.json의 sourceGif로 찾아 바꾸세요. 루트 README.md는 포크 소유라 해당 없습니다.

되살아나도 제품은 안전합니다 — 번들 제외와 저장소 게이트는 upstream 쪽 장치라 그대로 유효합니다. 이 절차는 "감사 표면을 늘리지 않는다"는 유지 작업이지 보안 게이트가 아닙니다. 그러니 급하게 되돌리지 말고 다음 정기 동기화에서 처리해도 됩니다.

upstream이 이미 게이트된 레인에 두 번째 provider를 붙일 때 (v1.4.198에서 실제로 겪음)

v1.4.198이 structured 네이티브 챗 레인에 Claude 를 추가했습니다(@anthropic-ai/claude-agent-sdk, src/main/claude/**). 이 레인은 이미 allowedAgents로 잠겨 있었지만 잠금은 codex 모양으로 잠겨 있었습니다 — 게이트 4곳 중 셋(attach 퍼널·가용성 프로브·렌더러 좁히기)은 provider-agnostic 이라 새 provider 를 공짜로 덮었고, 마지막 스폰 게이트만 codex-structured-launch-resolution.ts 안에 있어서 claude-structured-launch-resolution.ts 에는 없었습니다.

왜 아무것도 이것을 잡지 못했나.

  • 원장의 앵커 검사는 문자열이 사라졌는지만 봅니다. codex 쪽 줄은 그대로 있었습니다.
  • policySwitchMinConsumers 하한도 못 잡습니다. 그 카운터는 .allowedAgents 출현 수를 세는데, 게이트 함수(assertAgentAllowedByEnterprisePolicy)는 가드 모듈 안에서 정책을 읽으므로 새 레인이 하나 안 잠겨도 숫자가 줄지 않습니다.
  • 행동 테스트도 못 잡습니다. codex 케이스는 계속 초록이고, claude 케이스는 존재한 적이 없습니다.

그래서 판별식은 "쌍둥이 대조"입니다. 머지 후, 게이트가 박힌 파일마다 그 파일의 형제가 새로 생기지 않았는지 보십시오. 기계적으로:

# 게이트가 있는 파일들의 이름 패턴을 뽑아, 같은 패턴의 형제 중 게이트 없는 것을 찾는다
git grep -l 'assertAgentAllowedByEnterprisePolicy' -- src/ | sed 's|.*/||'
# 예: codex-structured-launch-resolution.ts 가 나오면
ls src/main/*/[a-z]*-structured-launch-resolution.ts
git grep -L 'EnterprisePolicy' -- 'src/main/*/[a-z]*-structured-launch-resolution.ts'

빈 결과여야 합니다. 뭔가 나오면 그 파일이 새 provider 이고 게이트가 안 따라간 것입니다. 고칠 때는 쌍둥이 양쪽을 다 원장에 등재하십시오 — 하나만 올리면 다음 릴리스에 같은 일이 반복됩니다.

게이트가 박힌 파일을 upstream이 통째로 해체할 때 (v1.4.196에서 실제로 겪음)

v1.4.196은 max-lines 우회 34건을 한 릴리스에서 청산했습니다(config/max-lines-baseline.txt에서 34행 제거, 추가 0행). 그 목록이 이 포크의 게이트가 박혀 있던 파일과 거의 겹칩니다:

해체된 파일 변화 upstream이 옮긴 곳
src/main/index.ts 3,805 → 65줄 src/main/startup/** (33개 신규)
src/main/runtime/orca-runtime.ts −44,399줄 src/main/runtime/** (490개 신규)
src/preload/index.ts −5,302줄 src/preload/api/*-bridge.ts (90개 신규)
src/main/rate-limits/service.ts −2,180줄 src/main/rate-limits/service/**
src/main/browser/browser-manager.ts −2,590줄 browser-manager-{bindings,guest-popup-policy,types,...}.ts
src/renderer/src/store/slices/{ui,tabs}.ts −5,025줄 slices/{ui,tabs}/**
components/{StatusBar,Terminal,TerminalPane,AutomationsPage}.tsx −11,875줄 각 디렉터리의 hook/surface 모듈

이 상황의 함정은 셋입니다.

  1. merge base가 여러 개면 유령 충돌이 난다. 릴리스 태그는 서로 조상이 아니라 동기화마다 base가 하나씩 늘어납니다(v1.4.196 시점 16개). git이 가상 조상을 만들면서 upstream이 손대지도 않은 파일에 충돌이 납니다 — 이번엔 9건. 판별식은 git diff <옛태그> main -- <파일>이 비어 있는지이고, 해법은 실제 태그를 base로 준 재머지입니다: git merge-file -L ours -L base -L theirs <ours> <base@옛태그> <theirs@main>.

  2. 게이트는 조용히 사라진다. 코드가 통째로 다른 파일로 가면 게이트만 남겨지지 않고 같이 없어지는데, 타입체크도 테스트도 그것을 잡지 않습니다(그 파일의 테스트도 함께 옮겨 갔기 때문). 머지를 시작하기 전에 기준선을 떠 두십시오:

    git grep -n -E "getEnterprisePolicy\(\)|getEnterprisePolicyView\(\)" <포크tip> -- src/ \
      | grep -v '\.test\.' | grep -v 'src/main/enterprise/' > /tmp/gates-before.txt   # v1.4.196 시점 62건

    포크 전용 모듈(포크tip에는 있고 upstream 태그에는 없는 src/**/*.ts)의 호출지점 수도 같이 떠 두면 guard 모듈이 통째로 고아가 된 경우까지 잡힙니다(v1.4.196 시점 706건 → 머지 후 715건).

  3. 되살아난 표면은 충돌로 뜨지 않는다. 포크가 지운 기능을 upstream이 새 경로에 새 파일로 다시 만들면 git은 그냥 추가합니다. v1.4.196에서 31건이 그렇게 들어왔습니다 — src/main/updater.ts가 src/main/updater/** 16개로, preload 인라인 API가 api/{updater,feedback,bitbucket}-bridge.ts로, docs/site 자산 8건. 판별법은 포크 삭제 목록의 도메인 키워드로 신규 파일 목록을 훑는 것입니다:

    comm -23 <(git ls-tree -r --name-only <옛태그> | sort) <(git ls-tree -r --name-only <포크tip> | sort)  # 삭제 목록
    git diff --name-status <옛태그> <새태그> | awk '$1=="A"{print $2}'                                     # 신규 목록

GitHub 에서 직접 받는 개발 의존성 — 사내 미러를 우회한다 (v1.4.205에서 제외)

v1.4.205가 lint 전용 플러그인 oxlint-plugin-anti-slop 을 npm 레지스트리가 아니라 git 의존성 (github:dmmulroy/anti-slop#<커밋>, lockfile 에서는 codeload.github.com tarball)으로 들여왔습니다. 사내 빌드의 pnpm install 은 사내 npm 미러만 거치므로(docs/reference/windows-corporate-build.md §6) 이 tarball 에서 설치가 실패할 수 있고, 앱에 실리지도 않는 lint 도구입니다. 포크에서 뺐습니다.

무엇 어디
oxlint-plugin-anti-slop devDependency package.json
audit:anti-slop · sync:anti-slop-plugin 스크립트, lint 체인 package.json
플러그인 설정·동기화 스크립트 config/oxlint-anti-slop.json, config/scripts/sync-anti-slop-plugin.mjs
Reject low-evidence patterns 스텝 .github/workflows/pr.yml

다음 동기화에서 할 일. package.json 은 늘 포크판 + upstream 델타로 풀므로 의존성은 돌아오지 않지만, pr.yml 스텝과 두 설정 파일은 충돌 없이 되살아납니다. 원장이 잡습니다 — absentSymbols 에 package.json·pr.yml 의 이름과 lockfile 의 codeload.github.com 이, absentPaths 에 두 파일이 등재돼 있습니다. upstream 코드 곳곳의 oxlint-disable anti-slop/... 주석은 주석일 뿐이라 그대로 둡니다.

같은 부류의 판별식: 동기화 뒤 grep -c codeload.github.com pnpm-lock.yaml 이 0 이어야 합니다. git 의존성이 새로 들어오면 lockfile 에 이 호스트가 나타납니다.

함께 겪은 lockfile 함정. 태그판 lockfile 에서 --lockfile-only 로 재계산하자, 포크가 직접 선언한 @tiptap/core·@tiptap/extension-code 의 importer 버전이 peer 접미사 없이(3.31.3) 기록돼 snapshots 에 없는 변형을 가리켰습니다. --frozen-lockfile 은 통과하는데 node_modules/@tiptap/core 가 끊어진 링크라 tc:web 이 수백 건 실패합니다. importer version 을 peer 포함 스냅샷(3.31.3(@tiptap/pm@3.31.3))으로 맞추면 재계산 후에도 유지됩니다. 판별식은 ls node_modules/@tiptap/core/package.json 입니다.

테스트·카탈로그 검증을 Bun 으로 옮긴 upstream — 포크는 Node 유지 (v1.4.223)

v1.4.223 이 pnpm test 를 node config/scripts/run-vitest.mjs(= bun --bun vitest)로, pnpm lint 안의 verify:localization-catalogs 를 bun 실행으로 바꿨습니다(config/.bun-version 1.4.2). 사내 개발 PC·빌드 PC 에는 Bun 이 없어 둘 다 시작조차 못 하므로 포크는 Node 실행을 유지합니다 — test 는 upstream 이 남겨 둔 test:node 와 같은 명령이고, 카탈로그 검증 스크립트는 두 런타임에서 같은 판정을 내도록 upstream 이 테스트합니다. config/vitest.config.ts 의 projects 는 Bun 이 없으면 node 프로젝트 하나로 돌므로 그대로 받았습니다.

다음 동기화에서 할 일. package.json 은 늘 포크판 + upstream 델타로 풀므로 보통은 유지되지만, upstream 판을 받으면 두 줄이 돌아옵니다 — 원장 absentSymbols(node config/scripts/run-vitest.mjs, "bun config/scripts/verify-localization-catalogs.mjs")가 잡습니다. verify-localization-catalogs.test.mjs 는 bun 을 직접 띄워 두 런타임을 비교하므로 Bun 이 없는 PC 에서는 순정 태그에서도 빨갛습니다(환경성). Bun 을 사내 미러로 들이기로 하면 이 절을 지우고 두 줄을 upstream 판으로 되돌리면 됩니다.

v1.4.230 — upstream 이 bun 을 devDependency 로 들였습니다. 포크는 싣지 않습니다. "bun": "1.4.3" 과 pnpm-workspace.yaml 의 allowBuilds: bun: true 가 함께 왔고, typecheck 계열 스크립트의 기본 컴파일러가 bun 으로 바뀌었습니다(run-typecheck-projects-in-parallel.mjs 의 compiler: 'bun'). bun npm 패키지의 postinstall(install.js)은 플랫폼 패키지(@oven/bun-*)를 못 찾으면 registry.npmjs.org 에서 직접 받고, 그것도 실패하면 bun.sh 설치 스크립트를 안내합니다 — 사내 미러를 우회하는 경로입니다. 그래서:

무엇 어디
bun devDependency, allowBuilds: bun: true package.json, pnpm-workspace.yaml — 빼고 lockfile 재계산
로컬 typecheck 4개 스크립트에 --compiler=native package.json 의 typecheck·typecheck:node·typecheck:cli·typecheck:web — 없으면 build:desktop·build:win·build:unpack 이 "Bun … is missing" 으로 죽습니다(build:release 는 typecheck 를 부르지 않아 영향 없음)
업스트림 계약 케이스 1개를 포크 계약으로 run-typecheck-projects-in-parallel.test.mjs 의 "pins the Bun devDependency" → "ships no Bun devDependency and pins local typecheck scripts to tsc"

typecheck:ci(--compiler=bun)는 꺼진 upstream CI 워크플로만 부르므로 그대로 둡니다. 다음 동기화에서: package.json 은 늘 포크판 + upstream 델타로 풀므로 보통 돌아오지 않지만, 원장 absentSymbols (package.json 의 "bun": ", pnpm-workspace.yaml 의 bun: true)와 present(--compiler=native")가 지킵니다.

경로 리터럴 래칫 — 포크 원장은 스캔에서 뺐다 (v1.4.230)

upstream v1.4.230 이 check:path-literal-ratchet 를 들였습니다. config/·.github/·tests/ 안의 src/... 경로 리터럴이 추적 파일을 가리키지 않으면 실패하고, 예외는 config/path-literal-baseline.txt 에 적되 줄어들기만 합니다. 실패 모드 D 그대로 포크 파일에서 먼저 터졌습니다 — 처리한 방식:

걸린 것 처리
config/fork-feature-ledger.json 54건 — absentPaths 는 일부러 없는 경로를 적는 목록 래칫의 EXCLUDED_FILES 에 원장 한 줄(check-path-literal-ratchet.mjs). present 쪽 존재 검사는 원장 테스트가 따로 합니다. 이 줄이 빠지면 래칫이 소리 내어 빨개집니다
.github/workflows/macos-updater-tests.yml 9건 — 포크가 지운 업데이터 테스트만 도는 워크플로 워크플로 삭제 + ci-closed-pr-caches.yml 의 동시성 키 행 삭제 + 원장 absentPaths
config/scripts/vitest-node-runtime-files.mjs 1건 — 지운 main-window-updater-deferred-setup.test.ts 목록에서 줄 삭제
ssh-{hostile,windows}-hosts.yml 6건 순정 v1.4.230 에서도 실패(릴리스 브랜치가 main 의 워크플로를 앞서 동기화 — 실패 모드 W). baseline 에 근거 주석과 함께 등재, upstream 트리가 따라오면 --prune

같은 정리에서 ci-closed-pr-cancellation.test.mjs 의 기대값도 고쳤습니다 — v1.4.229 에서 docs.yml·cloud-verify.yml 행을 지우면서 이 테스트의 docs-pr-123·cloud-verify-… 기대를 남겨 둬 포크 tip 부터 빨갛던 것입니다.

Chat UI 가 곧 구조화 채팅이 됐다 — 포크 기본값을 upstream 과 같은 꺼짐으로 (v1.4.224)

upstream #22933 이 openAgentTabsInChatByDefault(새 탭을 채팅으로 열기)와 experimentalStructuredNativeChat 을 폐기했습니다. 이제 Chat UI(experimentalNativeChat)를 켜면 Claude/Codex 새 탭이 곧바로 구조화 채팅(PTY 없음)이고, 끄면 채팅/터미널 토글도 없습니다. 포크는 봇 레인 시절부터 이 값을 켜짐으로 출하해 왔지만(봇 레인은 이미 enterprise/bots 로 빠졌습니다) 그 전제가 사라져 upstream 과 같은 꺼짐으로 바꿨고, 저장된 프로필은 upstream 마이그레이션(savedChatUiWithTerminalDefaultView)이 같은 값으로 내립니다. 그 설정을 덮어쓰던 포크의 탭 바 + 메뉴 강제 터미널(forceTerminalView, launch-agent-forced-terminal-view.ts)도 함께 폐기했습니다.

다음 동기화에서 할 일. default-global-settings.ts 의 experimentalNativeChat 은 이제 upstream 과 같은 줄이라 충돌하지 않습니다. 다시 켜짐으로 출하하려면 Windows 에서 구조화 채팅이 실패할 때 터미널로 떨어지지 않는다는 점 (upstream 이 폴백을 약속하지 않음)부터 확인하세요.

같은 판에서 upstream 이 docs/reference/ 를 통째로 지웠습니다(64개). 포크 소유 문서 8개만 남기고 .gitignore 허용목록·CLAUDE.md·이 README 의 링크를 정리했습니다. 지워진 upstream 문서는 git show v1.4.223:docs/reference/<이름>.md 로 읽을 수 있고, AGENTS.md 도 그 링크를 뺀 판으로 들어왔습니다.

SSH 호스트를 빌드에서 뺐다 — upstream 이 relay 를 지웠다 (v1.4.226)

upstream 이 SSH relay 서버·클라이언트를 지웠습니다(#27214·#27215, src/relay/ 는 WSL 게스트 전용 src/wsl-guest/ 로 이동). 이제 SSH 호스트는 원격에 관리형 Orca 서버(orcad)를 설치해야만 돕니다. 이 빌드는 그 템플릿을 싣지 않고(build:orcad-template 없음), 첫 배포는 원격용 Node 를 nodejs.org 에서 받으며, 원격 서버는 이 데스크톱의 정책 파일을 읽지 않습니다 — 정책을 지키면서 SSH 가 동작하는 구성이 없어 2026-10-15 에 SSH 호스트 기능 전체를 막기로 했습니다.

  • 구현은 정책 스위치가 아니라 컴파일 상수입니다 — src/shared/ssh-remote-hosts-removal.ts 의 SSH_REMOTE_HOSTS_REMOVED(타입을 boolean 으로 적어 둔 덕에 가려진 upstream 본문도 계속 컴파일됩니다). main 의 초크포인트가 거부합니다: 모든 연결이 지나는 connectTarget(ssh-connect-flow.ts)과 추가·편집·~/.ssh/config 가져오기(ssh-target-crud-handlers.ts). 렌더러는 설정의 SSH 섹션과 "SSH 호스트 추가" 진입점(AddRepoHostSelectorSlot.tsx, NewWorkspaceComposerProjectSection.tsx)을 감춥니다. 목록과 삭제는 남겼습니다 — 이전 빌드에서 저장한 호스트의 프로젝트를 지울 수 있게, 연결을 누르면 "이 빌드는 SSH 호스트를 지원하지 않는다"는 메시지가 뜹니다.
  • WSL 배포판 호스트(배포판 안에 orcad 를 띄워 붙는 레인)는 disableRemoteOrcaServer 에 묶었습니다 (wsl-host-gate.ts). deployOrcad 진입에도 같은 거부를 넣어, SSH 를 되살리는 빌드에서도 정책 아래 원격 배포는 일어나지 않습니다.

다음 동기화에서 손으로 볼 것. 원장 기능 ssh-remote-hosts-removed 가 알려진 진입점을 잡지만, upstream 이 SSH 호스트를 추가하는 새 진입점(새 onAddSshHost 호출지점, 새 ssh:* 쓰기 IPC/RPC)을 만들면 충돌 없이 들어옵니다. git diff <옛태그> <새태그> -- src | grep -n "onAddSshHost\|'ssh:" 로 새로 생긴 줄을 확인하고 같은 상수로 막으십시오.

되살리려면 상수를 false 로 바꾸는 것만으로는 부족합니다 — orcad 템플릿을 싣고, orcad 엔트리가 설치본 번들 정책을 읽고 허용목록 fetch 가드를 갖게 하고(감사 #41), disableRemoteOrcaServer 의 범위 (관리형 SSH orcad 를 면제할지)를 다시 정해야 합니다(감사 #39).

벤더가 원격으로 바꾸는 설정 — 에이전트 상태 규칙 다운로드 (v1.4.221에서 제거)

v1.4.221 이 src/main/runtime/agent-state-rules/agent-state-rules-live-update.ts 를 들여왔습니다. github.com/stablyai/orca/releases 에서 규칙 묶음을 시작할 때와 4시간마다 받아, 터미널 화면을 읽어 에이전트 상태를 판정하는 규칙을 덮어씁니다. 데스크톱·orca serve·원격 orcad 가 각자 받고, 원격 호스트에는 데스크톱 정책 파일이 닿지 않으므로 스위치 대신 다운로더를 지웠습니다.

무엇 어디
다운로더와 그 테스트 agent-state-rules-live-update{,.test}.ts — 삭제
시작 지점 2곳을 로컬 override 전용 모듈로 (별칭 임포트) main-process-runtime-service.ts, orcad/orcad-entry.ts
그 모듈 (번들 규칙 + 사용자가 고른 agentStateRulesPath 만) agent-state-rules-local-override.ts — 핫 리로드 테스트 2건도 이리로 옮김
다운로드 URL 을 단언하던 케이스, fetch 호출 수 감사 항목 agent-state-rules-bundle.test.mjs, proxy-guarded-fetch-call-site-audit.test.ts

다음 동기화에서 할 일. upstream 이 다운로더를 고치면 DU 충돌로 뜹니다 — 삭제 유지하세요. 이 모듈이 import 하던 src/shared/release-channel.ts 는 업데이터와 함께 이미 지운 파일이라, tc 가 "모듈 없음"으로 빨개져도 되살리면 안 됩니다(되살리는 순간 레인이 산다). 시작 지점이 upstream 임포트로 되돌아가면 원장 absentSymbols 가, 파일이 새 이름으로 돌아오면 absentPaths 가 잡습니다. 업데이터 계열 신규 파일은 하이픈 이름(updater-*.test.ts, main-window-updater*, macos-update-running-instances*)으로도 충돌 없이 들어오므로 원장이 계열 글롭으로 지킵니다.

i18n 카탈로그는 포크 판을 기준으로 두고 upstream 델타만 얹어라 (v1.4.206에서 실제로 겪음)

src/renderer/src/i18n/locales/en.json 은 3만 줄이고 키 순서가 양쪽에서 재배치돼 머지가 hunk 13개로 터집니다. upstream 판(theirs)을 통째로 받으면 안 됩니다 — 포크가 지운 표면의 키가 전부 돌아와 pnpm sync:localization-catalog 가 extra 1,331건으로 죽습니다. 반대로 포크 판만 유지하면 upstream 이 바꾼 문구가 낡은 채로 남아 렌더러 테스트가 문구 불일치로 빨개집니다.

순서는 이렇습니다.

  1. 충돌은 일단 아무 쪽으로든 풀고, en.json 과 en-runtime-required.json 을 포크 tip 판으로 되돌린다(git checkout <포크tip> -- <두 파일>).
  2. pnpm sync:localization-catalog 를 돌린다. 코드에 fallback 이 있는 신규 키는 여기서 채워진다. "missing without fallback" 로 남는 키는 upstream 태그의 en.json 에서 값을 옮겨 넣는다.
  3. upstream 이 지운 키를 지운다. v1.4.x en.json − v1.4.y en.json 의 차집합 중 포크 소스에서 참조되지 않는 것(v1.4.206 에서 69건 — 제거된 대시보드 맵 뷰, Starting chat… 라벨 등). 남겨 두면 structured-agent-startup-label-census 같은 "제거된 문구가 없어야 한다" 가드가 잡는다.
  4. upstream 이 값을 바꾼 키를 수용한다. 두 태그에서 값이 달라진 키 중 포크 en.json 의 값이 옛 upstream 값과 같은 것만 갱신한다. 값이 다르면 포크가 손댄 것이니 건드리지 않는다(v1.4.206: 수용 5건, 포크 소유 0건). 이걸 빠뜨리면 use-reveal-requests·browser-search 가 빨개진다.
  5. 복수형 변형을 손으로 옮긴다. _one/_other 같은 변형은 sync 가 코드 fallback 에서 만들어 내지 못한다. upstream 판에 있고 포크에 base 키가 있는 변형만 골라 넣는다(v1.4.206: 8건). 빠지면 i18next 가 엉뚱한 형태를 골라 NativeChatResumeOnRestartModal 이 같은 토스트를 두 번 낸다.
  6. 다른 로케일은 en 기준으로 extras 만 정리하고, 마지막에 pnpm sync:localization-runtime-catalog 로 부트 카탈로그를 재생성한다.

모바일 웹 번들 레인은 사내 빌드에서 뺐다 (v1.4.206 계약 → v1.4.210 제거)

v1.4.206 에서 upstream 이 build:mobile-web 을 build:release·build:desktop 체인에 넣고 electron-builder beforePack 에 assertMobileWebBundleBuilt 를 걸었습니다. 그때는 번들이 src/mobile-web 만으로 만들어져 영향이 없었습니다. v1.4.210 에서 예고했던 전환(OTA Phase C)이 왔습니다 — build:mobile-web 이 mobile/ 의 Expo/React Native 앱 본체를 번들해서, mobile/node_modules 가 없으면 420개 에러로 실패합니다. 사내 npm 미러로는 그 트리를 설치할 수 없고 모바일 페어링은 정책으로 막혀 있으므로 레인을 뺐습니다:

  • package.json — build:desktop·build:release·build:release:parallel 끝의 && pnpm run build:mobile-web 제거(스크립트 정의 자체는 남김 — 꺼진 upstream 워크플로와 테스트가 부른다).
  • config/electron-builder.config.cjs — beforePack 의 번들 가드와 그 import 제거.
  • upstream 핀 테스트 2개를 포크 계약으로 뒤집음 — mobile-web-bundle-packaging-workflow-contract.test.mjs ("어떤 체인도 build:mobile-web 에 닿지 않는다", v1.4.223 에서 upstream 이 파일째 삭제 — 원장이 대신 지킨다), verify-packaged-mobile-web-bundle.test.mjs("beforePack 은 번들 없이 통과한다").
  • v1.4.223 이 새로 넣은 build:release:javascript 체인에서도 && pnpm run build:mobile-web 을 뺐다.
  • 원장 absentSymbols 2건(package.json 의 && pnpm run build:mobile-web, 빌드 설정의 assertMobileWebBundleBuilt)이 다음 머지에서 체인이 조용히 돌아오는 것을 잡습니다.

런타임은 번들이 없으면 "번들 없음"으로 처리합니다(src/main/runtime/bundled-mobile-web-bundle.ts). 다음 동기화에서 볼 것: upstream 이 새 패키징 잡이나 새 빌드 체인 스크립트에 번들을 붙이면 뒤집은 테스트가 빨개집니다 — 그 체인에서도 빼면 됩니다. 모바일 컴패니언을 사내에서 쓰기로 하면 이 절을 통째로 되돌리고 enterprise-build.yml 에 install-mobile-dependencies 를 넣어야 합니다.

v1.4.232 — beforePack 의 두 번째 인자가 다시 쓰입니다. upstream 이 다크 빌드 검사에 mobileWebBundleDir 를 넣었는데(assertNotDarkBuild([out/main, mobileWebBundleDir])), 포크는 그 인자를 _mobileWebBundleDir 로 이름을 바꿔 두었으므로 충돌 없이 병합된 채 패키징에서 ReferenceError 가 납니다. 포크는 그 인자에 out/mobile-web 기본값을 주고 다크 빌드 검사에만 씁니다(없는 디렉터리는 건너뜀). 원장 windows-build 의 앵커가 이 기본값을 지킵니다.

macOS ShipIt 스케줄링 애드온은 싣지 않는다 (v1.4.232에서 제거)

v1.4.232 가 Squirrel.Mac 의 설치 작업 우선순위를 바꾸는 네이티브 애드온을 들였습니다 — native/macos-shipit-scheduling/, 그 빌드 스크립트 config/scripts/macos-shipit-scheduling.cjs(+테스트), electron-builder 의 macOS afterPack 에서 그것을 빌드·서명하는 두 줄, 런타임 쪽 src/main/updater/macos-shipit-scheduling.ts. 인앱 업데이터 전용이고 포크는 업데이터를 지웠으므로 전부 뺐습니다. 런타임 쪽은 src/main/updater/ 아래라 원장 absentPaths 가 이미 잡고, 나머지는 이번에 absentPaths(native/macos-shipit-scheduling, config/scripts/macos-shipit-scheduling*)와 absentSymbols(빌드 설정의 buildMacShipItScheduling)에 올렸습니다. 충돌 없이 들어오는 신규 파일이므로 다음 동기화에서도 원장이 먼저 말해 줍니다.

CLI 빌드는 tsc 로 남긴다 — upstream 은 bun·tsrs 만 쓴다 (v1.4.237)

v1.4.237 이 build:cli 를 node config/scripts/build-cli.mjs 로 바꿨습니다. 그 스크립트는 bun(포크 미탑재, 위 "테스트·카탈로그 검증을 Bun 으로" 절)으로 검사하거나 새 devDependency tsrs 로 내보내는데, tsrs 의 플랫폼 패키지는 darwin-arm64·linux 뿐이라 Windows 바이너리가 없습니다. 받으면 사내 Windows 빌드의 build:release·build:desktop 이 build:cli 에서 죽으므로, 포크는 v1.4.232 의 tsc 명령을 유지합니다(resolveProjectCompiler 가 쓰는 것과 같은 typescript 의 tsc 입니다). 같은 원칙으로 새 typecheck:e2e(run-e2e-typecheck.mjs, 기본 bun)에도 --compiler=native 를 붙였습니다. tsrs 의존성 자체는 npm 미러로 받는 일반 패키지(설치 스크립트 없음)라 그대로 둡니다.

다음 동기화에서 볼 것. build:cli 줄은 포크가 손대지 않은 upstream 줄이었기 때문에 충돌 없이 자동 병합으로 upstream 판이 들어왔습니다. 원장 present(package.json 의 "build:cli": "pnpm run build:packages && tsc -p …)와 run-typecheck-projects-in-parallel.test.mjs 의 포크 계약 케이스가 지킵니다.

Remote Orca Servers 단일 목록 — 원격 서버 업데이트를 다시 덜어 냈다 (v1.4.237)

upstream #31082 가 Remote Orca Servers 를 "섹션 여섯 개 → 목록 하나"로 다시 짰습니다(runtime-server-list.tsx· runtime-server-row*.ts(x)·runtime-servers-header-actions.tsx 신설, runtime-servers-connect-section.tsx 삭제). 새 구조가 행마다 원격 서버 업데이트 상태·Update 버튼·"Check for updates" 메뉴·헤더의 "Update all" 을 다시 렌더링하므로, 포크가 지운 remote-server-update-coordinator/RemoteServerUpdateStatus 참조를 새 파일들에서 걷어 냈습니다. capability REMOTE_SERVER_UPDATE_CAPABILITY 는 protocol-version.ts 에서 host-runtime-capabilities.ts 로 옮겨 와 거기서도 뺐고, 헤더 메뉴로 옮겨 온 "Set up Orca on an SSH host"(관리형 orcad 설치)는 SSH 호스트를 뺀 이 빌드에서 항상 실패하므로 SSH_REMOTE_HOSTS_REMOVED 로 감췄습니다. 원장 absentSymbols 7건과 present 1건이 지킵니다 — 포크에 업데이트 코디네이터 모듈이 없어 되살아나면 tc 도 깨지지만, 원장이 먼저 파일을 짚어 줍니다.

pnpm 전역 가상 스토어 — upstream 이 로컬 설치의 기본값을 바꿨다 (v1.4.232)

v1.4.232 의 config/pnpmfile.cjs 가 CI 밖 모든 설치에서 pnpm 전역 가상 스토어를 켭니다(Windows 는 junction). node_modules/<pkg> 가 사용자 홈의 공유 스토어를 가리키므로 패키지 디렉터리를 그 자리에서 고치면 모든 체크아웃이 공유하는 스토어(하드링크)를 덮어씁니다. upstream 은 네이티브 빌드를 비공개 사본에서 돌리도록 바꿨고(shared-native-build.mjs, build-windows-process-tree-addon.mjs 의 SCRATCH_DIR), 포크의 windows-process-tree 소스 수리(ensure-windows-process-tree-source.mjs)도 그 사본에 걸리게 packageDir·resolveFromDir 를 받도록 했습니다. GitHub Actions(enterprise build)는 CI 라 꺼져 있어 예전과 같고, 사내 Windows 단말에서 로컬로 빌드할 때만 달라집니다 — 문제가 생기면 빌드 셸에서 ORCA_PNPM_GLOBAL_VIRTUAL_STORE=0 으로 끌 수 있습니다(빌드 셸 전용 변수, 사용자 환경에 넣지 않음).

upstream 파일이 max-lines 상한에 닿을 때 — 별칭 임포트로 호출부를 되돌려라 (v1.4.204에서 실제로 겪음)

v1.4.204가 src/main/ipc/settings.ts에 8줄을 더하자, 포크가 넣어 둔 4줄과 합쳐져 oxlint의 300줄 상한을 2줄 넘겨 커밋 자체가 막혔습니다. 포크의 4줄은 새 코드가 아니라 포맷 차이였습니다 — 게이트가 붙은 래퍼 이름(applyAgentStatusHooksEnabledUnderEnterprisePolicy)이 upstream 이름보다 길어서 oxfmt가 인자 목록을 다르게 접었기 때문입니다.

해법은 게이트를 빼는 것도, 모듈로 뽑는 것도 아니라 별칭 임포트였습니다:

// Aliased to upstream's name so the call site below stays byte-identical to upstream — the
// enterprise gate lives in the wrapper, not here.
import { applyAgentStatusHooksEnabledUnderEnterprisePolicy as applyAgentStatusHooksEnabled } from '../agent-hooks/enterprise-agent-hook-policy'

이득이 둘입니다. ① 호출부가 upstream과 바이트 동일해져 4줄이 줄고, ② 그 hunk는 앞으로 충돌하지 않습니다. 대신 앵커가 호출부에서 임포트 줄로 옮겨 가므로 원장의 contains를 임포트 문자열로 적어야 합니다(applyAgentStatusHooksEnabledUnderEnterprisePolicy as applyAgentStatusHooksEnabled).

이 수법은 래퍼의 시그니처가 upstream 함수와 같을 때만 씁니다. 인자가 다르면 별칭이 거짓말이 되고, 그때는 guard 모듈 추출이 맞습니다.

게이트를 다시 심을 때의 요령. 새 위치는 기계적으로 찾을 수 있습니다 — 포크 hunk의 컨텍스트 라인을 upstream 새 트리에서 grep하면 그 코드가 어느 모듈로 갔는지 나옵니다. 클래스 체인으로 쪼개진 런타임 (OrcaRuntimeWithX extends OrcaRuntimeWithY)에서는 포크 메서드를 체인 말단에 두십시오 (src/main/runtime/orca-runtime-fork-surface.ts) — 아래 계층이 다시 쪼개져도 살아남고, 유실되면 호출지점에서 타입체크가 먼저 깨집니다. 새 레이어를 끼울 때 @ts-nocheck는 금지입니다 (check:ts-nocheck-ratchet이 막습니다) — 아래 계층이 정의한 멤버가 필요하면 그 멤버만 이름으로 적은 forward-ref 타입 하나로 캐스팅하십시오(orca-runtime-agent-prompt-rescue.ts의 AgentPromptRescueForwardRefs).

max-lines 를 넘겼을 때 쓸 수 있는 수법은 별칭 임포트만이 아닙니다. v1.4.206 에서는 upstream 이 300 줄에 딱 맞춰 둔 파일 5개에 포크 줄이 얹혀 상한을 넘었고, 게이트를 빼지 않고 네 가지로 줄였습니다 — ① 포크 필드를 교차 타입으로 분리(ForkGlobalSettings & { … }: global-settings-types.ts 에서 8 필드를 fork-global-settings.ts 로 옮겨 −30줄), ② 포크 타입을 한 이름으로 묶고 upstream 타입을 그 모듈에서 재수출해 임포트 두 줄을 한 줄로(ForkTerminalFields + TerminalPaneLayoutNode: 100자 안에 들어가야 oxfmt 가 접지 않는다 — 이름이 길면 4줄로 불어나 역효과), ③ 별칭 임포트 (v1.4.204 수법, ai-vault-reuse.ts 가 upstream 이름으로 래퍼를 재수출해 호출부를 바이트 동일하게), ④ 배럴 재수출로 인자를 흡수(ownerSurfacingWithPaneGroup as ownerSurfacing: 포크 전개 한 줄을 upstream 호출 한 줄에 접어 넣는다). 넷 다 원장에 앵커를 새로 등재해야 합니다 — 호출부가 upstream 과 같아진 만큼 사라져도 티가 안 나기 때문입니다.

upstream 이 파일을 상한에 정확히 맞춰 두면 포크 한 줄도 들어갈 자리가 없습니다(v1.4.226). local-worker-start.ts 가 그랬습니다 — 워커 패널 앵커를 넘기는 포크 한 줄이 상한을 1줄 넘겼고, 인라인 spread 는 no-useless-spread 가, 도우미 함수는 그 임포트 한 줄이 다시 막았습니다. 해법은 값을 prop 이 아니라 비동기 컨텍스트로 운반하는 것이었습니다: 호출자(workers.ts)가 별칭 임포트로 upstream 함수를 포크 래퍼(worker-start-pane-anchor.ts)로 바꾸고, 래퍼가 AsyncLocalStorage 범위 안에서 upstream 함수를 부르며, 소비 지점(workerPaneAnchorForStart)이 그 범위를 읽습니다. 두 upstream 파일의 호출부가 모두 바이트 동일해집니다. 대가는 값의 흐름이 코드에서 안 보인다는 것이라, 끝에서 끝까지 도는 테스트 (composed-workers.test.ts)가 뮤테이션으로 잡는지 반드시 확인하십시오. 테스트 파일이 상한에 닿으면 포크 시드를 포크 모듈로 빼거나(packaged-enterprise-resources-test-fixture.mjs) 포크 케이스를 포크 소유 테스트 파일로 옮깁니다.

upstream 이 @ts-nocheck 를 걷어내면 그 파일의 포크 줄이 처음으로 타입체크를 받습니다(v1.4.229). v1.4.229 가 config/ts-nocheck-baseline.txt 를 80 → 0 으로 비웠고, 포크 줄이 든 파일이 8개였습니다. 가장 큰 것은 Enter 재전송 구제였습니다 — terminal-input-dispatch.ts(옛 orca-runtime-controller-knows-pty-is-live.ts)의 포크 줄이 아래 계층 메서드를 this 로 부르고 있어 타입 오류 8건이 났고, 파일도 316줄로 상한을 넘겼습니다. @ts-nocheck 를 되살리지 말고(래칫이 거부합니다) 포크 계층에서 upstream 메서드를 override 로 감싸십시오 — orca-runtime-agent-prompt-rescue.ts 의 sendTerminalAgentPrompt 가 super 를 부르고, upstream 이 버리는 값 (쓰기 결과의 stalled)은 직렬화 계층 override 가 AsyncLocalStorage(agent-prompt-stall-scope.ts)로 운반합니다. upstream 파일은 바이트 동일해져 다음 머지에서 충돌하지 않고, upstream 이 메서드 이름을 바꾸면 override 가 타입 오류로 알려 줍니다. 같은 판에서 validateOrchestrationAgentLauncher·getStructuredAgentSessionCreateSupport 의 허용목록 게이트도 OrcaRuntimeForkSurface 의 override 로 옮겼습니다. 포크 계층이 체인에 끼는 접합점 (extends OrcaRuntimeWithAgentPromptRescue, extends OrcaRuntimeForkSurface)은 원장 앵커가 지킵니다 — --theirs 로 풀리면 override 가 타입 오류 없이 통째로 빠지기 때문입니다.

upstream 이 체인 최상단을 빌드 스위치로 고르면 그 선택을 포크 계층이 소유합니다(v1.4.230). orca-runtime.ts 가 const OrcaRuntimeSidebarBase = ORCA_SIDEBAR_INPUTS_IS_MAIN ? OrcaRuntimeWithSidebarTemporal : OrcaRuntimeWithMigrationCatalog 를 두고 그것을 상속하기 시작했습니다. 포크 계층을 MigrationCatalog 위에 그대로 두면 스위치를 켠 빌드에서만 사이드바 계층이 조용히 빠지므로, 같은 조건식을 orca-runtime-fork-surface.ts 의 ForkSurfaceBase 로 옮기고 orca-runtime.ts 에서는 쓰이지 않게 된 const 를 지웠습니다. upstream 이 스위치를 없애면 그 import 가 깨져 tc 가 먼저 알려 줍니다.

빌드 설정 파일에 포크 define 을 둘 자리가 없을 때는 upstream 상수를 빌립니다(v1.4.229). 내장 에이전트 하한은 "electron-vite 번들인가"를 포크 define(ORCA_BUNDLED_MAIN_BUILD)으로 판단했는데 electron.vite.config.ts 가 상한에 정확히 닿았습니다. upstream 이 모든 main 번들에 치환하고 vitest 는 정의하지 않는 ORCA_BUILD_IDENTITY 의 존재 여부로 바꿨습니다(enterprise-policy-file.ts). upstream 이 그 define 을 지우면 하한이 조용히 꺼지므로 원장이 electron.vite.config.ts 의 그 줄을 지켜야 합니다.

tests/e2e/.cross-version-checkouts/ 는 메인 워크트리에서만 테스트를 깨뜨립니다. gitignore 된 구버전 체크아웃이라 새 워크트리에는 없습니다. v1.4.206 의 신규 e2e-worker-env-isolation.unit.test.ts 는 tests/e2e 를 재귀 순회하는데 이 디렉터리에서 스택이 넘칩니다(순정 워크트리에서는 통과). wsl-exec-mode-separator 와 같은 부류이니 메인 워크트리 전용 실패가 보이면 먼저 이 디렉터리를 의심하십시오.

테스트 판정은 3자 대조로 하십시오. 전체 스위트 1회 실행은 판정이 아닙니다(이 맥에서 상시 3천여 파일이 환경 문제로 실패합니다). 워크트리 두 개를 띄우고 실패 파일 집합을 비교하면 진짜 회귀만 남습니다:

git worktree add --detach /tmp/pre  <포크tip>     # 머지 전 포크
git worktree add --detach /tmp/base <새태그>       # 순정 upstream
# 각 트리에서: vitest run --reporter=json --outputFile=...
# 회귀 = (머지 실패집합 − 포크tip 실패집합) − 순정upstream 실패집합

v1.4.196에서는 이 방법으로 3,804건의 실패 파일에서 진짜 회귀 9건을 뽑아냈습니다(나머지는 환경 문제거나 out/ 스테일 빌드 산출물). 9건 중 2건이 실제 게이트 유실이었습니다 — validateOrchestrationAgentLauncher와 createManagedWorktree의 allowedAgents 검사.

위 목록에는 정책 작업과 무관한 upstream 접근성/드리프트 수정도 섞여 있습니다(예: src/renderer/src/components/DetachedHeadBadge.tsx, src/renderer/src/components/skills/skill-freshness-group.tsx). 이런 커밋은 upstream에 PR로 올려 없애는 편이 장기적으로 리베이스 충돌 면적을 줄입니다.


7. 오케스트레이션 규칙 원장

/orchestration으로 워커를 띄우면 코디네이터가 태스크마다 프로젝트 규칙을 골라 워커의 --spec에 주입합니다. 워커는 매번 새 세션이고 새 worktree일 때가 많아 코디네이터의 세션 메모리를 구조적으로 볼 수 없기 때문입니다(세션 메모리는 작업 디렉터리 키). dispatch spec이 규칙을 워커에게 전달하는 유일한 경로입니다.

무엇 어디 성격
프로토콜(동작 규정) skill-guides/orchestration.md의 ## Project Rule Ledger 바이너리에 임베드
규칙 데이터 .claude/harness/rules.md 커밋. 블록 12개 / 200줄 상한
승격 후보 .claude/harness/candidates.md 커밋. 어떤 컨텍스트에도 안 들어감
폐기 이력 .claude/harness/retired.md 커밋. 부활 방지용

패키징 경로 — URL로 설치해도 동작합니다. 프로토콜은 스킬 파일이 아니라 바이너리가 서빙합니다.

skill-guides/orchestration.md          ← 유일한 편집 대상
  └─ pnpm generate:bundled-skill-guides
       ├─ src/cli/bundled-skill-guides.ts   → build:cli → 설치 프로그램에 포함
       └─ skills/orchestration/SKILL.md     → 얇은 디스커버리 스텁(내용 안 바뀜)

마켓플레이스 URL로 설치하면 사용자는 스텁만 받고, 스텁은 orca skills get orchestration을 실행하라고 지시합니다. 그 명령이 설치된 사내 바이너리에서 위 절을 그대로 꺼내므로 원장 프로토콜이 그대로 적용됩니다. 반대로 스텁에 프로토콜을 넣으면 바이너리와 드리프트하므로 넣지 않습니다(orchestration-skill-rule-ledger.test.mjs가 이를 강제합니다 — upstream 이 v1.4.223 에서 원래 테스트 orchestration-skill-guidance.test.mjs 를 지워 포크 소유로 옮겼습니다).

편집 절차: skill-guides/orchestration.md를 고친 뒤 pnpm generate:bundled-skill-guides를 반드시 실행하세요. 빼먹으면 pnpm lint의 verify:bundled-skill-guides가 stale로 실패합니다.

규칙이 늘어나는 방식: 워커 완료 후 사용자가 지적하거나 실행 도중 원장에 없던 프로젝트 요구를 추가로 주면, 코디네이터가 candidates.md에 자동으로 한 건 적습니다(승인 불필요 — 어떤 컨텍스트에도 안 들어가므로 비용 0). 승격 심사는 한 run의 마지막 워커가 정산된 뒤에 묻고, 세션이 예고 없이 끝날 수 있으므로 다음 run이 rules.md를 읽는 시점에 미결 후보를 다시 확인합니다. 최종 반영은 항상 사람이 승인합니다. 관측 2회 미만, pnpm lint/pnpm typecheck가 이미 잡는 것, 기존 블록과 scope가 겹치는 것, 이 머신에서만 참인 것은 승격하지 않습니다.

프로토콜은 사내 빌드에만 실립니다. 원장 데이터(.claude/harness/)는 프로젝트마다 다르고 저장소에 커밋되지만, 동작 규정은 이 포크의 orca 바이너리가 서빙합니다. 공개 upstream Orca 설치본으로 같은 프로젝트를 열면 orca skills get orchestration에 이 절이 없어 원장이 그대로 무시됩니다 — 파일은 있는데 아무도 안 읽는 상태가 되므로, 팀에 공유할 때 사내 빌드 사용 여부를 함께 확인하세요.


7-1. Confluence 연결 (설정 → 연동)

에이전트가 사내 위키를 읽을 수 있도록 URL + 토큰만 받습니다.

항목 값
기본 URL https://confluence-mirror.samsungds.net
토큰 Confluence → 프로필 → Personal Access Tokens 에서 발급
사용자 이름 보통 비워 둡니다. 채우면 자격증명이 Basic으로 나갑니다(아래)
  • 자체 호스팅(Server / Data Center) 전용입니다. Atlassian Cloud 주소(*.atlassian.net)는 입력 단계에서 거부합니다 — Cloud는 API 경로(/wiki/rest/api)와 인증 헤더(email 기반 Basic)가 달라서, 받아 두면 절대 동작하지 않을 자격증명을 저장하는 셈이 됩니다.
  • 사용자 이름이 비어 있으면 토큰이 Authorization: Bearer 로, 채워져 있으면 Authorization: Basic 으로 나갑니다. 이 PC에만 저장되고 OS가 지원하면 암호화됩니다 (opencodeSessionCookie·httpProxyUrl과 같은 보호 슬롯). 화면에 다시 보여 주지 않으며, 비워 두면 기존 값이 유지되고 연결 해제로만 지웁니다.
  • ⚠️ 읽기 전용입니다. 이 미러는 쓰기를 받지 않을 것으로 보고 페이지 생성·수정 기능을 제공하지 않습니다. Confluence는 출처이지 목적지가 아닙니다.
  • REST 경로를 통째로 붙여 넣어도 됩니다(…/rest/api/content/12345) — 저장할 때 잘라 냅니다. 컨텍스트 경로(…/confluence)는 보존합니다.

연결 테스트가 실패할 때

설정 → 연동 → 사내 Confluence의 연결 테스트가 알려 주는 것과 대처:

결과 뜻과 대처
리다이렉트된다고 나온다 넣은 URL이 최종 주소가 아닙니다. 메시지가 가야 할 주소를 알려 주니 그걸 URL 칸에 넣으세요. http://로 넣어 서버가 https://로 301을 주는 경우가 가장 흔합니다
토큰이 거부됐다(401/403) 주소는 맞고 토큰이 문제입니다. 다시 발급하세요
Basic 인증을 요구한다고 나온다 서버가 베어러 토큰을 안 받은 것입니다. PAT는 Confluence Server/DC 7.9 이상에만 있고, 그 이하이거나 PAT를 꺼 둔 설치본은 사용자 이름 + 비밀번호를 받습니다 — 사용자 이름 칸을 채우면 Basic으로 나갑니다. 이미 채웠는데도 계속 나면 앞단 프록시가 Authorization 헤더를 떼고 있을 수 있습니다
그 URL에 API가 없다(404) 설치본이 하위 경로에 있는 경우입니다. …/confluence 처럼 경로까지 넣으세요
CAPTCHA를 요구한다고 나온다 로그인 실패가 누적돼 계정이 잠긴 상태입니다. 브라우저로 한 번 로그인해 CAPTCHA를 풀고 다시 테스트하세요 — 토큰 문제가 아닙니다
게이트웨이가 답한 것 같다고 나온다 Confluence 자신의 인증 헤더가 하나도 없는 401입니다. 앞단 SSO 게이트웨이가 막은 것이니 주소를 관리자와 확인하세요
토큰에 공백/줄바꿈이 있다고 나온다 줄바꿈된 메일·표에서 복사하면 중간에 개행이 섞입니다. 한 줄로 다시 붙여넣으세요

⚠️ 왜 리다이렉트를 따라가지 않는가 — 웹 표준상 리다이렉트가 주소(origin)를 바꾸면 Authorization 헤더가 제거됩니다(http:→https:도 바뀐 것으로 봅니다). 그대로 따라가면 토큰 없이 재요청이 나가고 서버는 당연히 401을 돌려줍니다. 즉 *"301도 나고 401도 난다"*는 토큰 문제가 아니라 주소 문제입니다. 그래서 따라가지 않고 갈 주소를 알려 줍니다.

💡 위키 페이지 주소를 그대로 붙여넣어도 됩니다. …/display/TEAM/문서, …/pages/viewpage.action?pageId=… 같은 주소를 넣으면 앞부분(기지 주소)만 남기고 잘라 냅니다.


개발 / 저장소 구조

  • 아키텍처와 명령어 개요: CLAUDE.md (upstream의 @AGENTS.md 스텁을 이 포크가 확장한 파일)
  • 프로젝트 규칙(크로스플랫폼, Git 호환성, 디자인 시스템 등): AGENTS.md (upstream 원본, 손대지 않음)
  • 사내 커스터마이즈의 핵심:
    • src/shared/enterprise-policy.ts — 순수 리졸버 + 타입 (파일 I/O 없음)
    • src/main/enterprise/enterprise-policy-file.ts — 정책 파일 탐색·파싱·캐시
    • src/main/enterprise/enterprise-policy-trace.ts — 채택된 정책을 NDJSON 로그에 1회 기록
    • src/main/enterprise/enterprise-network-guard.ts — opt-in 허용 목록
    • src/main/enterprise/enterprise-secure-dns.ts — lockdown 시 DNS-over-HTTPS 승격 차단
    • src/main/enterprise/enterprise-direct-download-guard.ts — node:https 직접 다운로드 거부
    • src/shared/enterprise-policy-fixture.ts — 테스트 전용 픽스처
    • config/vitest-enterprise-policy-isolation.ts — 이 포크를 빌드하는 머신에는 머신 전역 정책 파일이 깔려 있으므로, 테스트 스위트가 lockdown 상태로 돌지 않도록 무력화
    • src/shared/gateway-auth.ts + src/main/gateway/ — 사내 게이트웨이 로그인 레인(§3.1). 자격증명은 다루지 않고 gateway-cli의 실행과 상태 표시만 합니다. 이전의 AWS SSO 레인(src/main/aws/, awsSso:* IPC)을 완전히 대체했습니다
    • upstream 파일에 삽입된 게이트 목록은 §6 표 참고
  • 원본 프로젝트의 일반 기여/개발 안내: 원본 CONTRIBUTING.md

License

원본 Orca는 MIT License 하에 배포되는 오픈소스이며, 이 포크도 동일 라이선스를 따릅니다.

About

Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and VPS.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages