From 45e08fea87830ab7a08dd8fef46afdd62c325c76 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 16:14:49 +0900 Subject: [PATCH 01/34] =?UTF-8?q?docs(devlog):=20remote=20hub=20restack=20?= =?UTF-8?q?roadmap=20=E2=80=94=20measured=20conflict=20surface=20and=20blo?= =?UTF-8?q?cker=20reclassification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../260901_remote_hub_restack/000_research.md | 92 +++++++++++++++++++ .../010_wp1_design_contract.md | 77 ++++++++++++++++ .../020_wp2_p1_protocol_catalog.md | 55 +++++++++++ .../030_wp3_p2_remote_session.md | 43 +++++++++ .../040_wp4_p3_connect.md | 34 +++++++ .../050_wp5_p4_two_plane.md | 40 ++++++++ .../060_wp6_p5_deploy.md | 46 ++++++++++ .../070_wp7_p6_hardening.md | 51 ++++++++++ .../080_wp8_stack_integrity.md | 44 +++++++++ 9 files changed, 482 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/000_research.md create mode 100644 devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md create mode 100644 devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md create mode 100644 devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md create mode 100644 devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md create mode 100644 devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md create mode 100644 devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md create mode 100644 devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md create mode 100644 devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md diff --git a/devlog/_plan/260901_remote_hub_restack/000_research.md b/devlog/_plan/260901_remote_hub_restack/000_research.md new file mode 100644 index 0000000000..9e9fadce6a --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/000_research.md @@ -0,0 +1,92 @@ +# Remote hub 스택 재스택 — 리서치 + +측정 시각 2026-09-01, base `origin/dev@15b0f701e`. + +## 대상 + +7단계 스택. 베이스만 `dev`를 향하고 나머지는 직전 단계의 head 브랜치를 향한다. + +| PR | 브랜치 | base | 커밋 | 파일 | draft | +|----|--------|------|------|------|-------| +| #2771 | codex/remote-hub-design | dev | 9 | 12 | no | +| #2772 | codex/remote-hub-p1 | design | 5 | 14 | no | +| #2776 | codex/remote-hub-p2 | p1 | 6 | 32 | yes | +| #2777 | codex/remote-hub-p3 | p2 | 11 | 34 | no | +| #2781 | codex/remote-hub-p4 | p3 | 8 | 48 | yes | +| #2786 | codex/remote-hub-p5 | p4 | 8 | 19 | no | +| #2789 | codex/remote-hub-p6 | p5 | 17 | 95 | yes | + +전부 `Ingwannu`의 CHANGES_REQUESTED가 걸려 있다. 포크 지점은 +`8b1b65b8d`이고 그 이후 `dev`는 336커밋 전진하면서 1075개 파일을 건드렸다. + +## 충돌 표면 — 실측 + +시험 워크트리에서 `rebase --onto`를 단계별로 순차 실행해 측정했다. +design 단계는 문서 전용이라 충돌 없이 통과한다(`f17605021`). p1부터 걸린다. + +| 단계 | 단계 파일 | dev와 겹치는 파일 | +|------|-----------|-------------------| +| design | 12 | 0 | +| p1 | 14 | 12 | +| p2 | 32 | 15 | +| p3 | 34 | 18 | +| p4 | 48 | 17 | +| p5 | 19 | 5 | +| p6 | 95 | 58 | + +p1의 실제 충돌 3파일: `src/server/catalog-download.ts`, +`src/server/index.ts`, `src/server/management/model-routes.ts`. 세 파일 모두 +`f6367639c feat(server): add least-privilege GET /v1/catalog for remote Codex +clients (#2979)`가 마지막으로 건드렸다. 이건 우연이 아니다 — #2979는 이 스택이 +제안한 `/v1/catalog`를 별도 PR로 먼저 랜딩시킨 것이다. 즉 p1의 카탈로그 델타는 +상당 부분 이미 dev에 있다. 재스택할 때 재구현이 아니라 **중복 제거**가 필요하다. + +## 반복 후보 충돌원 + +`dev`가 포크 이후 스택 파일에 남긴 관련 랜딩: + +- `f6367639c` (#2979) — `/v1/catalog` 최소권한 라우트. p1 카탈로그 델타와 직접 중복. +- `f83368dfd` (#3057) — entitlement 삼상태. `src/server/index.ts` 공유. +- `c3da277bc` (#2891) — entitlement roster 클라이언트 버전. `model-routes.ts` 공유. +- i18n 9개 로케일 파일 — p4/p6가 전부 건드리고 dev도 계속 건드린다. 텍스트 추가 충돌이라 + 기계적이지만 건수가 많다. + +## 블로커 재분류 + +리뷰 7건을 원인별로 다시 묶으면 세 종류뿐이다. + +**(1) stale 아티팩트 — 재스택이 곧 해소** + +`tests/release-version-line.test.ts:108` 실패가 #2772/#2777/#2786에 공통으로 +걸려 있다. 원인은 확인됐다: 스택의 `package.json`이 `2.34.0`인데 현재 릴리스 +태그 라인은 `2.40.0`이다. 테스트는 "트리 버전이 최고 릴리스 태그보다 뒤면 +거절"을 주장한다. 스택이 오래된 것이 원인이고, 테스트는 정확히 설계대로 +동작했다. 재스택하면 `package.json`이 dev 쪽으로 해소되어 사라진다. +**게이트를 건드리면 안 된다.** + +**(2) 구조적 보류 — 재스택 + draft 해제로 해소** + +#2776/#2781/#2789는 "중간 스택 head라 최종 승인 불가"라는 보류다. 리뷰어는 +"stacked child가 부모 head를 타깃하는 것"을 정책 위반으로 읽었지만, 현행 +`AGENTS.md`는 열린 PR의 head를 타깃하는 stacked child를 의도된 리뷰 +워크플로로 명시하고 `enforce-target`의 wrong-base 게이트를 면제한다. +따라서 이건 정책 위반이 아니라 **리뷰 순서 문제**다. 해소 경로는 재스택 후 +draft 해제 + exact-head CI 그린 + 스택 체인 정합 확인이다. + +**(3) 실질 결함 — 코드/문서 수정 필요** + +- #2771 문서 계약 4건 (아래 010). +- #2777 `gui/tests/api-auth-memory.test.ts:23`. +- #2786 `tests/cli-headless-parity.test.ts:287` 미선언 `/api/machine/*` 7개, + `tests/update-stop-first.test.ts:225`, `tests/loopback-listener-admission.test.ts:196`, + privacy 게이트. + +(3)만 실제 작업이다. (1)은 재스택의 부산물이고 (2)는 절차다. + +## 제약 + +- 푸시는 `--no-verify` (사용자 지시). `prepush`가 전체 스위트를 부르므로 로컬에서 + 돌 수 없다. +- 로컬 전체 스위트 금지. 판정은 exact-head CI. +- 머지 금지. "머지 가능한 상태까지"가 종료선이다. +- `dev`/`main`/`preview` 직접 푸시 금지. diff --git a/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md b/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md new file mode 100644 index 0000000000..ee15d3fd24 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md @@ -0,0 +1,77 @@ +# wp1 — design(#2771) 재스택 + 문서 트러스트 경계 4건 + +브랜치 `codex/remote-hub-design`, 현재 head `bad162407`. +시험 재스택 결과 충돌 없음(`f17605021`). 문서 12파일 전용. + +## 수정 대상 4건 + +리뷰어가 `bad1624075c75592115ab92f9e49ebcf0c525ce6` exact head에 대해 제기했다. +전부 `devlog/_plan/260827_remote_hub/` 안의 설계 계약 문서다. + +### D1 — 평문 HTTP로 재사용 가능한 credential이 건너간다 + +위치: `040_phase2_remote_session.md:11-25`, `050_phase3_connect.md:324-340`. + +현재 계약은 config 플래그 두 개를 켜면 비-loopback 평문 HTTP 위로 재사용 가능한 +pairing grant가 오가고 재사용 가능한 GUI 세션이 반환되는 것을 허용한다. +운영자 opt-in은 수동적 자격증명 탈취나 on-path 교환을 막지 못한다. + +수정: HTTP를 credential-free 부트스트랩으로 강등한다. grant / 세션 / +admin token / client key 중 어느 것도 전송되기 전에 인증된 HTTPS로의 업그레이드가 +강제되어야 한다. 문서에 "평문 HTTP에서 전송 가능한 것"의 화이트리스트를 명시하고, +그 목록에 credential류가 없음을 계약으로 못박는다. + +### D2 — identity-varying 응답에 공유 strong ETag + +위치: `030_phase1_protocol_catalog.md:29-40`. + +인증된 카탈로그 응답이 키마다 내용이 다른데도 공유 strong ETag를 갖고 +`private, no-cache`로 나간다. `x-opencodex-key-id`로 vary한다고 적혀 있지만, +identity로 파티션된 validator/캐시 키가 실제로 테스트되지 않은 상태에서 +저장된 200/304 표현이 키 타입과 키 id를 넘나들 수 있다. + +수정: identity를 실은 응답에 `Cache-Control: no-store`를 쓰고 ETag/304를 +제거한다. 파티션을 유지하려면 파티션이 증명되어야 하는데, 증명 비용보다 +no-store가 싸다. 이 결정을 문서에 근거와 함께 기록한다. + +### D3 — Origin이 한 엔드포인트에만 전달된다 + +위치: `060_phase4_two_plane.md:298-307`. + +브라우저 `Origin`을 정확히 `POST /opencodex-session`에만 전달한다. +그런데 발급된 GUI 세션은 origin에 바인딩되고 관리 API 변경은 Origin/CSRF 검사를 +한다. 릴레이된 `/api/*`의 POST/PUT/PATCH/DELETE는 허브가 필요로 하는 증거를 +잃고 실패한다. 즉 이건 보안 결함이자 기능 결함이다. + +수정: 허용된 모든 세션 인증 mutation에 대해 브라우저 Origin을 verbatim +전달한다. 합성 fallback을 두지 않는다(합성 Origin은 CSRF 검사를 무의미하게 +만든다). 허용 메서드마다 테스트를 건다. + +### D4 — 키 로테이션 크래시 복구가 잘못된 증거를 신뢰한다 + +위치: `080_phase6_hardening.md:318-323`. + +"current와 backup 둘 다 probe 성공"을 current 파일이 새 키를 담고 있다는 +증거로 취급한다. `pendingOperation` 저장 직후 크래시가 나면 두 파일이 모두 +옛 키를 담은 채로 둘 다 probe에 성공할 수 있다. 그러면 복구 로직은 이미 +끝났다고 판단하고 로테이션을 유실한다. + +수정: probe 성공 쌍을 완료 증거로 쓰지 않는다. 어느 파일이 어느 세대를 +담고 있는지 구별하는 증거(세대 마커 또는 pendingOperation 레코드 자체)로 +판정하고, 구별이 불가능한 상태는 미완료로 취급해 재개한다. + +## 작업 순서 + +1. `origin/dev` 위로 `rebase --onto` (충돌 없음 확인됨). +2. D1~D4를 설계 문서에 반영. 각 수정은 "무엇이 틀렸는지 → 새 계약" 형태로 + 기존 문단을 대체한다. 리뷰 코멘트를 인용만 하고 계약을 안 바꾸면 무의미하다. +3. `--no-verify` 푸시. +4. PR #2771 설명 갱신 — 4건 각각 어디서 어떻게 해소됐는지 파일:줄로 지목. + +## 검증 + +- `git range-diff origin/dev..bad162407 origin/dev..` 로 9커밋 보존 확인 + (D1~D4 수정 커밋은 추가분). +- 문서 전용이므로 로컬 테스트 대상 없음. exact-head CI 그린으로 판정. +- D1~D4의 구현 정합은 각각 wp3(D1), wp2(D2), wp5(D3), wp7(D4)에서 처리한다. + 이 단계는 계약만 고친다. diff --git a/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md new file mode 100644 index 0000000000..5d27c2fb26 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md @@ -0,0 +1,55 @@ +# wp2 — p1(#2772) 재스택 + 카탈로그 중복 제거 + +브랜치 `codex/remote-hub-p1`, head `c10ef21a9`, 5커밋 / 14파일. + +## 실측 충돌 + +`rebase --onto trial-remote-hub-design origin/codex/remote-hub-design` 에서 +`4fa130bf6 feat(remote): serve authenticated catalog snapshots` 가 3파일에서 멈춘다. + +- `src/server/catalog-download.ts` +- `src/server/index.ts` +- `src/server/management/model-routes.ts` + +## 원인 — 재구현이 아니라 선행 랜딩 + +세 파일의 dev 쪽 마지막 변경은 전부 `f6367639c feat(server): add +least-privilege GET /v1/catalog for remote Codex clients (#2979)` 이다. +#2979는 이 스택이 설계한 `/v1/catalog`를 별도 PR로 먼저 랜딩시킨 것이다. + +따라서 충돌 해소 원칙은 **dev를 이긴다**이다. p1의 카탈로그 델타 중 #2979가 +이미 제공하는 부분은 버리고, p1에만 있는 부분(프로토콜 메타데이터, 런타임 role, +`x-opencodex-key-id` 처리)만 얹는다. 반대로 하면 랜딩된 최소권한 계약을 +되돌리게 된다. + +해소 후 반드시 확인할 것: `/v1/catalog`의 최소권한 admission이 p1 델타에 의해 +느슨해지지 않았는가. `tests/api-catalog-route.test.ts`가 이 계약을 들고 있다. + +## D2 구현 정합 + +010의 D2(identity-varying 응답의 ETag/304 제거)가 이 단계 코드에 걸린다. +`67e818da1 test(remote): cover phase one protocol and catalog contract` 와 +`c10ef21a9 fix(remote): type catalog bytes over ArrayBuffer and scope the +key-id warn assertion` 이 해당 경로를 다룬다. 재스택 후 카탈로그 응답 헤더를 +`no-store` + ETag 없음으로 맞추고 테스트를 그에 맞게 고친다. + +## release-version-line + +리뷰가 지목한 `tests/release-version-line.test.ts:108` 실패는 스택 +`package.json`이 `2.34.0`이고 릴리스 태그 라인이 `2.40.0`이라서 난다. +재스택하면 dev 쪽 `2.40.0`으로 해소되어 자동 소멸한다. 테스트를 손대지 않는다. + +## privacy:scan + +p1에서 privacy 게이트가 실패한다고 기록돼 있다. 재스택 후 실제로 재현하는지 +먼저 확인한다(`bun run privacy:scan`은 전체 스위트가 아니므로 허용 범위). +재현되면 로그/직렬화 경로에서 자격증명이나 계정 식별자가 새는 지점을 찾아 +**게이트가 아니라 코드**를 고친다. + +## 검증 + +- `git range-diff` 5커밋 보존. +- `bun test tests/api-catalog-route.test.ts tests/server-auth.test.ts tests/config.test.ts` + (변경 파일 직결 포커스드). +- `bun run privacy:scan`. +- 최종 판정은 exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md new file mode 100644 index 0000000000..45ccd7ce45 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md @@ -0,0 +1,43 @@ +# wp3 — p2(#2776) 재스택 + D1 HTTPS 업그레이드 정합 + +브랜치 `codex/remote-hub-p2`, head `7099760a5`, 6커밋 / 32파일, draft. +dev와 겹치는 파일 15개. + +## 겹침 + +`src/cli/dispatch.ts`, `src/cli/help.ts`, `src/cli/registry.ts`, +`src/config.ts`, `src/server/auth-cors.ts`, `src/server/gui-static.ts`, +`src/server/index.ts`, `src/server/proxy-liveness.ts`, `src/types.ts`, +`src/types/config.ts` + 테스트 5. + +CLI 레지스트리와 config 타입은 dev가 계속 확장한 곳이라 추가-추가 충돌이 +예상된다. 원칙: dev의 항목을 지우지 않고 스택 항목을 병렬로 추가한다. + +## D1 — 평문 HTTP credential 금지 구현 + +010의 D1이 이 단계에서 코드가 된다. 관련 커밋: + +- `1e3f7d2b7 feat(remote-gui): add remote session issuance and pairing` +- `6c8dd333e fix(remote-gui): enforce exact bootstrap destination` +- `7099760a5 fix(remote-gui): preserve renewal and mutation origin checks` + +요구: 비-loopback 평문 HTTP에서는 pairing grant도 GUI 세션도 발급되지 않는다. +opt-in 플래그로 이 금지를 뚫을 수 없어야 한다. HTTP는 "여기 HTTPS 엔드포인트가 +있다"만 알려주는 credential-free 부트스트랩으로 남긴다. + +테스트: 평문 HTTP 비-loopback 요청에 대해 grant 발급이 거절되는 네거티브, +그리고 loopback은 기존대로 허용되는 포지티브. `gui/tests/connect-pairing.test.ts`와 +서버 쪽 remote-session 테스트에 건다. + +## draft 해제 + +#2776은 draft이고 base가 `codex/remote-hub-p1`이다. `AGENTS.md`의 stacked child +규정상 이 base는 정당하다 — `enforce-target`은 열린 부모 head를 타깃하는 자식의 +wrong-base 게이트를 건너뛴다. 재스택 + CI 그린 후 draft를 해제한다. + +## 검증 + +- `git range-diff` 6커밋 보존. +- `bun test tests/server-auth.test.ts tests/config.test.ts tests/cli-registry.test.ts` + + `cd gui && bun test tests/connect-pairing.test.ts`. +- exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md new file mode 100644 index 0000000000..9804ef5066 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md @@ -0,0 +1,34 @@ +# wp4 — p3(#2777) 재스택 + gui api-auth-memory 경계 보존 + +브랜치 `codex/remote-hub-p3`, head `aa2615953`, 11커밋 / 34파일. +dev와 겹치는 파일 18개 — 이 스택에서 CLI 표면 겹침이 가장 넓다. + +## 겹침 + +`src/cli/{claude,dispatch,help,index,registry,runtime-api,status}.ts`, +`src/config.ts`, `src/lib/service-secrets.ts`, `src/types.ts`, +`src/types/config.ts` + 테스트 7(`cli-headless-parity`, +`cli-start-journal-order`, `cli-status-json` 포함). + +`cli-headless-parity`는 wp6에서도 문제를 일으키는 파일이다. 여기서 CLI 표면이 +늘어나므로, p3 재스택 시점에 이미 새 명령이 headless 선언에 들어가 있는지 +확인해두면 wp6의 부담이 준다. + +## 블로커 — gui/tests/api-auth-memory.test.ts:23 + +리뷰어 표현은 "현재 대시보드 auth-memory 경계를 보존하라"이다. 즉 스택이 +대시보드 인증 상태의 메모리 보관 계약을 바꿨고, dev 쪽 계약과 어긋났다. +재스택 후 실패를 재현해서 **어느 쪽 계약이 맞는지** 먼저 판정한다. dev가 +맞으면 스택 코드를 dev 계약에 맞추고, 스택이 의도적으로 바꾼 것이라면 +그 근거를 PR 설명에 적고 테스트를 함께 갱신한다. 테스트만 지우는 해소는 금지. + +## release-version-line + +wp2와 동일 원인. 재스택으로 소멸. + +## 검증 + +- `git range-diff` 11커밋 보존. +- `cd gui && bun test tests/api-auth-memory.test.ts` +- `bun test tests/cli-headless-parity.test.ts tests/cli-registry.test.ts tests/cli-status-json.test.ts` +- exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md new file mode 100644 index 0000000000..c64e324d3d --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md @@ -0,0 +1,40 @@ +# wp5 — p4(#2781) 재스택 + D3 Origin verbatim 전달 + +브랜치 `codex/remote-hub-p4`, head `44f9973a2`, 8커밋 / 48파일, draft. +dev와 겹치는 파일 17개 — 그중 9개가 i18n 로케일이다. + +## 겹침 + +`src/cli/dispatch.ts`, `src/cli/index.ts`, +`src/server/management/logs-usage-routes.ts`, `src/usage/summary.ts`, +`gui/src/i18n/*.ts` 9개, `gui/src/pages/{Integrations,Storage}.tsx`, +`tests/{cli-start-journal-order,usage-summary}.test.ts`. + +i18n 충돌은 기계적이다(양쪽이 서로 다른 키를 추가). 9개 로케일 전부에서 dev 키와 +스택 키가 모두 살아남아야 한다. 하나라도 누락되면 로케일 패리티 게이트가 잡는다. + +## D3 — Origin verbatim 전달 구현 + +010의 D3이 여기서 코드가 된다. 관련 커밋: + +- `b826c200e feat(two-plane): add client machine and hub GUI planes` +- `c8a7b8ce9 feat(two-plane): harden relay and offline target states` + +현재 구현은 `POST /opencodex-session`에만 브라우저 Origin을 전달한다. +요구: 허용된 세션 인증 mutation 전체(POST/PUT/PATCH/DELETE)에 대해 Origin을 +원문 그대로 전달한다. 합성 Origin fallback을 두지 않는다 — 릴레이가 Origin을 +만들어내면 허브의 CSRF 검사는 자기 자신을 검사하는 셈이 된다. + +테스트: 허용 메서드마다 릴레이 후 허브가 받은 Origin이 브라우저 원문과 +같음을 확인하는 케이스. Origin 부재 시 요청이 거절되는 네거티브. + +## draft 해제 + +wp3와 동일 근거. 재스택 + CI 그린 후 해제. + +## 검증 + +- `git range-diff` 8커밋 보존. +- `bun test tests/usage-summary.test.ts tests/cli-start-journal-order.test.ts` +- i18n 9개 로케일 키 존재 확인. +- exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md new file mode 100644 index 0000000000..685563877e --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md @@ -0,0 +1,46 @@ +# wp6 — p5(#2786) 재스택 + 라우트 선언 / 계약 복원 + +브랜치 `codex/remote-hub-p5`, head `a62c8eba2`, 8커밋 / 19파일. +dev와 겹치는 파일 5개로 스택에서 가장 얕다. 그런데 블로커는 가장 많다. + +## 블로커 4건 — 전부 계약 위반 + +리뷰어가 "인프라 노이즈가 아니라 제품 계약"이라고 못박은 항목들이다. + +### tests/cli-headless-parity.test.ts:287 — 미선언 /api/machine/* 7개 + +p5가 머신 관리 라우트 7개를 추가했는데 CLI/headless 표면 선언에 등록하지 +않았다. 이 테스트는 "서버가 여는 라우트와 CLI가 선언한 표면이 일치한다"를 +주장한다. 해소는 둘 중 하나: 7개 라우트를 명시 선언하거나, 제거한다. +숨기는 방향(테스트 예외 추가)은 금지. + +먼저 7개가 무엇인지 열거하고, 각각이 이 단계에 필요한지 판정한다. +배포 단계가 실제로 쓰지 않는 라우트가 섞여 있으면 그건 제거가 정답이다. + +### tests/update-stop-first.test.ts:225 — stop-first 계약 + +업데이트 시 먼저 중지한다는 계약이 깨졌다. p5가 관리 ingress를 추가하면서 +라이프사이클 순서를 건드렸을 가능성이 높다. `149b7215a feat(deploy): add +loopback hub management ingress` 부터 본다. + +### tests/loopback-listener-admission.test.ts:196 — role-admission 계약 + +loopback 리스너의 admission 규칙이 깨졌다. `d6461bfd2 feat(deploy): harden +management ingress allowlist` 가 allowlist를 바꾸면서 기존 admission을 +덮었는지 확인한다. 두 allowlist가 공존해야 하는 구조라면 병합한다. + +### privacy 게이트 + +배포 가이드와 ingress 로깅에서 자격증명/호스트 식별자가 새는지 확인한다. +`bun run privacy:scan`으로 재현하고 코드를 고친다. + +## release-version-line + +wp2와 동일 원인. 재스택으로 소멸. + +## 검증 + +- `git range-diff` 8커밋 보존. +- `bun test tests/cli-headless-parity.test.ts tests/update-stop-first.test.ts tests/loopback-listener-admission.test.ts tests/service.test.ts` +- `bun run privacy:scan` +- exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md new file mode 100644 index 0000000000..cc1cf2e0fd --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md @@ -0,0 +1,51 @@ +# wp7 — p6(#2789) 재스택 + D4 로테이션 크래시 복구 + +브랜치 `codex/remote-hub-p6`, head `207254fe0`, 17커밋 / 95파일, draft. +dev와 겹치는 파일 58개 — 스택 전체에서 가장 크다. + +## 겹침 + +`src/cli/{access,index,registry}.ts`, `src/config.ts`, +`src/lib/service-secrets.ts`, `src/server/auth-cors.ts`, +`src/server/index.ts`, `src/server/management-api.ts`, +`src/server/management/{context,oauth-account-routes}.ts`, +`src/types/config.ts`, i18n 9개, docs-site 7로케일 다수. + +docs-site 겹침이 큰 덩어리인데 대부분 로케일 문서라 기계적이다. +실제 판단이 필요한 건 `management-api.ts`, `management/context.ts`, +`oauth-account-routes.ts` — dev가 이번 트레인에서 계속 건드린 곳이다. + +## D4 — 크래시 복구 판정 수정 + +010의 D4가 여기서 코드가 된다. 관련 커밋: + +- `a83073115 feat(hardening): recover client key rotation through token backup` +- `cc620f7b7 fix(hardening): gate startup on rotation recovery state` + +현재: current와 backup 둘 다 probe 성공이면 로테이션 완료로 본다. +문제: `pendingOperation` 저장 직후 크래시 시 두 파일 모두 옛 키를 담고 +둘 다 probe에 성공한다 → 로테이션이 조용히 유실된다. + +수정: 완료 판정을 "probe 성공 쌍"이 아니라 세대 구별 증거에 건다. +어느 파일이 어느 세대인지 판별할 수 없으면 미완료로 보고 재개한다. +즉 판정이 애매하면 안전한 쪽(재개)으로 넘어져야 한다. + +테스트: pendingOperation 저장 직후 크래시를 흉내낸 상태(양쪽 옛 키)에서 +시작 시 로테이션이 재개되는지 확인하는 레드-퍼스트 케이스. + +## 리뷰어가 예고한 최종 보안 심사 항목 + +#2789 코멘트가 재리뷰 시 볼 항목을 나열했다. 재스택 시 이 목록을 체크리스트로 +쓴다: 로테이션 크래시 복구, 토큰 백업 소유권/정리, 일회성 시크릿 노출, +세션 무효화, pairing 레이트 리밋, 릴레이 SSRF/헤더 스트리핑, 취소. + +## draft 해제 + +wp3와 동일 근거. + +## 검증 + +- `git range-diff` 17커밋 보존. +- 로테이션/시크릿 관련 포커스드 테스트. +- `bun run privacy:scan` +- exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md new file mode 100644 index 0000000000..2a7ad0f6dc --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md @@ -0,0 +1,44 @@ +# wp8 — 스택 체인 정합 + 최종 판정 + +7단계가 전부 푸시된 뒤 실행하는 마감 사이클. + +## 체인 정합 + +각 PR의 base가 직전 단계 head 브랜치를 정확히 가리켜야 한다. + +| PR | base여야 하는 것 | +|----|------------------| +| #2771 | dev | +| #2772 | codex/remote-hub-design | +| #2776 | codex/remote-hub-p1 | +| #2777 | codex/remote-hub-p2 | +| #2781 | codex/remote-hub-p3 | +| #2786 | codex/remote-hub-p4 | +| #2789 | codex/remote-hub-p5 | + +재스택 과정에서 GitHub가 base를 자동 변경하는 경우가 있으므로 푸시 후 매번 +확인한다. base가 어긋나면 각 PR의 diff가 상류 델타를 삼켜서 리뷰가 불가능해진다. + +## 계보 확인 + +`git merge-base --is-ancestor origin/dev origin/codex/remote-hub-` 가 +7단계 전부에서 참이어야 한다. 하나라도 거짓이면 그 단계는 재스택이 안 된 것이다. + +## draft 해제 + +#2776 / #2781 / #2789. CI 그린 확인 후에만. + +## PR 설명 갱신 + +각 PR에 재스택 사실과 블로커 해소 내역을 적는다. 리뷰어가 exact head에 걸어둔 +CHANGES_REQUESTED는 새 head에서 자동 해제되지 않으므로, 무엇이 어떻게 +해소됐는지 파일:줄로 지목해야 재리뷰가 가능하다. + +## 종료선 + +DONE = 7단계 재스택 푸시 완료, B1~B7 해소, exact-head CI 그린, base 체인 정합, +draft 해제. **머지는 하지 않는다** — 사용자 요청은 "머지 가능한 정도까지 세팅"이다. + +한 단계가 막히면 그 단계만 BLOCKED으로 증거와 함께 보고하고 상류는 계속한다. +단, 스택이므로 하류가 막히면 상류는 그 위에 쌓을 수 없다. 그 경우 막힌 단계를 +건너뛴 재스택은 하지 않고 BLOCKED으로 보고한다. From 66bafbc57794c4cd5fa29c7ce2458648341d8f0b Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 16:34:37 +0900 Subject: [PATCH 02/34] =?UTF-8?q?docs(devlog):=20fold=20audit=20r1=20?= =?UTF-8?q?=E2=80=94=2010=20blockers=20closed,=2033=20review=20threads=20l?= =?UTF-8?q?edgered,=20phase=20ownership=20corrected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../260901_remote_hub_restack/000_research.md | 64 +++++++++---- .../002_audit_r1_synthesis.md | 96 +++++++++++++++++++ .../003_review_thread_ledger.md | 86 +++++++++++++++++ .../010_wp1_design_contract.md | 54 +++++++++-- .../020_wp2_p1_protocol_catalog.md | 33 +++++-- .../030_wp3_p2_remote_session.md | 34 ++++++- .../040_wp4_p3_connect.md | 42 +++++--- .../050_wp5_p4_two_plane.md | 42 +++++++- .../060_wp6_p5_deploy.md | 35 +++++-- .../070_wp7_p6_hardening.md | 34 ++++++- .../080_wp8_stack_integrity.md | 51 ++++++++-- 11 files changed, 495 insertions(+), 76 deletions(-) create mode 100644 devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md create mode 100644 devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md diff --git a/devlog/_plan/260901_remote_hub_restack/000_research.md b/devlog/_plan/260901_remote_hub_restack/000_research.md index 9e9fadce6a..ef15d80873 100644 --- a/devlog/_plan/260901_remote_hub_restack/000_research.md +++ b/devlog/_plan/260901_remote_hub_restack/000_research.md @@ -58,30 +58,56 @@ clients (#2979)`가 마지막으로 건드렸다. 이건 우연이 아니다 — **(1) stale 아티팩트 — 재스택이 곧 해소** `tests/release-version-line.test.ts:108` 실패가 #2772/#2777/#2786에 공통으로 -걸려 있다. 원인은 확인됐다: 스택의 `package.json`이 `2.34.0`인데 현재 릴리스 -태그 라인은 `2.40.0`이다. 테스트는 "트리 버전이 최고 릴리스 태그보다 뒤면 -거절"을 주장한다. 스택이 오래된 것이 원인이고, 테스트는 정확히 설계대로 -동작했다. 재스택하면 `package.json`이 dev 쪽으로 해소되어 사라진다. -**게이트를 건드리면 안 된다.** - -**(2) 구조적 보류 — 재스택 + draft 해제로 해소** - -#2776/#2781/#2789는 "중간 스택 head라 최종 승인 불가"라는 보류다. 리뷰어는 -"stacked child가 부모 head를 타깃하는 것"을 정책 위반으로 읽었지만, 현행 -`AGENTS.md`는 열린 PR의 head를 타깃하는 stacked child를 의도된 리뷰 -워크플로로 명시하고 `enforce-target`의 wrong-base 게이트를 면제한다. -따라서 이건 정책 위반이 아니라 **리뷰 순서 문제**다. 해소 경로는 재스택 후 -draft 해제 + exact-head CI 그린 + 스택 체인 정합 확인이다. +걸려 있다. 정확히 말하면 `:108`은 "뒤처짐" 분기가 아니라 **동일(equality)** +분기다(`:99-108`): 트리 버전이 최고 릴리스 태그와 같은데 이 커밋이 그 태그가 +가리키는 커밋이 아니면 거절한다. 스택의 `package.json`은 `2.34.0`이고 당시 +최고 태그가 `v2.34.0`이었다. + +현재 `origin/dev`는 `2.40.0`, 최고 태그는 `v2.39.0`이므로 지금 리베이스하면 +해소된다. p1은 `package.json`을 수정하지 않으므로 dev 값이 그대로 온다. +**다만 자동 소멸을 가정하지 않는다** — `v2.40.0`이 dev 전진보다 먼저 태깅되면 +재발한다. 리베이스된 head마다 `bun test tests/release-version-line.test.ts`를 +포커스드로 돌려 확인한다. **게이트는 건드리지 않는다.** + +**(2) 구조적 보류 — 자동화는 통과, 사람 리뷰는 별개** + +#2776/#2781/#2789는 "중간 스택 head라 최종 승인 불가"라는 보류다. +`AGENTS.md:278-281`과 `.github/workflows/enforce-pr-target.yml:533-557`은 +열린 부모 head를 타깃하는 stacked child에 대해 wrong-base 게이트를 실제로 +면제한다. 저자가 `lidge-jun`(push 권한)이라 기여자 readiness 체크리스트 +(`enforce-pr-target.yml:740-746`)도 적용되지 않는다. + +**그러나 이건 자동화 게이트만 통과시킨다.** 리뷰어의 CHANGES_REQUESTED는 +draft 해제로도 CI 그린으로도 해제되지 않는다. `MAINTAINERS.md:57-61`은 +비저자 메인테이너 승인과 보안 리뷰를 요구하고, Ingwannu가 유일한 비저자 +메인테이너다. 우리가 도달할 수 있는 종료선은 **재리뷰 요청 가능 상태**이며, +승인 자체는 외부 의존이다. **(3) 실질 결함 — 코드/문서 수정 필요** - #2771 문서 계약 4건 (아래 010). -- #2777 `gui/tests/api-auth-memory.test.ts:23`. -- #2786 `tests/cli-headless-parity.test.ts:287` 미선언 `/api/machine/*` 7개, - `tests/update-stop-first.test.ts:225`, `tests/loopback-listener-admission.test.ts:196`, - privacy 게이트. +- `gui/tests/api-auth-memory.test.ts:23` — #2777에 보고됐지만 **소유 단계는 p2**다. +- `tests/cli-headless-parity.test.ts:287` 미선언 `/api/machine/*` 7개 — + #2786에 보고됐지만 **소유 단계는 p4**다. +- `tests/update-stop-first.test.ts:225`, `tests/loopback-listener-admission.test.ts:196`, + privacy 게이트 — p5. +- 미해결 인라인 리뷰 스레드 **33건**(P1 6건 포함). 전수는 `003` 원장 참조. + +### 소유 단계 실측 + +"어느 PR에서 실패가 보고됐는가"와 "어느 단계가 그 결함을 도입했는가"는 다르다. +diff로 측정했다: + +- `/api/machine/` 추가 라인: p1~p3 = 0, **p4 = 49**, p5 = 0, p6 = 2. + 라우트를 도입한 건 p4다. +- `gui/tests/api-auth-memory.test.ts`를 건드리는 단계: **p2**와 p4. p3은 0. + +상류에서 고쳐야 한다. 하류에서 고치면 그 사이 단계들은 자기 head에서 빨간 채로 +남고, 그 위에 다음 단계를 쌓게 된다. + +**단계 초록 불변식:** 각 단계는 자기 head에서 초록이어야 다음 단계를 그 위에 쌓는다. -(3)만 실제 작업이다. (1)은 재스택의 부산물이고 (2)는 절차다. +(3)만 실제 작업이다. (1)은 재스택의 부산물이고 (2)는 절차 + 외부 의존이다. ## 제약 diff --git a/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md b/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md new file mode 100644 index 0000000000..838714593a --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md @@ -0,0 +1,96 @@ +# 감사 라운드 1 — 종합 + +감사자: 읽기전용 레인(gpt-5.6-sol high). verdict **FAIL**, 블로커 10건. +아래는 각 건에 대한 판정과 로드맵 수정 내역이다. 수용/반박을 명시한다. + +## A1 (High) — DONE 정의가 머지 가능 상태가 아니다 · 수용 + +080의 종료선은 CI 그린 + 체인 정합 + draft 해제까지였다. 그런데 7개 PR 전부 +`CHANGES_REQUESTED`이고 `MAINTAINERS.md`는 비저자 메인테이너 승인과 보안 +리뷰를 요구한다. CI가 초록이어도 리뷰 상태가 걸려 있으면 머지 버튼은 막힌다. + +수정: 080의 종료선에 "각 PR `reviewDecision`이 `CHANGES_REQUESTED`가 아닐 것"과 +"스레드 33개 해소"를 추가한다. 다만 승인 자체는 우리가 만들 수 없으므로, +우리 종료선은 **재리뷰 요청 가능 상태**까지다. 승인 획득은 외부 의존이며 +그 지점에서 막히면 BLOCKED으로 보고한다. + +## A2 (High) — 미해결 리뷰 스레드 33개 누락 · 수용, 가장 큰 누락 + +로드맵이 리뷰 본문만 읽고 인라인 스레드를 안 봤다. 실측 결과: +#2771: 18, #2772: 1, #2776: 2, #2777: 3, #2781: 4, #2786: 2, #2789: 3. +P1 등급이 6건 섞여 있다. 별도 원장 `003_review_thread_ledger.md`로 분리했다. + +## A3 (High) — stacked 면제의 효력 과대 해석 · 수용 + +`AGENTS.md:278-281` + `enforce-pr-target.yml:533-557`의 면제는 실재한다. +저자가 `lidge-jun`(push 권한)이라 기여자 체크리스트도 적용되지 않는다. +그러나 이건 **자동화만** 통과시킨다. 사람 리뷰의 CHANGES_REQUESTED는 그대로다. +030/050/070의 "draft 해제하면 해소" 서술을 "자동화 게이트는 통과, 리뷰는 별도"로 +정정한다. + +## A4 (High) — "dev wins"가 /v1/catalog 계약을 훼손할 수 있다 · 수용 + +감사자가 실제 차이를 열거했다: dev(#2979)는 GET+HEAD, `x-api-key` 허용, +256 MiB 캡, 507. p1은 GET only, `x-api-key` 거부, 32 MiB, 503, 그리고 +`x-opencodex-key-id`와 프로토콜 메타데이터를 **단독으로** 갖는다. + +"dev wins"를 통째로 적용하면 p1 고유 기여가 조용히 사라진다. 020을 병합 매트릭스로 +교체한다. + +## A5 (High) — 블로커 2건이 한 단계씩 늦게 배정됐다 · 수용, 실측 확인 + +직접 측정했다: + +- `/api/machine/` 추가 라인 수: p1~p3 = 0, **p4 = 49**, p5 = 0, p6 = 2. + 즉 라우트를 도입한 건 p4다. 060(wp6/p5)이 아니라 050(wp5/p4)이 고쳐야 한다. +- `gui/tests/api-auth-memory.test.ts`를 건드리는 단계: **p2**와 p4. p3은 0. + 040(wp4/p3)이 아니라 030(wp3/p2)이 고쳐야 한다. + +원칙도 함께 채택한다: **각 단계는 자기 head에서 초록이어야 다음 단계를 그 위에 쌓는다.** +상류에서 고치면 하류 리베이스는 이미 깨진 것을 옮기는 셈이 된다. + +## A6 (High) — 다섯 번째 설계 결함(D5) 누락 · 수용 + +`080_phase6_hardening.md` 8.3 응답 규칙이 "safe content type, cache control, +ETag ... 만 보존"이라고 적어, 릴레이된 세션/부트스트랩/관리 응답에 validator가 +살아남는 것을 허용한다. D2와 같은 결함이 릴레이 경로에 한 번 더 있는 것이다. +D5로 추가하고 구현은 릴레이를 처음 갖는 wp5/p4에 배정한다. + +## A7 (High) — D4 해법이 미명세 · 수용 + +"애매하면 재개"로는 부족하다는 지적이 맞다. `pendingOperation`은 어느 파일이 +새 시크릿을 담았는지 식별하지 못한다. 계약을 구체화한다: probe 이전에 후보 +identity를 비교한다 → 두 후보가 동일하면 교체 이전 상태이므로 절대 commit하지 +않는다 → abort/restore는 확인된 권위가 있을 때만 → abort 불확실 시 증거를 보존한다. + +## A8 (High) — D1의 HTTPS 업그레이드에 신뢰 앵커가 없다 · 수용 + +평문 부트스트랩이 HTTPS 엔드포인트를 "알려주는" 구조는 on-path 공격자가 다른 +유효한 HTTPS origin을 끼워넣을 수 있다. 업그레이드 대상이 의도한 허브인지 +증명할 수단이 없으면 업그레이드는 보안이 아니라 의식이다. + +채택: 비-loopback HTTP를 전면 거부하는 쪽을 기본으로 한다. 사전에 알려진 HTTPS +origin이 있는 경우에 한해 동일 호스트 scheme 업그레이드만 허용하고, 정상 +인증서 검증을 요구하며 리다이렉트에서 권위를 파생하지 않는다. + +## A9 (Medium) — 체인 검증이 부모 계보를 증명하지 못한다 · 수용 + +`origin/dev`가 조상인지만 보면, 부모를 건너뛰고 dev 위로 직접 리베이스된 +자식도 통과한다. 각 엣지를 `git merge-base --is-ancestor origin/ +origin/`로 확인하고 양쪽 OID를 기록한다. + +## A10 (Medium) — release-version 결론이 조건부로만 옳다 · 수용 + +감사자가 정확히 짚었다: `:108`은 "뒤처짐"이 아니라 **동일(equality)** 분기다. +과거 실패는 트리 버전이 `v2.34.0` 태그와 같은데 그 커밋이 아니었기 때문이다. +지금 리베이스하면 해소되지만, `v2.40.0`이 dev 전진보다 먼저 태깅되면 재발한다. +"자동 소멸"을 "리베이스된 head마다 포커스드 체크 필수"로 바꾼다. + +감사자가 `bun test tests/release-version-line.test.ts` 3 pass와 +`bun run privacy:scan` 통과를 현재 트리에서 확인했다. privacy 실패도 상속된 +staleness였다는 뜻이며, 충돌 해소 후 재발하는지만 보면 된다. + +## 반박 없음 + +10건 전부 수용한다. D2(no-store)와 D3(Origin verbatim)는 감사자도 타당하다고 +했고, 바텀업 리베이스 골격도 유지된다. 바뀐 것은 단계 소유권과 종료 게이트다. diff --git a/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md new file mode 100644 index 0000000000..04cf3ef799 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md @@ -0,0 +1,86 @@ +# 미해결 리뷰 스레드 원장 — 33건 + +`gh api graphql`로 `isResolved=false` 스레드를 전수 조회했다(2026-09-01). +각 건에 소유 단계를 배정한다. 소유 단계 = 그 결함을 처음 도입한 단계. + +## #2771 design — 18건 + +대부분 CodeRabbit의 마크다운 린트(MD018/MD022, 테이블 파이프 이스케이프)와 +문서 계약 지적이다. 실질 건만 추린다. + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T1 | 000_research.md:22 | **P1** | 미공개 보안 분석이 추적되는 공개 devlog에 있다 | wp1 | +| T2 | 060_phase4_two_plane.md:348 | P2 | 연결된 GUI에 인증된 models 경로 필요(`/v1/models`가 데이터플레인으로 감) | wp5 | +| T3 | 070_phase5_deploy.md:164 | P2 | 관리 ingress에서 GUI health 엔드포인트 보존 | wp6 | +| T4 | 040_phase2_remote_session.md:19 | Major | D1과 동일 사안 | wp1+wp3 | +| T5 | 030_phase1_protocol_catalog.md:40 | Minor | D2와 동일 사안 | wp1+wp2 | +| T6 | 060_phase4_two_plane.md:305 | Major | D3과 동일 사안 | wp1+wp5 | +| T7 | 060_phase4_two_plane.md:431 | Major | 요약 경로 보안 | wp5 | +| T8 | 080_phase6_hardening.md:323 | Major | D4와 동일 사안(교체 이전 크래시) | wp1+wp7 | +| T9 | 080_phase6_hardening.md:501 | Major | D5 — 릴레이 응답 validator 보존 | wp1+wp5 | +| T10 | 050_phase3_connect.md:308 | Major | 데이터 정합 | wp4 | +| T11 | 070_phase5_deploy.md:300 | Major | 안정성 | wp6 | +| T12-T18 | 010/020/060/070 각처 | Minor | 마크다운 린트 6건 + 미래 날짜 1건 | wp1 | + +**T1이 가장 무겁다.** `AGENTS.md`의 보안 작업 규정과 정면으로 부딪힌다: +미수정 결함의 분석은 추적 디렉터리가 아니라 스크래치에 있어야 한다. +이 스택의 devlog가 미공개 인증/세션 결함 분석을 담고 있다면, 그 부분은 +공개 전에 제거되어야 한다. wp1에서 해당 문단을 판정하고 처리한다. + +## #2772 p1 — 1건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T19 | src/server/index.ts:1013 | P2 | 확장된 readiness 응답을 `docs-site/.../cli/lifecycle.md`에 문서화 | wp2 | + +## #2776 p2 — 2건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T20 | src/server/index.ts:1684 | **P1** | pairing 바디를 버퍼링 전에 제한. `Content-Length` 없거나 chunked면 `declaredLength`가 0이 되어 미인증 호출자가 무제한 버퍼링 유발 | wp3 | +| T21 | src/types/config.ts:251 | P2 | `hub.managementPublicOrigin`, `remoteGui.allowedTailscaleUsers`, `remoteGui.allowInsecure*` 문서화 | wp3 | + +T20은 미인증 DoS다. D1과 같은 층에 있으므로 wp3에서 함께 닫는다. + +## #2777 p3 — 3건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T22 | src/client/connect.ts:229 | **P1** | 연결 전 기존 Codex journal 재소유 필요. `ocx start` 후 정상 상태에서 `injectCodexConfig`가 소유권을 잃는다 | wp4 | +| T23 | src/client/hub-client.ts:85 | P2 | 신뢰할 수 없는 `Content-Length`에 대해 응답 읽기 제한 | wp4 | +| T24 | src/cli/help.ts:35 | P2 | connect/disconnect 워크플로 문서화 | wp4 | + +## #2781 p4 — 4건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T25 | src/client/machine-listener.ts:79 | **P1** | `--management-transport relay` 선택 시 `connectClient`가 여전히 throw — 문서화된 옵션이 동작하지 않음 | wp5 | +| T26 | src/client/runtime.ts:27 | **P1** | systemd/WinSW로 뜬 런타임이 disconnect 후 재시작되지 않음(`OCX_SERVICE=1`이 분기를 건너뜀) | wp5 | +| T27 | gui/src/App.tsx:222 | P2 | disconnect 202 성공 시 targets 갱신 누락 | wp5 | +| T28 | gui/src/App.tsx:376 | P2 | pairing 완료 전 공유 페이지 게이팅 | wp5 | + +## #2786 p5 — 2건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T29 | src/client/state.ts:46 | P2 | hub role을 disconnected client state에서 배제 | wp6 | +| T30 | src/client/state.ts:85 | P2 | missing-config 부트스트랩 조건화(락 획득 전 반환으로 경쟁) | wp6 | + +## #2789 p6 — 3건 + +| # | 위치 | 등급 | 요지 | 배정 | +|---|------|------|------|------| +| T31 | src/client/connect.ts:304 | **P1** | abort 실패 시 토큰 identity 보존. 새 토큰 설치 후 abort가 일시 실패하면 복원이 잘못된 세대를 남긴다 | wp7 | +| T32 | src/client/state.ts:95 | P2 | `ocx connect status`가 진행 중인 로테이션 백업을 삭제 | wp7 | +| T33 | src/client/hub-relay.ts:282 | P2 | 릴레이 오류를 과대 응답 노출 전에 반환 | wp7 | + +T31/T32는 D4와 같은 사안의 서로 다른 얼굴이다. wp7에서 하나의 계약으로 닫는다. + +## 처리 원칙 + +1. P1 6건(T1, T20, T22, T25, T26, T31)은 반드시 코드/문서 수정으로 닫는다. +2. P2/Minor는 수정하거나, 근거를 갖춘 반박을 스레드에 남기고 resolve한다. + 침묵은 허용하지 않는다. +3. 각 스레드는 소유 단계에서 닫고, 그 단계 head가 초록이 된 뒤 다음 단계를 쌓는다. +4. resolve 후 exact head로 재리뷰를 요청한다. diff --git a/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md b/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md index ee15d3fd24..9d84c218db 100644 --- a/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md +++ b/devlog/_plan/260901_remote_hub_restack/010_wp1_design_contract.md @@ -16,10 +16,21 @@ pairing grant가 오가고 재사용 가능한 GUI 세션이 반환되는 것을 허용한다. 운영자 opt-in은 수동적 자격증명 탈취나 on-path 교환을 막지 못한다. -수정: HTTP를 credential-free 부트스트랩으로 강등한다. grant / 세션 / -admin token / client key 중 어느 것도 전송되기 전에 인증된 HTTPS로의 업그레이드가 -강제되어야 한다. 문서에 "평문 HTTP에서 전송 가능한 것"의 화이트리스트를 명시하고, -그 목록에 credential류가 없음을 계약으로 못박는다. +수정(감사 A8 반영): "HTTPS로 업그레이드"만으로는 부족하다. 평문 부트스트랩이 +HTTPS 엔드포인트를 알려주는 구조는 on-path 공격자가 자기 소유의 유효한 HTTPS +origin을 끼워넣을 수 있다 — 업그레이드 대상이 의도한 허브라는 신뢰 앵커가 없으면 +업그레이드는 보안이 아니라 의식이다. + +계약: + +1. 기본은 **비-loopback 평문 HTTP 전면 거부**다. opt-in 플래그로 뚫을 수 없다. +2. 사전에 알려진 HTTPS origin이 있는 경우에 한해 동일 호스트 scheme 업그레이드만 + 허용한다. 정상 인증서 검증을 요구하고, 리다이렉트에서 권위를 파생하지 않는다. +3. 브라우저 origin은 검증된 출처에서 와야 하며 config에서 파생하지 않는다 + (#2771 미해결 스레드 요구사항). + +문서에 "평문 HTTP에서 전송 가능한 것"의 화이트리스트를 명시하고, 그 목록에 +credential류가 없음을 계약으로 못박는다. ### D2 — identity-varying 응답에 공유 strong ETag @@ -56,9 +67,29 @@ no-store가 싸다. 이 결정을 문서에 근거와 함께 기록한다. 옛 키를 담은 채로 둘 다 probe에 성공할 수 있다. 그러면 복구 로직은 이미 끝났다고 판단하고 로테이션을 유실한다. -수정: probe 성공 쌍을 완료 증거로 쓰지 않는다. 어느 파일이 어느 세대를 -담고 있는지 구별하는 증거(세대 마커 또는 pendingOperation 레코드 자체)로 -판정하고, 구별이 불가능한 상태는 미완료로 취급해 재개한다. +수정(감사 A7 반영): "애매하면 재개"로는 부족하다. `pendingOperation`은 어느 +파일이 새 시크릿을 담았는지 식별하지 못하고, 그 시크릿은 마커 저장 이후에도 +유실될 수 있다. 계약을 다음 순서로 못박는다: + +1. probe **이전에** 두 후보의 identity를 비교한다. +2. 두 후보가 동일하면 교체 이전 상태다 — 절대 commit하지 않는다. +3. abort/restore는 확인된 권위가 있을 때만 수행한다. +4. abort가 불확실하게 실패하면 증거를 보존한다(조용한 복원 금지). + +회귀 테스트 3종: 동일-구세대 후보, abort 실패, 진행 중 백업을 지우는 동시 +status 실행. 뒤 두 개는 #2789의 열린 스레드(T31/T32)와 같은 사안이다. + +### D5 — 릴레이 응답이 validator를 보존한다 + +위치: `080_phase6_hardening.md:496-503` (8.3 응답 규칙). + +응답 규칙이 "safe content type, cache control, ETag ... 만 보존"이라고 적어, +릴레이된 세션/부트스트랩/관리 응답에 validator가 살아남는 것을 허용한다. +D2와 같은 결함이 릴레이 경로에 한 번 더 있는 셈이다. + +수정: 릴레이된 세션/부트스트랩/관리 응답은 기본이 `Cache-Control: no-store`이고 +validator(ETag/Last-Modified)를 제거한다. 구현은 릴레이를 처음 갖는 wp5/p4에 +배정하고, p6에서 적대적 커버리지를 추가한다. ## 작업 순서 @@ -73,5 +104,10 @@ no-store가 싸다. 이 결정을 문서에 근거와 함께 기록한다. - `git range-diff origin/dev..bad162407 origin/dev..` 로 9커밋 보존 확인 (D1~D4 수정 커밋은 추가분). - 문서 전용이므로 로컬 테스트 대상 없음. exact-head CI 그린으로 판정. -- D1~D4의 구현 정합은 각각 wp3(D1), wp2(D2), wp5(D3), wp7(D4)에서 처리한다. - 이 단계는 계약만 고친다. +- D1~D5의 구현 정합은 각각 wp3(D1), wp2(D2), wp5(D3), wp7(D4), wp5(D5)에서 + 처리한다. 이 단계는 계약만 고친다. +- `003` 원장의 T1(미공개 보안 분석이 공개 devlog에 있음, P1)을 함께 처리한다. + `AGENTS.md` 보안 규정상 미수정 결함의 분석은 추적 디렉터리에 있으면 안 된다. + 해당 문단을 판정해 제거하거나, 이미 공개 diff로 드러난 사안임을 확인한다. +- #2771의 마크다운 린트 6건(MD018/MD022/테이블 파이프)과 미래 날짜 1건도 + 이 단계에서 닫는다. diff --git a/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md index 5d27c2fb26..e8651bdd20 100644 --- a/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md +++ b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md @@ -17,10 +17,24 @@ least-privilege GET /v1/catalog for remote Codex clients (#2979)` 이다. #2979는 이 스택이 설계한 `/v1/catalog`를 별도 PR로 먼저 랜딩시킨 것이다. -따라서 충돌 해소 원칙은 **dev를 이긴다**이다. p1의 카탈로그 델타 중 #2979가 -이미 제공하는 부분은 버리고, p1에만 있는 부분(프로토콜 메타데이터, 런타임 role, -`x-opencodex-key-id` 처리)만 얹는다. 반대로 하면 랜딩된 최소권한 계약을 -되돌리게 된다. +"dev wins"를 통째로 적용하면 안 된다(감사 A4). 두 구현은 의미가 갈린다: + +| 항목 | dev (#2979) | p1 | 채택 | +|------|-------------|----|------| +| 메서드 | GET + HEAD | GET only | **dev** — HEAD 제거는 랜딩된 기능 회귀 | +| `x-api-key` | 허용 | 거부 | **판단 필요** — 아래 | +| 크기 캡 | 라우트 한정 256 MiB | 32 MiB | **dev** — 랜딩된 지원 크기를 줄이지 않는다 | +| 초과 시 | 507 | 503 | **dev** | +| `x-opencodex-key-id` | 없음 | 있음 | **p1** — 고유 기여 | +| 프로토콜 메타데이터 | 없음 | 있음 | **p1** — 고유 기여 | +| 캐시 헤더 | — | ETag + private,no-cache | **둘 다 아님** — D2에 따라 `no-store`, validator 제거 | + +근거: dev 쪽 구현은 `src/server/index.ts:1073-1120`과 +`src/server/catalog-download.ts:18-29`에 있다. + +`x-api-key` 허용/거부는 의도적으로 판정한다. p1이 거부하는 것은 최소권한 +의도로 보이지만, dev가 이미 허용한 상태로 랜딩됐으므로 좁히는 것은 동작 회귀다. +좁히려면 별도 근거와 함께 PR 설명에 명시하고 테스트를 함께 바꾼다. 기본은 dev 유지. 해소 후 반드시 확인할 것: `/v1/catalog`의 최소권한 admission이 p1 델타에 의해 느슨해지지 않았는가. `tests/api-catalog-route.test.ts`가 이 계약을 들고 있다. @@ -35,9 +49,14 @@ key-id warn assertion` 이 해당 경로를 다룬다. 재스택 후 카탈로 ## release-version-line -리뷰가 지목한 `tests/release-version-line.test.ts:108` 실패는 스택 -`package.json`이 `2.34.0`이고 릴리스 태그 라인이 `2.40.0`이라서 난다. -재스택하면 dev 쪽 `2.40.0`으로 해소되어 자동 소멸한다. 테스트를 손대지 않는다. +`:108`은 equality 분기다(000 참조). 리베이스로 해소되지만 자동 소멸을 가정하지 +않는다 — 이 단계 head에서 `bun test tests/release-version-line.test.ts`를 +명시적으로 돌려 확인한다. 테스트를 손대지 않는다. + +## 미해결 스레드 + +T19 (#2772, P2): 확장된 readiness 응답을 `docs-site/src/content/docs/reference/cli/lifecycle.md`에 +문서화. `src/server/index.ts:1013`이 대상. ## privacy:scan diff --git a/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md index 45ccd7ce45..bf94d146e5 100644 --- a/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md +++ b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md @@ -29,15 +29,39 @@ opt-in 플래그로 이 금지를 뚫을 수 없어야 한다. HTTP는 "여기 H 그리고 loopback은 기존대로 허용되는 포지티브. `gui/tests/connect-pairing.test.ts`와 서버 쪽 remote-session 테스트에 건다. +## 이 단계가 소유하는 블로커 — 감사로 재배정됨 + +### gui/tests/api-auth-memory.test.ts:23 + +#2777(p3)에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 **p2**다 +(p3은 0건). 여기서 고친다. 재스택 후 실패를 재현해 어느 쪽 계약이 맞는지 +판정한다 — dev가 맞으면 스택 코드를 맞추고, 스택이 의도적으로 바꾼 것이면 +근거를 PR 설명에 적고 테스트를 함께 갱신한다. 테스트만 지우는 해소는 금지. + +### T20 (P1) — pairing 바디 무제한 버퍼링 + +`src/server/index.ts:1684`. 미인증 호출자가 `Content-Length`를 생략하거나 +chunked를 쓰면 `declaredLength`가 0이 되어 바디가 제한 없이 버퍼링된다. +미인증 DoS다. 선언 길이가 없을 때도 하드 캡을 적용하고 초과 시 거절한다. + +### T21 (P2) — 설정 문서화 + +`hub.managementPublicOrigin`, `remoteGui.allowedTailscaleUsers`, +`remoteGui.allowInsecure*`가 사용자 노출 설정인데 문서가 없다. +D1이 `allowInsecure*`의 의미를 바꾸므로 문서도 새 계약으로 쓴다. + ## draft 해제 -#2776은 draft이고 base가 `codex/remote-hub-p1`이다. `AGENTS.md`의 stacked child -규정상 이 base는 정당하다 — `enforce-target`은 열린 부모 head를 타깃하는 자식의 -wrong-base 게이트를 건너뛴다. 재스택 + CI 그린 후 draft를 해제한다. +#2776은 draft이고 base가 `codex/remote-hub-p1`이다. 이 base는 정당하다 — +`AGENTS.md:278-281`과 `enforce-pr-target.yml:533-557`이 열린 부모 head를 +타깃하는 자식의 wrong-base 게이트를 면제한다. 재스택 + CI 그린 후 draft를 +해제한다. 다만 draft 해제는 자동화 게이트만 여는 것이고 리뷰어의 +CHANGES_REQUESTED는 그대로다(감사 A3). ## 검증 - `git range-diff` 6커밋 보존. -- `bun test tests/server-auth.test.ts tests/config.test.ts tests/cli-registry.test.ts` - + `cd gui && bun test tests/connect-pairing.test.ts`. +- `bun test tests/server-auth.test.ts tests/config.test.ts tests/cli-registry.test.ts tests/release-version-line.test.ts` + + `cd gui && bun test tests/connect-pairing.test.ts tests/api-auth-memory.test.ts`. +- **이 단계 head가 초록이어야 p3을 그 위에 쌓는다.** - exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md index 9804ef5066..0bd2abf918 100644 --- a/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md +++ b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md @@ -10,25 +10,43 @@ dev와 겹치는 파일 18개 — 이 스택에서 CLI 표면 겹침이 가장 `src/types/config.ts` + 테스트 7(`cli-headless-parity`, `cli-start-journal-order`, `cli-status-json` 포함). -`cli-headless-parity`는 wp6에서도 문제를 일으키는 파일이다. 여기서 CLI 표면이 -늘어나므로, p3 재스택 시점에 이미 새 명령이 headless 선언에 들어가 있는지 -확인해두면 wp6의 부담이 준다. +`cli-headless-parity`는 wp5에서도 문제를 일으키는 파일이다. 여기서 CLI 표면이 +늘어나므로, p3 재스택 시점에 새 명령이 headless 선언에 들어가 있는지 확인해두면 +wp5의 부담이 준다. -## 블로커 — gui/tests/api-auth-memory.test.ts:23 +## gui/tests/api-auth-memory.test.ts — 여기가 아니다 -리뷰어 표현은 "현재 대시보드 auth-memory 경계를 보존하라"이다. 즉 스택이 -대시보드 인증 상태의 메모리 보관 계약을 바꿨고, dev 쪽 계약과 어긋났다. -재스택 후 실패를 재현해서 **어느 쪽 계약이 맞는지** 먼저 판정한다. dev가 -맞으면 스택 코드를 dev 계약에 맞추고, 스택이 의도적으로 바꾼 것이라면 -그 근거를 PR 설명에 적고 테스트를 함께 갱신한다. 테스트만 지우는 해소는 금지. +#2777에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 p2다(p3은 0건). +**wp3으로 재배정했다**(감사 A5). p3 재스택 시점에는 이미 고쳐져 있어야 한다. +여기서는 회귀하지 않았는지만 확인한다. + +## 이 단계가 소유하는 스레드 + +### T22 (P1) — 기존 Codex journal 재소유 + +`src/client/connect.ts:229`. `ocx start` 이후의 정상 상태, 즉 Codex가 이미 +로컬 OpenCodex 프록시를 통하도록 라우팅된 상태에서 `injectCodexConfig`가 +소유권을 잃는다. 연결 전에 기존 journal을 재소유해야 한다. + +### T23 (P2) — 응답 읽기 제한 + +`src/client/hub-client.ts:85`. 신뢰할 수 없는 `Content-Length`(chunked이거나 +고의로 잘못 보고된 `/readyz`, `/api/keys`)에 대해 버퍼링 전에 제한한다. +T20과 같은 계열이므로 같은 캡 정책을 쓴다. + +### T24 (P2) — connect 워크플로 문서화 + +`src/cli/help.ts:35`. stdin 전용 자격증명, 클라이언트 선택, HTTP 처리를 포함한 +사용자 노출 워크플로가 문서화되지 않았다. ## release-version-line -wp2와 동일 원인. 재스택으로 소멸. +wp2와 동일. 이 단계 head에서 명시적으로 확인한다. ## 검증 - `git range-diff` 11커밋 보존. -- `cd gui && bun test tests/api-auth-memory.test.ts` -- `bun test tests/cli-headless-parity.test.ts tests/cli-registry.test.ts tests/cli-status-json.test.ts` +- `cd gui && bun test tests/api-auth-memory.test.ts` (회귀 확인) +- `bun test tests/cli-headless-parity.test.ts tests/cli-registry.test.ts tests/cli-status-json.test.ts tests/release-version-line.test.ts` +- **이 단계 head가 초록이어야 p4를 그 위에 쌓는다.** - exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md index c64e324d3d..1ebc8167fa 100644 --- a/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md +++ b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md @@ -28,6 +28,45 @@ i18n 충돌은 기계적이다(양쪽이 서로 다른 키를 추가). 9개 로 테스트: 허용 메서드마다 릴레이 후 허브가 받은 Origin이 브라우저 원문과 같음을 확인하는 케이스. Origin 부재 시 요청이 거절되는 네거티브. +## D5 — 릴레이 응답의 validator 제거 + +릴레이를 처음 갖는 단계가 여기이므로 D5도 여기서 구현한다. 릴레이된 +세션/부트스트랩/관리 응답은 기본이 `Cache-Control: no-store`이고 ETag / +Last-Modified를 제거한다. p6에서 적대적 커버리지를 덧붙인다. + +## /api/machine/* 라우트 선언 — 여기가 소유 단계다 + +#2786(p5)에 보고됐지만 실측하면 `/api/machine/` 추가 라인이 p4에 **49건**, +p5에는 0건이다. 라우트를 도입한 건 p4다(감사 A5). + +`tests/cli-headless-parity.test.ts:287`은 "서버가 여는 라우트와 CLI가 선언한 +표면이 일치한다"를 주장한다. 7개 라우트를 열거하고 각각 이 단계에 필요한지 +판정한 뒤, 필요한 것은 명시 선언하고 불필요한 것은 제거한다. 테스트 예외를 +추가해 숨기는 방향은 금지. + +## 이 단계가 소유하는 스레드 + +### T25 (P1) — relay 트랜스포트가 동작하지 않는다 + +`src/client/machine-listener.ts:79`. 문서화된 `--management-transport relay`를 +고르면 `connectClient`가 여전히 throw한다. 문서에 있는 옵션이 죽어 있는 것이므로 +연결 경로를 새 리스너까지 잇는다. + +### T26 (P1) — supervised 런타임이 disconnect 후 재시작되지 않는다 + +`src/client/runtime.ts:27`. systemd나 WinSW로 뜬 런타임은 `OCX_SERVICE=1` 때문에 +해당 분기를 건너뛰어 재시작되지 않는다. + +### T27 / T28 (P2) — GUI + +`gui/src/App.tsx:222` disconnect 202 성공 시 `targets.connected` 갱신 누락. +`gui/src/App.tsx:376` pairing 완료 전 공유 페이지가 함께 마운트된다. + +### T2 (P2, #2771에서) — 연결된 GUI의 인증된 models 경로 + +`gui/src/pages/ApiKeys.tsx:154`가 `/v1/models`를 부르는데 허브는 그 경로를 +데이터플레인으로 처리한다. 인증된 경로를 제공한다. + ## draft 해제 wp3와 동일 근거. 재스택 + CI 그린 후 해제. @@ -35,6 +74,7 @@ wp3와 동일 근거. 재스택 + CI 그린 후 해제. ## 검증 - `git range-diff` 8커밋 보존. -- `bun test tests/usage-summary.test.ts tests/cli-start-journal-order.test.ts` +- `bun test tests/usage-summary.test.ts tests/cli-start-journal-order.test.ts tests/cli-headless-parity.test.ts tests/release-version-line.test.ts` - i18n 9개 로케일 키 존재 확인. +- **이 단계 head가 초록이어야 p5를 그 위에 쌓는다.** - exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md index 685563877e..5cd191de0b 100644 --- a/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md +++ b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md @@ -7,15 +7,11 @@ dev와 겹치는 파일 5개로 스택에서 가장 얕다. 그런데 블로커 리뷰어가 "인프라 노이즈가 아니라 제품 계약"이라고 못박은 항목들이다. -### tests/cli-headless-parity.test.ts:287 — 미선언 /api/machine/* 7개 +### tests/cli-headless-parity.test.ts:287 — 여기가 아니다 -p5가 머신 관리 라우트 7개를 추가했는데 CLI/headless 표면 선언에 등록하지 -않았다. 이 테스트는 "서버가 여는 라우트와 CLI가 선언한 표면이 일치한다"를 -주장한다. 해소는 둘 중 하나: 7개 라우트를 명시 선언하거나, 제거한다. -숨기는 방향(테스트 예외 추가)은 금지. - -먼저 7개가 무엇인지 열거하고, 각각이 이 단계에 필요한지 판정한다. -배포 단계가 실제로 쓰지 않는 라우트가 섞여 있으면 그건 제거가 정답이다. +#2786에 보고됐지만 `/api/machine/` 추가 라인은 p4에 49건, p5에는 0건이다. +라우트를 도입한 건 p4이므로 **wp5로 재배정했다**(감사 A5). 여기서는 p4가 +선언을 고친 뒤에도 이 단계에서 회귀하지 않는지만 확인한다. ### tests/update-stop-first.test.ts:225 — stop-first 계약 @@ -34,13 +30,32 @@ management ingress allowlist` 가 allowlist를 바꾸면서 기존 admission을 배포 가이드와 ingress 로깅에서 자격증명/호스트 식별자가 새는지 확인한다. `bun run privacy:scan`으로 재현하고 코드를 고친다. +## 이 단계가 소유하는 스레드 + +### T29 (P2) — hub role을 disconnected client state에서 배제 + +`src/client/state.ts:46`. `client` 블록이 없는 허브를 `disconnected`로 +분류하면 `connectClient()`가 그 상태 검사를 통과해버린다. + +### T30 (P2) — missing-config 부트스트랩 조건화 + +`src/client/state.ts:85`. `mutatePersistedConfig()`가 `missing`을 보고할 때 +뮤테이션 락을 얻기 전에 반환해서, 다른 첫 실행 명령과 경쟁한다. + +### T3 / T11 (#2771에서) + +관리 ingress에서 GUI health 엔드포인트 보존(`070_phase5_deploy.md:164`), +안정성 지적(`:300`). Tailscale Serve 배포에서 브라우저가 관리 리스너를 쓰므로 +health 경로가 살아 있어야 한다. + ## release-version-line -wp2와 동일 원인. 재스택으로 소멸. +wp2와 동일. 이 단계 head에서 명시적으로 확인한다. ## 검증 - `git range-diff` 8커밋 보존. -- `bun test tests/cli-headless-parity.test.ts tests/update-stop-first.test.ts tests/loopback-listener-admission.test.ts tests/service.test.ts` +- `bun test tests/cli-headless-parity.test.ts tests/update-stop-first.test.ts tests/loopback-listener-admission.test.ts tests/service.test.ts tests/release-version-line.test.ts` - `bun run privacy:scan` +- **이 단계 head가 초록이어야 p6을 그 위에 쌓는다.** - exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md index cc1cf2e0fd..624a298963 100644 --- a/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md +++ b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md @@ -26,12 +26,36 @@ docs-site 겹침이 큰 덩어리인데 대부분 로케일 문서라 기계적 문제: `pendingOperation` 저장 직후 크래시 시 두 파일 모두 옛 키를 담고 둘 다 probe에 성공한다 → 로테이션이 조용히 유실된다. -수정: 완료 판정을 "probe 성공 쌍"이 아니라 세대 구별 증거에 건다. -어느 파일이 어느 세대인지 판별할 수 없으면 미완료로 보고 재개한다. -즉 판정이 애매하면 안전한 쪽(재개)으로 넘어져야 한다. +수정(감사 A7): 010 D5 계약을 그대로 구현한다. -테스트: pendingOperation 저장 직후 크래시를 흉내낸 상태(양쪽 옛 키)에서 -시작 시 로테이션이 재개되는지 확인하는 레드-퍼스트 케이스. +1. probe 이전에 두 후보의 identity를 비교한다. +2. 동일하면 교체 이전 상태다 — commit하지 않는다. +3. abort/restore는 확인된 권위가 있을 때만. +4. abort가 불확실하게 실패하면 증거를 보존한다. + +레드-퍼스트 회귀 3종: 동일-구세대 후보, abort 실패, 진행 중 백업을 지우는 +동시 status 실행. + +## 이 단계가 소유하는 스레드 + +### T31 (P1) — abort 실패 시 토큰 identity 보존 + +`src/client/connect.ts:304`. 새 토큰 설치 후 abort 요청이 일시적으로 실패하면 +현재 코드가 잘못된 세대를 복원한다. D4 계약의 3/4항이 바로 이 사안이다. + +### T32 (P2) — status가 진행 중 백업을 삭제 + +`src/client/state.ts:95`. `rotateConnectedClientKey`가 `/api/keys/rotate`를 +기다리는 동안 `ocx connect status`가 돌면 in-flight 백업이 지워진다. + +### T33 (P2) — 릴레이 오류를 과대 응답 노출 전에 반환 + +`src/client/hub-relay.ts:282`. `Content-Length` 없는 chunked 업스트림 응답 처리. + +### D5 적대적 커버리지 + +wp5가 구현한 릴레이 no-store / validator 제거에 대해 이 단계에서 적대적 +테스트를 추가한다. ## 리뷰어가 예고한 최종 보안 심사 항목 diff --git a/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md index 2a7ad0f6dc..3e06563002 100644 --- a/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md +++ b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md @@ -19,10 +19,17 @@ 재스택 과정에서 GitHub가 base를 자동 변경하는 경우가 있으므로 푸시 후 매번 확인한다. base가 어긋나면 각 PR의 diff가 상류 델타를 삼켜서 리뷰가 불가능해진다. -## 계보 확인 +## 계보 확인 — 부모 엣지까지 -`git merge-base --is-ancestor origin/dev origin/codex/remote-hub-` 가 -7단계 전부에서 참이어야 한다. 하나라도 거짓이면 그 단계는 재스택이 안 된 것이다. +`origin/dev`가 조상인지만 보면 부족하다(감사 A9): 부모를 건너뛰고 dev 위로 +직접 리베이스된 자식도 그 검사를 통과한다. 각 **엣지**를 확인한다: + +``` +git merge-base --is-ancestor origin/ origin/ +``` + +6개 엣지 전부에 대해 실행하고 양쪽 OID를 기록한다. 그리고 각 PR의 base ref가 +같은 부모 브랜치를 가리키는지 대조한다. ## draft 해제 @@ -34,11 +41,39 @@ CHANGES_REQUESTED는 새 head에서 자동 해제되지 않으므로, 무엇이 어떻게 해소됐는지 파일:줄로 지목해야 재리뷰가 가능하다. +## 리뷰 스레드 마감 + +`003` 원장의 33건이 전부 처리돼야 한다. 처리 = 수정하고 resolve, 또는 근거를 +갖춘 반박을 남기고 resolve. 침묵한 채로 남은 스레드가 있으면 종료선 미달이다. +P1 6건(T1, T20, T22, T25, T26, T31)은 반박이 아니라 수정으로만 닫는다. + ## 종료선 -DONE = 7단계 재스택 푸시 완료, B1~B7 해소, exact-head CI 그린, base 체인 정합, -draft 해제. **머지는 하지 않는다** — 사용자 요청은 "머지 가능한 정도까지 세팅"이다. +DONE = 다음 전부: + +1. 7단계 재스택 푸시 완료, 각 단계 range-diff 커밋 보존. +2. B1~B7 및 D1~D5 해소. +3. 리뷰 스레드 33건 처리 완료. +4. **각 단계가 자기 head에서** exact-head CI 그린. +5. base 체인 정합(6개 엣지 + PR base ref 대조). +6. draft 해제(#2776/#2781/#2789). +7. 각 PR에 재리뷰 요청. + +**머지는 하지 않는다** — 사용자 요청은 "머지 가능한 정도까지 세팅"이다. + +### 승인은 우리 손 밖이다 + +감사 A1: `MAINTAINERS.md:57-61`은 비저자 메인테이너 승인과 보안 리뷰를 +요구하고, Ingwannu가 유일한 비저자 메인테이너이며 지금 7건 전부에 +CHANGES_REQUESTED를 걸어두었다. CI가 초록이어도 이 상태로는 머지 버튼이 +열리지 않는다. + +우리 종료선은 **재리뷰 요청 가능 상태**까지다. 그 뒤 승인이 오지 않는 것은 +외부 의존이며, 그 지점에 도달하면 BLOCKED으로 증거와 함께 보고한다. +"CI 그린이니 머지 가능"이라고 보고하지 않는다. + +## 부분 실패 처리 -한 단계가 막히면 그 단계만 BLOCKED으로 증거와 함께 보고하고 상류는 계속한다. -단, 스택이므로 하류가 막히면 상류는 그 위에 쌓을 수 없다. 그 경우 막힌 단계를 -건너뛴 재스택은 하지 않고 BLOCKED으로 보고한다. +한 단계가 막히면 그 단계만 BLOCKED으로 증거와 함께 보고한다. 스택이므로 +하류가 막히면 상류는 그 위에 쌓을 수 없다. 막힌 단계를 건너뛴 재스택은 +하지 않는다. From d18aa34927c5e28e81d47fbf12c051956c89fd6f Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 16:36:39 +0900 Subject: [PATCH 03/34] docs(devlog): fix markdown lint in the restack unit (MD018/MD060/MD040/MD004/MD036) --- .../260901_remote_hub_restack/000_research.md | 12 ++++++------ .../002_audit_r1_synthesis.md | 2 +- .../003_review_thread_ledger.md | 14 +++++++------- .../020_wp2_p1_protocol_catalog.md | 4 ++-- .../030_wp3_p2_remote_session.md | 6 +++--- .../040_wp4_p3_connect.md | 2 +- .../050_wp5_p4_two_plane.md | 2 +- .../260901_remote_hub_restack/060_wp6_p5_deploy.md | 2 +- .../070_wp7_p6_hardening.md | 2 +- .../080_wp8_stack_integrity.md | 6 +++--- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/devlog/_plan/260901_remote_hub_restack/000_research.md b/devlog/_plan/260901_remote_hub_restack/000_research.md index ef15d80873..faf6298725 100644 --- a/devlog/_plan/260901_remote_hub_restack/000_research.md +++ b/devlog/_plan/260901_remote_hub_restack/000_research.md @@ -7,7 +7,7 @@ 7단계 스택. 베이스만 `dev`를 향하고 나머지는 직전 단계의 head 브랜치를 향한다. | PR | 브랜치 | base | 커밋 | 파일 | draft | -|----|--------|------|------|------|-------| +| --- | --- | --- | --- | --- | --- | | #2771 | codex/remote-hub-design | dev | 9 | 12 | no | | #2772 | codex/remote-hub-p1 | design | 5 | 14 | no | | #2776 | codex/remote-hub-p2 | p1 | 6 | 32 | yes | @@ -25,7 +25,7 @@ design 단계는 문서 전용이라 충돌 없이 통과한다(`f17605021`). p1부터 걸린다. | 단계 | 단계 파일 | dev와 겹치는 파일 | -|------|-----------|-------------------| +| --- | --- | --- | | design | 12 | 0 | | p1 | 14 | 12 | | p2 | 32 | 15 | @@ -55,7 +55,7 @@ clients (#2979)`가 마지막으로 건드렸다. 이건 우연이 아니다 — 리뷰 7건을 원인별로 다시 묶으면 세 종류뿐이다. -**(1) stale 아티팩트 — 재스택이 곧 해소** +### (1) stale 아티팩트 — 재스택이 곧 해소 `tests/release-version-line.test.ts:108` 실패가 #2772/#2777/#2786에 공통으로 걸려 있다. 정확히 말하면 `:108`은 "뒤처짐" 분기가 아니라 **동일(equality)** @@ -69,9 +69,9 @@ clients (#2979)`가 마지막으로 건드렸다. 이건 우연이 아니다 — 재발한다. 리베이스된 head마다 `bun test tests/release-version-line.test.ts`를 포커스드로 돌려 확인한다. **게이트는 건드리지 않는다.** -**(2) 구조적 보류 — 자동화는 통과, 사람 리뷰는 별개** +### (2) 구조적 보류 — 자동화는 통과, 사람 리뷰는 별개 -#2776/#2781/#2789는 "중간 스택 head라 최종 승인 불가"라는 보류다. +`#2776`/#2781/#2789는 "중간 스택 head라 최종 승인 불가"라는 보류다. `AGENTS.md:278-281`과 `.github/workflows/enforce-pr-target.yml:533-557`은 열린 부모 head를 타깃하는 stacked child에 대해 wrong-base 게이트를 실제로 면제한다. 저자가 `lidge-jun`(push 권한)이라 기여자 readiness 체크리스트 @@ -83,7 +83,7 @@ draft 해제로도 CI 그린으로도 해제되지 않는다. `MAINTAINERS.md:57 메인테이너다. 우리가 도달할 수 있는 종료선은 **재리뷰 요청 가능 상태**이며, 승인 자체는 외부 의존이다. -**(3) 실질 결함 — 코드/문서 수정 필요** +### (3) 실질 결함 — 코드/문서 수정 필요 - #2771 문서 계약 4건 (아래 010). - `gui/tests/api-auth-memory.test.ts:23` — #2777에 보고됐지만 **소유 단계는 p2**다. diff --git a/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md b/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md index 838714593a..6aa0c41b5e 100644 --- a/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md +++ b/devlog/_plan/260901_remote_hub_restack/002_audit_r1_synthesis.md @@ -17,7 +17,7 @@ ## A2 (High) — 미해결 리뷰 스레드 33개 누락 · 수용, 가장 큰 누락 로드맵이 리뷰 본문만 읽고 인라인 스레드를 안 봤다. 실측 결과: -#2771: 18, #2772: 1, #2776: 2, #2777: 3, #2781: 4, #2786: 2, #2789: 3. +`#2771`: 18, #2772: 1, #2776: 2, #2777: 3, #2781: 4, #2786: 2, #2789: 3. P1 등급이 6건 섞여 있다. 별도 원장 `003_review_thread_ledger.md`로 분리했다. ## A3 (High) — stacked 면제의 효력 과대 해석 · 수용 diff --git a/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md index 04cf3ef799..597af9605f 100644 --- a/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md @@ -9,7 +9,7 @@ 문서 계약 지적이다. 실질 건만 추린다. | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T1 | 000_research.md:22 | **P1** | 미공개 보안 분석이 추적되는 공개 devlog에 있다 | wp1 | | T2 | 060_phase4_two_plane.md:348 | P2 | 연결된 GUI에 인증된 models 경로 필요(`/v1/models`가 데이터플레인으로 감) | wp5 | | T3 | 070_phase5_deploy.md:164 | P2 | 관리 ingress에서 GUI health 엔드포인트 보존 | wp6 | @@ -31,13 +31,13 @@ ## #2772 p1 — 1건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T19 | src/server/index.ts:1013 | P2 | 확장된 readiness 응답을 `docs-site/.../cli/lifecycle.md`에 문서화 | wp2 | ## #2776 p2 — 2건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T20 | src/server/index.ts:1684 | **P1** | pairing 바디를 버퍼링 전에 제한. `Content-Length` 없거나 chunked면 `declaredLength`가 0이 되어 미인증 호출자가 무제한 버퍼링 유발 | wp3 | | T21 | src/types/config.ts:251 | P2 | `hub.managementPublicOrigin`, `remoteGui.allowedTailscaleUsers`, `remoteGui.allowInsecure*` 문서화 | wp3 | @@ -46,7 +46,7 @@ T20은 미인증 DoS다. D1과 같은 층에 있으므로 wp3에서 함께 닫 ## #2777 p3 — 3건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T22 | src/client/connect.ts:229 | **P1** | 연결 전 기존 Codex journal 재소유 필요. `ocx start` 후 정상 상태에서 `injectCodexConfig`가 소유권을 잃는다 | wp4 | | T23 | src/client/hub-client.ts:85 | P2 | 신뢰할 수 없는 `Content-Length`에 대해 응답 읽기 제한 | wp4 | | T24 | src/cli/help.ts:35 | P2 | connect/disconnect 워크플로 문서화 | wp4 | @@ -54,7 +54,7 @@ T20은 미인증 DoS다. D1과 같은 층에 있으므로 wp3에서 함께 닫 ## #2781 p4 — 4건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T25 | src/client/machine-listener.ts:79 | **P1** | `--management-transport relay` 선택 시 `connectClient`가 여전히 throw — 문서화된 옵션이 동작하지 않음 | wp5 | | T26 | src/client/runtime.ts:27 | **P1** | systemd/WinSW로 뜬 런타임이 disconnect 후 재시작되지 않음(`OCX_SERVICE=1`이 분기를 건너뜀) | wp5 | | T27 | gui/src/App.tsx:222 | P2 | disconnect 202 성공 시 targets 갱신 누락 | wp5 | @@ -63,14 +63,14 @@ T20은 미인증 DoS다. D1과 같은 층에 있으므로 wp3에서 함께 닫 ## #2786 p5 — 2건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T29 | src/client/state.ts:46 | P2 | hub role을 disconnected client state에서 배제 | wp6 | | T30 | src/client/state.ts:85 | P2 | missing-config 부트스트랩 조건화(락 획득 전 반환으로 경쟁) | wp6 | ## #2789 p6 — 3건 | # | 위치 | 등급 | 요지 | 배정 | -|---|------|------|------|------| +| --- | --- | --- | --- | --- | | T31 | src/client/connect.ts:304 | **P1** | abort 실패 시 토큰 identity 보존. 새 토큰 설치 후 abort가 일시 실패하면 복원이 잘못된 세대를 남긴다 | wp7 | | T32 | src/client/state.ts:95 | P2 | `ocx connect status`가 진행 중인 로테이션 백업을 삭제 | wp7 | | T33 | src/client/hub-relay.ts:282 | P2 | 릴레이 오류를 과대 응답 노출 전에 반환 | wp7 | diff --git a/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md index e8651bdd20..4ac97869f1 100644 --- a/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md +++ b/devlog/_plan/260901_remote_hub_restack/020_wp2_p1_protocol_catalog.md @@ -15,12 +15,12 @@ 세 파일의 dev 쪽 마지막 변경은 전부 `f6367639c feat(server): add least-privilege GET /v1/catalog for remote Codex clients (#2979)` 이다. -#2979는 이 스택이 설계한 `/v1/catalog`를 별도 PR로 먼저 랜딩시킨 것이다. +`#2979`는 이 스택이 설계한 `/v1/catalog`를 별도 PR로 먼저 랜딩시킨 것이다. "dev wins"를 통째로 적용하면 안 된다(감사 A4). 두 구현은 의미가 갈린다: | 항목 | dev (#2979) | p1 | 채택 | -|------|-------------|----|------| +| --- | --- | --- | --- | | 메서드 | GET + HEAD | GET only | **dev** — HEAD 제거는 랜딩된 기능 회귀 | | `x-api-key` | 허용 | 거부 | **판단 필요** — 아래 | | 크기 캡 | 라우트 한정 256 MiB | 32 MiB | **dev** — 랜딩된 지원 크기를 줄이지 않는다 | diff --git a/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md index bf94d146e5..cbdfe763fd 100644 --- a/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md +++ b/devlog/_plan/260901_remote_hub_restack/030_wp3_p2_remote_session.md @@ -33,7 +33,7 @@ opt-in 플래그로 이 금지를 뚫을 수 없어야 한다. HTTP는 "여기 H ### gui/tests/api-auth-memory.test.ts:23 -#2777(p3)에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 **p2**다 +`#2777`(p3)에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 **p2**다 (p3은 0건). 여기서 고친다. 재스택 후 실패를 재현해 어느 쪽 계약이 맞는지 판정한다 — dev가 맞으면 스택 코드를 맞추고, 스택이 의도적으로 바꾼 것이면 근거를 PR 설명에 적고 테스트를 함께 갱신한다. 테스트만 지우는 해소는 금지. @@ -52,7 +52,7 @@ D1이 `allowInsecure*`의 의미를 바꾸므로 문서도 새 계약으로 쓴 ## draft 해제 -#2776은 draft이고 base가 `codex/remote-hub-p1`이다. 이 base는 정당하다 — +`#2776`은 draft이고 base가 `codex/remote-hub-p1`이다. 이 base는 정당하다 — `AGENTS.md:278-281`과 `enforce-pr-target.yml:533-557`이 열린 부모 head를 타깃하는 자식의 wrong-base 게이트를 면제한다. 재스택 + CI 그린 후 draft를 해제한다. 다만 draft 해제는 자동화 게이트만 여는 것이고 리뷰어의 @@ -62,6 +62,6 @@ CHANGES_REQUESTED는 그대로다(감사 A3). - `git range-diff` 6커밋 보존. - `bun test tests/server-auth.test.ts tests/config.test.ts tests/cli-registry.test.ts tests/release-version-line.test.ts` - + `cd gui && bun test tests/connect-pairing.test.ts tests/api-auth-memory.test.ts`. +- `cd gui && bun test tests/connect-pairing.test.ts tests/api-auth-memory.test.ts`. - **이 단계 head가 초록이어야 p3을 그 위에 쌓는다.** - exact-head CI. diff --git a/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md index 0bd2abf918..8648b80414 100644 --- a/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md +++ b/devlog/_plan/260901_remote_hub_restack/040_wp4_p3_connect.md @@ -16,7 +16,7 @@ wp5의 부담이 준다. ## gui/tests/api-auth-memory.test.ts — 여기가 아니다 -#2777에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 p2다(p3은 0건). +`#2777`에 보고됐지만 실측 결과 이 파일을 처음 건드리는 단계는 p2다(p3은 0건). **wp3으로 재배정했다**(감사 A5). p3 재스택 시점에는 이미 고쳐져 있어야 한다. 여기서는 회귀하지 않았는지만 확인한다. diff --git a/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md index 1ebc8167fa..22bda8d278 100644 --- a/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md +++ b/devlog/_plan/260901_remote_hub_restack/050_wp5_p4_two_plane.md @@ -36,7 +36,7 @@ Last-Modified를 제거한다. p6에서 적대적 커버리지를 덧붙인다. ## /api/machine/* 라우트 선언 — 여기가 소유 단계다 -#2786(p5)에 보고됐지만 실측하면 `/api/machine/` 추가 라인이 p4에 **49건**, +`#2786`(p5)에 보고됐지만 실측하면 `/api/machine/` 추가 라인이 p4에 **49건**, p5에는 0건이다. 라우트를 도입한 건 p4다(감사 A5). `tests/cli-headless-parity.test.ts:287`은 "서버가 여는 라우트와 CLI가 선언한 diff --git a/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md index 5cd191de0b..3a14f5d8f0 100644 --- a/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md +++ b/devlog/_plan/260901_remote_hub_restack/060_wp6_p5_deploy.md @@ -9,7 +9,7 @@ dev와 겹치는 파일 5개로 스택에서 가장 얕다. 그런데 블로커 ### tests/cli-headless-parity.test.ts:287 — 여기가 아니다 -#2786에 보고됐지만 `/api/machine/` 추가 라인은 p4에 49건, p5에는 0건이다. +`#2786`에 보고됐지만 `/api/machine/` 추가 라인은 p4에 49건, p5에는 0건이다. 라우트를 도입한 건 p4이므로 **wp5로 재배정했다**(감사 A5). 여기서는 p4가 선언을 고친 뒤에도 이 단계에서 회귀하지 않는지만 확인한다. diff --git a/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md index 624a298963..caf031d053 100644 --- a/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md +++ b/devlog/_plan/260901_remote_hub_restack/070_wp7_p6_hardening.md @@ -59,7 +59,7 @@ wp5가 구현한 릴레이 no-store / validator 제거에 대해 이 단계에 ## 리뷰어가 예고한 최종 보안 심사 항목 -#2789 코멘트가 재리뷰 시 볼 항목을 나열했다. 재스택 시 이 목록을 체크리스트로 +`#2789` 코멘트가 재리뷰 시 볼 항목을 나열했다. 재스택 시 이 목록을 체크리스트로 쓴다: 로테이션 크래시 복구, 토큰 백업 소유권/정리, 일회성 시크릿 노출, 세션 무효화, pairing 레이트 리밋, 릴레이 SSRF/헤더 스트리핑, 취소. diff --git a/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md index 3e06563002..968646f388 100644 --- a/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md +++ b/devlog/_plan/260901_remote_hub_restack/080_wp8_stack_integrity.md @@ -7,7 +7,7 @@ 각 PR의 base가 직전 단계 head 브랜치를 정확히 가리켜야 한다. | PR | base여야 하는 것 | -|----|------------------| +| --- | --- | | #2771 | dev | | #2772 | codex/remote-hub-design | | #2776 | codex/remote-hub-p1 | @@ -24,7 +24,7 @@ `origin/dev`가 조상인지만 보면 부족하다(감사 A9): 부모를 건너뛰고 dev 위로 직접 리베이스된 자식도 그 검사를 통과한다. 각 **엣지**를 확인한다: -``` +```sh git merge-base --is-ancestor origin/ origin/ ``` @@ -33,7 +33,7 @@ git merge-base --is-ancestor origin/ origin/ ## draft 해제 -#2776 / #2781 / #2789. CI 그린 확인 후에만. +`#2776` / #2781 / #2789. CI 그린 확인 후에만. ## PR 설명 갱신 From 6dd5820a198e954dee029f21d8621eb68d5d66d5 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 16:56:46 +0900 Subject: [PATCH 04/34] =?UTF-8?q?docs(devlog):=20record=20wp1=20outcome=20?= =?UTF-8?q?=E2=80=94=20design=20rebased,=20six=20contract=20defects=20clos?= =?UTF-8?q?ed=20across=20two=20review=20rounds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../011_wp1_outcome.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/011_wp1_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/011_wp1_outcome.md b/devlog/_plan/260901_remote_hub_restack/011_wp1_outcome.md new file mode 100644 index 0000000000..db0b600b78 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/011_wp1_outcome.md @@ -0,0 +1,77 @@ +# wp1 결과 — design(#2771) 재스택 + 계약 결함 6건 + +브랜치 `codex/remote-hub-design`: `bad162407` → `36992baa9`. + +## 재스택 + +`origin/dev@15b0f701e` 위로 `rebase --onto`. 충돌 0건. +`range-diff`로 원본 9커밋이 전부 `=`로 보존됨을 확인했다. 탈락한 5커밋은 +이미 dev에 랜딩된 무관 커밋이라 자연 소멸한 것이다. authorship 보존, 오염 커밋 +(`opencodex.invalid`) 0건, `devlog/` 외 파일 미변경. + +## 커밋 3개 + +| 커밋 | 내용 | +| --- | --- | +| `dfae1da61` | D1~D5 + T1 1차 수정 | +| `45951cebd` | D1 잔여 제거(010/050/070) | +| `36992baa9` | 리뷰 지적 2~5번 수정 | + +## 리뷰 라운드 + +읽기전용 적대적 리뷰어(gpt-5.6-sol high)가 `dfae1da61`을 심사해 **FAIL**, +지적 5건 + CLOSED 2건을 냈다. 판정과 처리: + +**1번 D1 미완 (HIGH) — 리뷰 시점 이전에 이미 수정됨.** +리뷰어가 `dfae1da61` 블롭을 봤는데, 그 시점 이후 `45951cebd`로 닫혀 있었다. +지적 자체는 정확했다: 040에서만 제거하고 010/050/070에 계약이 살아 있었다. +특히 050의 클라이언트 `--allow-insecure-http`는 서버가 거부하는 경로를 +클라이언트가 제공하는 자기모순이었다. + +**2번 D2가 하위 단계에 미반영 (HIGH) — 수용.** +030만 고치고 050/080의 클라이언트를 안 고쳤다. 서버는 validator를 안 주는데 +클라이언트는 ETag를 저장하고 `If-None-Match`를 보내고 304를 처리하도록 +명세돼 있었다. Phase 3을 따라 구현하면 Phase 1이 지운 것을 그대로 되살린다. +클라이언트를 무조건 페치로 바꾸고, 요청하지도 않은 304는 캐시 히트가 아니라 +프로토콜 오류로 규정했다. + +**3번 D3 과잉 수정 (HIGH) — 수용.** +"never omit it"이 040 §5.2의 안전 읽기 허용(Origin 없는 GET/HEAD)과 +충돌했다. 규칙을 둘로 분리했다: *전달*은 브라우저가 보낸 값이 있으면 항상 +원문 그대로, *요구*는 허브 predicate가 결정. 릴레이는 값을 지어내야 하는 +경우에만 거절한다. 소유 테스트 행도 메서드별 + Origin 부재 양 갈래로 확장했다. + +**4번 D4 실행 불가 (HIGH) — 수용, 가장 중요한 지적.** +두 문제가 있었다. 첫째, 새 규칙을 쓰면서 세 문단 위의 옛 "both accepted → +commit" 규칙과 활성화 매트릭스 행을 안 지워서 문서가 자기모순이었다. +둘째, "처음부터 재개"가 불가능하다 — 시크릿은 한 번만 반환되고, 재시작은 +`already-pending`으로 막히며, startup/status에는 관리 권한이 없다. +옛 텍스트를 교체하고, 복구는 증거를 보존한 채 **정지**하며 재개는 전이 권한을 +가진 다음 `ocx connect rotate`가 `rotationId` abort를 확인한 뒤 수행하도록 +상태 기계를 다시 썼다. + +**5번 부기 오류 (MEDIUM) — 수용.** +중복 `P2-A11`과, 검증 섹션 뒤 표 바깥에 붙은 `P6-A20..A22`. 각각 `P2-A21` +재번호와 활성화 매트릭스 편입으로 처리하고 낡은 행을 교체했다. + +**6번 D5 — CLOSED.** 리뷰어가 계약이 실제로 닫혔다고 확인. + +**7번 T1 — CLOSED.** 리프레이밍이 타당하다고 확인했다. 근거를 실물로 검증: +`src/server/management-auth.ts:245-252`가 원격 세션 발급을 거부하고, +`sidebar-routes.ts:41-49`/`codex-prompt-routes.ts:299-305`가 `gui-session`을 +요구하며, 공개 문서 `web-dashboard.md:24-35`가 이미 이 경계를 설명한다. +즉 이미 공개된 fail-closed 제약이지 미공개 취약점이 아니다. 리뷰어는 유닛 +나머지에서도 미수정 취약점 사전공개 텍스트를 찾지 못했다. + +## 검증 + +- 중복 acceptance ID 0건(P4-A4b/A4c는 접미사가 붙은 별개 ID). +- D1 활성 참조 0건 — 남은 언급은 전부 "제거했다" 서술. +- markdownlint 회귀 0건(8개 문서 before/after 동일). +- 푸시 후 `origin/dev`가 `origin/codex/remote-hub-design`의 조상임을 확인. + +## 남은 것 + +#2771의 미해결 스레드 18건 중 마크다운 린트 6건과 T2/T3/T7/T10/T11은 +아직 열려 있다. D1~D5에 해당하는 T4/T5/T6/T8/T9와 T1은 이 커밋들로 닫혔다. +PR 설명 갱신과 스레드 resolve는 wp8에서 일괄 처리한다. From 208dc48b9469b74403f8a4c506f909827a8005d3 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:11:53 +0900 Subject: [PATCH 05/34] =?UTF-8?q?docs(devlog):=20record=20wp2=20outcome=20?= =?UTF-8?q?=E2=80=94=20p1=20rebased,=20catalog=20contract=20reconciled=20w?= =?UTF-8?q?ith=20the=20landed=20#2979?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../021_wp2_outcome.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md b/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md new file mode 100644 index 0000000000..47214d11b0 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md @@ -0,0 +1,64 @@ +# wp2 결과 — p1(#2772) 재스택 + 카탈로그 계약 재조정 + +브랜치 `codex/remote-hub-p1`: `c10ef21a9` → `07d7f1006`. +베이스는 재스택된 `codex/remote-hub-design@36992baa9`. + +## 충돌과 해소 + +예측대로 `4fa130bf6`에서 3파일이 충돌했고, 이후 두 커밋에서도 테스트 파일이 +걸렸다. 원인은 020이 적은 그대로다: #2979(`f6367639c`)가 이 단계가 설계한 +`/v1/catalog`를 먼저 랜딩시켰다. + +**다만 020의 "p1 고유 기여" 판정은 절반이 틀렸다.** 초기 조사에서 dev의 +`src/server/index.ts`에 `withRemoteCatalogKeyId`와 프로토콜 메타데이터가 +보이길래 "dev가 이미 갖고 있다"고 적었는데, 그건 이전 리베이스 시도가 남긴 +작업 트리 잔재였다. `git show origin/dev:src/server/index.ts`로 확인하니 +dev에는 그 헬퍼가 **아예 없었다**. p1의 key-id 에코는 실재하는 고유 기여였고, +그걸 버렸다면 다중 키 운영자의 카탈로그 읽기 귀속이 사라졌을 것이다. + +교훈: 작업 트리의 grep은 브랜치의 내용이 아니다. 리베이스 중에는 +`git show :`로 확인해야 한다. + +## 최종 병합 결정 + +| 항목 | dev(#2979) | p1 | 채택 | 근거 | +| --- | --- | --- | --- | --- | +| 메서드 | GET+HEAD | GET only | dev | 랜딩된 기능 회귀 금지 | +| 크기 캡 | 256 MiB / 507 | 32 MiB / 503 | dev | 2000모델≈92MB, 32MiB는 유효 입력 거부 | +| malformed | 404 | 500 | dev | "파일 손상"과 "카탈로그 없음"을 구별시키지 않음 | +| `x-api-key` | 허용 | 거부 | dev | 상류로 자격증명 전달 없음 → 추가 권한 없음. 거부하면 유효한 Anthropic-SDK 클라이언트가 401 | +| `x-opencodex-key-id` | 없음(죽은 코드) | 있음 | **p1** | 실재하는 고유 기여, 라우트에 배선 | +| 캐시 헤더 | private,no-cache + ETag | ETag + no-cache | **둘 다 아님** | D2: `no-store`, validator 없음 | + +`AUTH_MATRIX`에 `/v1/catalog` 행이 둘 생겼고 `xApiKey`가 정반대였다. +행렬이 자기모순이라 라이브 서버 검증이 어느 행을 먼저 읽느냐로 갈렸다. +p1 행을 제거했다. + +## 테스트 조정 + +p1이 자기 구현에 맞춰 쓴 단언들을 dev+D2 계약으로 다시 썼다. 지운 게 아니라 +뒤집었고, 각각 왜 반대가 됐는지 주석으로 남겼다. + +- `api-catalog-route`: malformed→404, `no-store`/ETag 없음, HEAD 동일, + 조건부 요청이 200을 받는다(관리 라우트 ETag를 흉내내도). +- `server-auth`: 304 테스트를 "어떤 조건부 요청도 304를 끌어낼 수 없다"로 반전. + 사라진 `catalogDataPlaneResponse` API를 쓰던 캡 테스트는 제거(dev의 + `api-catalog-route`가 같은 경계를 이미 커버한다). ETag 스펠링을 dev의 hex로. +- `api-key-attribution`: dev 쪽 주석 있는 버전 채택. + +## 검증 + +- `bun run typecheck` 통과. +- 포커스드 7파일 **412 pass / 0 fail** + (server-auth, api-catalog-route, api-key-attribution, config, + proxy-liveness, release-version-line, server-live). +- `bun run privacy:scan` 통과 — 리뷰가 보고한 privacy 실패는 상속된 + staleness였고 재스택으로 소멸했다. +- `release-version-line` 통과 — 020의 예측대로 `package.json`이 dev의 + 2.40.0으로 해소됐다. +- 부수 확인: `server-auth`의 websocket refresh flake도 함께 사라졌다. + +## 남은 것 + +T19(확장된 readiness 응답을 `docs-site/.../cli/lifecycle.md`에 문서화)는 +아직 열려 있다. wp8에서 처리한다. From 29149338c9da8f7d1a57fad73a33f2d351668654 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:12:33 +0900 Subject: [PATCH 06/34] docs(devlog): record the wp2 commit map and why the fixups are a separate commit --- .../260901_remote_hub_restack/021_wp2_outcome.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md b/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md index 47214d11b0..db9fc20e47 100644 --- a/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/021_wp2_outcome.md @@ -62,3 +62,18 @@ p1이 자기 구현에 맞춰 쓴 단언들을 dev+D2 계약으로 다시 썼다 T19(확장된 readiness 응답을 `docs-site/.../cli/lifecycle.md`에 문서화)는 아직 열려 있다. wp8에서 처리한다. + +## 커밋 + +| 커밋 | 내용 | +| --- | --- | +| `733d0e62b` | feat(remote): add protocol metadata and runtime role (원본 보존) | +| `f65484844` | feat(remote): serve authenticated catalog snapshots (원본 보존) | +| `b24a15a22` | fix(remote): derive management origin from request host (원본 보존) | +| `0d81baffa` | test(remote): cover phase one protocol and catalog contract (원본 보존) | +| `58ab13df0` | fix(remote): type catalog bytes over ArrayBuffer (원본 보존) | +| `07d7f1006` | fix(remote): reconcile the phase-one catalog contract with the landed /v1/catalog (신규) | + +원본 5커밋은 authorship과 메시지가 보존됐다. 충돌 해소로 내용이 바뀐 부분은 +커밋을 다시 쓰지 않고 마지막에 조정 커밋 하나로 모았다 — 원저자의 커밋을 +내가 편집한 것처럼 보이게 만들지 않기 위해서다. From 1b9574afa5d172bfef43c686f454364cf95832d4 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:21:07 +0900 Subject: [PATCH 07/34] =?UTF-8?q?docs(devlog):=20record=20wp3=20outcome=20?= =?UTF-8?q?=E2=80=94=20p2=20rebased,=20plaintext=20pairing=20removed,=20un?= =?UTF-8?q?authenticated=20body=20bounded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../031_wp3_outcome.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md b/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md new file mode 100644 index 0000000000..79648e6ac8 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md @@ -0,0 +1,63 @@ +# wp3 결과 — p2(#2776) 재스택 + D1 구현 + T20 + +브랜치 `codex/remote-hub-p2`: `7099760a5` → `b7282858b`. +베이스는 재스택된 `codex/remote-hub-p1@07d7f1006`. + +## 충돌 + +`tests/cli-dispatch.test.ts`와 `tests/cli-registry.test.ts`에서 순수 +추가-추가 충돌. dev와 이 단계가 같은 위치에 서로 다른 테스트를 넣었다. + +처음에 정규식으로 충돌 마커만 지우는 방식을 썼는데, 그게 닫는 중괄호를 +삼켜서 두 파일이 파싱 불가가 됐다(dispatch 3개, registry 1개 손실). +테스트가 "Unexpected end of file"로 죽고 나서야 드러났다. + +고친 방법: dev 원본 파일에서 시작해 이 단계가 **추가한 블록만** 얹었다. +마커 텍스트를 편집하는 대신 양쪽의 의도를 재구성하는 쪽이 안전하다. +두 테스트 파일 42건 전부 통과한다. + +## D1 구현 — 평문 pairing 제거 + +설계(wp1)에서 계약을 고쳤지만 코드는 그대로였다. `src/server/gui-session.ts`의 +`consumeGuiPairingGrant`가 `remoteGui.allowInsecureHttp === true`이면 +비-loopback HTTP로 `insecure-http-pairing` 세션을 발급하고 있었다. + +제거했다. 그리고 **순서를 바꿨다.** 기존 코드는 grant를 찾아 검증한 뒤에 +scheme을 판정해서, 거절된 교환이 이미 단회용 코드를 소비했다. TLS 종단을 +걷어낸 공격자가 운영자가 출력하는 코드를 전부 태울 수 있다는 뜻이다. +이제 grant를 읽기 전에 거절하며, 회귀 테스트가 "같은 미사용 grant가 HTTPS로는 +여전히 통한다"로 이를 증명한다. + +`allowInsecureHttp` 키는 스키마에 남기고 retired로 표시했다. 설정 스키마가 +`.strict()`라 키를 지우면 기존 설정 파일 전체가 로드 실패한다. 받아들이되 +무시하는 쪽이 피해가 작다. + +## T20 (P1) — 미인증 바디 무제한 버퍼링 + +`POST /opencodex-session`은 자격증명 없이 도달 가능한데, 바디 제한이 +`Content-Length`에 의존했다. 헤더를 생략하면 `Number(null ?? "0")`이 0이고, +chunked를 쓰면 헤더 자체가 없다. 둘 다 사전 검사를 통과해 `req.text()`에 +도달했고, 그건 끝까지 버퍼링한다. 사후 검사는 이미 프로세스가 붙들도록 +강요당한 문자열을 잰 것이다. + +읽는 중에 limit+1에서 멈추고 바디를 cancel하도록 바꿨다. 회귀 테스트는 +4 KiB 제한에 512 KiB를 `Content-Length` 없이 스트리밍하고, 서버가 제공된 +청크보다 적게 당겼음을 단언한다. + +**레드-퍼스트 확인:** 수정 전 코드로 되돌려 이 테스트가 실제로 실패하는 것을 +확인한 뒤 다시 적용했다. 경계값(정확히 4096바이트)이 여전히 통과하는 것도 +함께 고정했다. + +## 검증 + +- `bun run typecheck` 통과. +- 포커스드 7파일 **348 pass / 0 fail** + (server-auth, server-management-auth, config, cli-dispatch, cli-registry, + gui-pair-capability, gui-pair-client). +- `server-management-auth` 35건 전부 통과 — D1 계약 반전 테스트 포함. + +## 남은 것 + +T21(`hub.managementPublicOrigin`, `remoteGui.allowedTailscaleUsers`, +retired `allowInsecureHttp` 문서화)은 wp8에서 처리한다. +draft 해제도 wp8에서 CI 그린 확인 후. From 349939a53ea3e11b13074873ce05dd6f5a093335 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:21:22 +0900 Subject: [PATCH 08/34] docs(devlog): record the wp3 commit map --- .../260901_remote_hub_restack/031_wp3_outcome.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md b/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md index 79648e6ac8..47dfc97b71 100644 --- a/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/031_wp3_outcome.md @@ -61,3 +61,19 @@ chunked를 쓰면 헤더 자체가 없다. 둘 다 사전 검사를 통과해 `r T21(`hub.managementPublicOrigin`, `remoteGui.allowedTailscaleUsers`, retired `allowInsecureHttp` 문서화)은 wp8에서 처리한다. draft 해제도 wp8에서 CI 그린 확인 후. + +## 커밋 + +| 커밋 | 내용 | +| --- | --- | +| `1e3f7d2b7`→재적용 | feat(remote-gui): add remote session issuance and pairing | +| `129a64184`→재적용 | fix(remote-gui): harden identity and capability replay checks | +| `53986b612`→재적용 | test(remote-gui): cover remote session consent boundaries | +| `6c8dd333e`→재적용 | fix(remote-gui): enforce exact bootstrap destination | +| `0c6670e88`→재적용 | test(remote-gui): lock replay and expiry negatives | +| `2d1262bc5` | fix(remote-gui): preserve renewal and mutation origin checks | +| `b7282858b` | fix(remote-gui): drop plaintext pairing and bound the unauthenticated exchange body (신규) | + +원본 6커밋은 authorship과 메시지를 보존했고, 계약 변경은 마지막 조정 커밋 +하나로 모았다. wp2와 같은 이유다 — 원저자의 커밋을 내가 편집한 것처럼 +보이게 만들지 않는다. From cdc016245f091f6fb5bc4f4f465033a13457d2b8 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:27:12 +0900 Subject: [PATCH 09/34] =?UTF-8?q?docs(devlog):=20record=20wp4=20outcome=20?= =?UTF-8?q?=E2=80=94=20p3=20rebased,=20stranded-connection=20journal=20def?= =?UTF-8?q?ect=20fixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../041_wp4_outcome.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md b/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md new file mode 100644 index 0000000000..2ec85b99be --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md @@ -0,0 +1,57 @@ +# wp4 결과 — p3(#2777) 재스택 + T22 + +브랜치 `codex/remote-hub-p3`: `aa2615953` → `ad1ab25d8`. +베이스는 재스택된 `codex/remote-hub-p2@b7282858b`. + +## 충돌 3건 — 전부 순수 추가 + +`src/cli/status.ts`, `tests/cli-dispatch.test.ts`, `src/cli/dispatch.ts`. +dev와 이 단계가 서로 다른 import와 블록을 같은 위치에 넣은 것뿐이라 +양쪽을 모두 살렸다. wp3에서 정규식으로 마커를 지우다 중괄호를 잃은 전례가 +있어, 이번에는 마커 줄 번호를 정확히 지정해 삭제하고 중괄호 균형을 매번 +확인했다. + +원본 11커밋 전부 보존. + +## T22 (P1) — process 소유 journal이 연결을 가둔다 + +리뷰 표현은 "연결 전 기존 Codex journal 재소유 필요"였다. 코드를 따라가니 +실제 증상은 더 나빴다. + +`ocx start` 후 connect하는 것은 예외가 아니라 **정상 경로**다. 그 시점에 +라우팅은 이미 주입돼 있고 journal 소유자는 프록시 프로세스다. connect는 +소유권을 가져오지 못한다 — `writeJournal()`이 이미 주입된 config를 가진 +journal을 덮어쓰지 않기 때문이다(`journal.ts:99`). 그래서 process 소유자가 +연결 상태로 그대로 살아남는다. + +그리고 `disconnectClient()`가 자기 키와 안 맞는 소유자를 전부 충돌로 읽고 +거부했다. 결과적으로 **운영자가 disconnect할 수 없다.** 아티팩트는 보존되니 +데이터를 잃지는 않지만, 연결 상태에서 나갈 방법이 없다. + +수정: process 소유 journal은 같은 도구가 쓴 주입 이전 baseline이므로 우리가 +되감을 대상이다. 진짜 충돌은 **다른 client 키**가 소유한 경우뿐이고, 그건 +여전히 거부한다(기존 테스트도 그대로 통과). + +journal 없이 라우팅만 주입된 경우는 별도 메시지로 분리했다. 기존에는 소유권 +오류로 뭉뚱그려졌는데, 복원할 baseline 기록이 아예 없다는 게 실제 원인이다. + +**레드-퍼스트:** 수정을 되돌려 새 테스트가 실패하는 것을 확인한 뒤 복원했다. +처음에 픽스처 조건이 `disconnect-conflict`에만 걸려 있어 새 시나리오가 +codex를 선택조차 하지 않는 실수가 있었고, 그래서 "통과"가 가짜였다. 조건을 +고친 뒤에야 진짜 레드가 나왔다. + +## gui api-auth-memory + +wp3에서 소유 단계를 p2로 재배정했으므로 여기서는 회귀만 확인했다. + +## 검증 + +- `bun run typecheck` 통과. +- 포커스드 8파일 **324 pass / 0 fail** + (client-connect, cli-dispatch, cli-registry, cli-status-json, + cli-headless-parity, cli-start-journal-order, config, claude-cli). + +## 남은 것 + +T23(신뢰할 수 없는 `Content-Length`에 대한 응답 읽기 제한)과 +T24(connect 워크플로 문서화)는 wp8에서 처리한다. From cddf76592ea6238686101ec0757b5f7b9993e4d7 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:27:26 +0900 Subject: [PATCH 10/34] docs(devlog): record the wp4 commit map --- .../_plan/260901_remote_hub_restack/041_wp4_outcome.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md b/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md index 2ec85b99be..42278dcf58 100644 --- a/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/041_wp4_outcome.md @@ -55,3 +55,13 @@ wp3에서 소유 단계를 p2로 재배정했으므로 여기서는 회귀만 T23(신뢰할 수 없는 `Content-Length`에 대한 응답 읽기 제한)과 T24(connect 워크플로 문서화)는 wp8에서 처리한다. + +## 커밋 + +원본 11커밋은 authorship과 메시지를 보존했고, T22 수정은 +`ad1ab25d8` 한 커밋으로 분리했다. 앞선 단계들과 같은 원칙이다. + +| 범위 | 내용 | +| --- | --- | +| `859bc17aa`..`232ad4e4b` | 원본 11커밋 재적용 | +| `ad1ab25d8` | fix(connect): a process-owned journal is ours to unwind, not a conflict (신규) | From d5658bc7fe64321fb44b4f36590f64f320ad94fc Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:36:48 +0900 Subject: [PATCH 11/34] =?UTF-8?q?docs(devlog):=20record=20wp5=20outcome=20?= =?UTF-8?q?=E2=80=94=20p4=20rebased,=20machine=20plane=20declared,=20relay?= =?UTF-8?q?=20enabled,=20D1/D2=20client=20side=20finished?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../051_wp5_outcome.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md b/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md new file mode 100644 index 0000000000..8e73fd4958 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md @@ -0,0 +1,82 @@ +# wp5 결과 — p4(#2781) 재스택 + 블로커 5건 + +브랜치 `codex/remote-hub-p4`: `44f9973a2` → `95787b9bc`. +베이스는 재스택된 `codex/remote-hub-p3@ad1ab25d8`. + +## 충돌 + +`src/usage/summary.ts`(2회), `gui/src/pages/Integrations.tsx`. + +usage/summary는 dev가 주석을 옮기고 이 단계가 그 위에 apiKeyId 필터를 +얹은 구조였다. 두 필터의 층이 다르다는 점을 주석으로 명시했다: apiKeyId는 +엔트리 전체를 자르고(키가 엔트리를 소유하므로), provider/model은 어트리뷰션 +단위로 좁힌다(콤보 엔트리의 다른 시도 비용이 딸려오면 안 되므로). + +**중간에 실수가 있었다.** 첫 해소에서 고아 마커 한 줄이 커밋에 들어갔고, +rerere가 그 잘못된 해소를 기억해 재시도에서 재현했다. 리베이스를 중단하고 +원본에서 다시 시작해 마커를 제거한 뒤, 스택 전 범위에 대해 +`git grep`으로 마커 0건을 확인했다. + +원본 8커밋 보존. + +## 블로커 5건 + +### `/api/machine/*` 7개 미선언 (wp6에서 재배정됨) + +`tests/cli-headless-parity.test.ts:287`이 잡은 그대로다. 7개 라우트는 +문서화되지 않은 게 아니라 선언되지 않은 것이었다: status/clients는 +`ocx connect status`, sync는 `ocx sync`, shim은 클라이언트 통합 명령, +disconnect는 `ocx disconnect`에 대응한다. hub-relay만 자체 verb가 없는데 +그건 `--management-transport relay`가 고르는 전송 경로이기 때문이다. +한 프리픽스로 선언하고 대응 관계를 주석에 적었다. + +### T25 (P1) — relay가 항상 throw + +`connectClient()`가 "relay management transport is not available before +Remote Hub Phase 4"를 던졌다. **그런데 이 단계가 Phase 4다.** 머신 리스너와 +hub-relay가 모두 여기서 랜딩한다. Phase 3의 가드가 남은 것이고, 문서화된 +옵션이 항상 실패하는 상태였다. + +### T26 (P1) — supervised 클라이언트가 disconnect 후 안 돌아온다 + +`scheduleStandaloneRecycle()`이 `OCX_SERVICE=1`이면 자가 재시작을 건너뛴다. +그것 자체는 옳다 — supervisor가 프로세스를 소유하므로 두 번째 복사본은 +포트를 두고 다툰다. 문제는 그 다음 `process.exit(0)`이다. + +실제 supervisor 설정은 전부 failure-only다: systemd `Restart=on-failure`, +WinSW ``, Task Scheduler ERRORLEVEL 루프. +깨끗한 종료는 "서비스가 끝났다"로 읽혀 아무것도 재시작하지 않는다. +클라이언트가 누군가 알아챌 때까지 죽어 있었다. + +supervised일 때 exit 1로 바꿨다. 대시보드 recycle이 이미 쓰는 정책이고 +(`src/server/management/system-restart.ts`), launchd `KeepAlive`는 어느 +쪽이든 정상 동작한다. + +### D1 클라이언트 측 + +`--allow-insecure-http`가 CLI, connect 옵션, hub-client에 남아 있었다. +허브가 이제 평문 pairing을 거부하므로 플래그를 남기면 단회용 grant를 +확실한 거절에 태우는 것뿐이다. 클라이언트도 같은 규칙을 로컬에서 검사해 +전송 전에 거절한다. + +### D2 클라이언트 측 — 연결 자체가 깨질 뻔했다 + +`connect`가 `catalog.etag`가 없으면 "initial hub catalog did not include a +fresh ETag"로 **실패**했다. D2로 서버가 validator를 안 주게 됐으니 그대로면 +모든 연결이 실패한다. 조건부 페치를 걷어내고, 저장하던 `catalogEtag`를 +`catalogFingerprint`(우리가 쓴 바이트의 해시)로 바꿨다. + +그 값은 애초에 캐시 관심사가 아니었다 — disconnect가 파일을 지우기 전에 +"디스크의 이 파일이 아직 우리 것인가"를 묻는 소유권 검사이고, 서버의 참여가 +필요 없다. ETag 문자열을 재사용했기 때문에 캐시처럼 보였을 뿐이다. + +## 검증 + +- `bun run typecheck` 통과. +- 포커스드 8파일 **356 pass / 0 fail**. +- 스택 전 범위 충돌 마커 0건. + +## 남은 것 + +T27/T28(GUI disconnect 타깃 갱신, pairing 전 페이지 게이팅), T2(연결된 GUI의 +인증된 models 경로), D5(릴레이 응답 no-store)는 wp8 또는 후속 단계에서. From ca95323c30e6552b3d3411df06847547ba306bed Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:37:01 +0900 Subject: [PATCH 12/34] docs(devlog): record the wp5 commit map --- .../_plan/260901_remote_hub_restack/051_wp5_outcome.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md b/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md index 8e73fd4958..84a272ad01 100644 --- a/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/051_wp5_outcome.md @@ -80,3 +80,13 @@ fresh ETag"로 **실패**했다. D2로 서버가 validator를 안 주게 됐으 T27/T28(GUI disconnect 타깃 갱신, pairing 전 페이지 게이팅), T2(연결된 GUI의 인증된 models 경로), D5(릴레이 응답 no-store)는 wp8 또는 후속 단계에서. + +## 커밋 + +| 범위 | 내용 | +| --- | --- | +| `67c6387a5`..`da6f97a39` | 원본 8커밋 재적용 | +| `95787b9bc` | fix(two-plane): declare the machine plane, enable relay, and finish the D1/D2 client side (신규) | + +앞선 단계들과 같은 원칙: 원본 커밋의 authorship과 메시지를 보존하고, +계약 변경은 마지막 조정 커밋 하나로 모은다. From 58307e557557fcf9a0ffd90bcdc131fb848fa04f Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:41:47 +0900 Subject: [PATCH 13/34] =?UTF-8?q?docs(devlog):=20record=20wp6=20outcome=20?= =?UTF-8?q?=E2=80=94=20p5=20rebased,=20three=20of=20four=20reported=20bloc?= =?UTF-8?q?kers=20were=20inherited=20staleness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../061_wp6_outcome.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md b/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md new file mode 100644 index 0000000000..8958cffc96 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md @@ -0,0 +1,54 @@ +# wp6 결과 — p5(#2786) 재스택 + +브랜치 `codex/remote-hub-p5`: `a62c8eba2` → `8bcfcaa8e`. +베이스는 재스택된 `codex/remote-hub-p4@95787b9bc`. + +## 충돌 2건 + +`tests/server-management-auth.test.ts`: 이 단계가 관리 ingress pairing 교환 +테스트를 wp3이 다시 쓴 평문 pairing 테스트 앞에 삽입한다. 둘 다 유지했다. + +`structure/01_runtime.md`: dev가 `codex-cli-update` 문장을, 이 단계가 +hub-management 리스너 절을 각각 추가했다. 두 행 모두 양쪽 내용을 담도록 합쳤고, +합친 뒤 각 문장이 실제로 살아 있는지 grep으로 확인했다. + +원본 8커밋 보존. + +## 리뷰가 지목한 블로커 4건 — 실측 결과 + +리뷰는 `cli-headless-parity:287`, `update-stop-first:225`, +`loopback-listener-admission:196`, privacy 게이트를 들었다. 재스택 후 실제로 +돌려보니 넷 중 셋은 이미 해소돼 있었다. + +- `cli-headless-parity` 42 pass — `/api/machine/*` 선언은 소유 단계인 wp5에서 + 이미 처리했다(감사 A5의 재배정이 맞았다). +- `update-stop-first` 15 pass — 상속된 staleness였다. +- privacy 게이트 통과 — 역시 staleness. +- `loopback-listener-admission`만 실제로 빨간색이었다. + +## loopback-listener-admission:196 + +테스트가 non-hub role 셋(undefined, standalone, client)을 순회하며 전부 +`"requires runtimeRole hub"` 메시지로 거절되기를 요구했다. 그런데 `client`는 +더 앞선 규칙 — client role은 완전한 연결 블록이 필요하다 — 에 먼저 걸린다. + +거절 자체는 옳다. 틀린 것은 **두 독립적인 검증 규칙 사이의 순서를 단언한 것**이다. +계약은 그런 순서를 약속한 적이 없다. + +행을 쪼갰다. undefined/standalone은 정확한 ingress 메시지를 그대로 단언하고, +`client`는 "거절된다"만 단언한다. 그리고 이게 구멍을 만들지 않도록 케이스를 +하나 더 넣었다: **완전한** client 연결을 주면 앞선 규칙이 안 걸리고, 그때 +거절하는 것이 ingress 규칙임을 확인한다. 이게 없으면 ingress 규칙이 그 role에 +아예 적용되지 않게 되어도 약해진 단언이 통과해버린다. + +## 검증 + +- `bun run typecheck` 통과, `bun run privacy:scan` 통과. +- 포커스드 5파일 **263 pass / 0 fail**. +- `tests/service.test.ts` 192 pass / 0 fail. +- 충돌 마커 0건. + +## 남은 것 + +T29/T30(hub role을 disconnected client state에서 배제, missing-config +부트스트랩 경쟁)과 T3/T11은 wp8에서. From 91529561e7eeb14a01e4340875cbd21454b4ad2c Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:42:12 +0900 Subject: [PATCH 14/34] docs(devlog): record the wp6 commit map --- devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md b/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md index 8958cffc96..19577c712a 100644 --- a/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/061_wp6_outcome.md @@ -52,3 +52,10 @@ hub-management 리스너 절을 각각 추가했다. 두 행 모두 양쪽 내 T29/T30(hub role을 disconnected client state에서 배제, missing-config 부트스트랩 경쟁)과 T3/T11은 wp8에서. + +## 커밋 + +| 범위 | 내용 | +| --- | --- | +| `149b7215a`..`f2bf97d4f` | 원본 8커밋 재적용 | +| `8bcfcaa8e` | test(deploy): assert the ingress role rule where the message is actually reachable (신규) | From 91010b1b355ef85e408aa42bd0a0ee640e6cb389 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:49:49 +0900 Subject: [PATCH 15/34] =?UTF-8?q?docs(devlog):=20record=20wp7=20outcome=20?= =?UTF-8?q?=E2=80=94=20p6=20rebased,=20rotation=20abort=20ordering=20and?= =?UTF-8?q?=20in-flight=20backup=20fixed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../071_wp7_outcome.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/071_wp7_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/071_wp7_outcome.md b/devlog/_plan/260901_remote_hub_restack/071_wp7_outcome.md new file mode 100644 index 0000000000..c4cdb293cc --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/071_wp7_outcome.md @@ -0,0 +1,67 @@ +# wp7 결과 — p6(#2789) 재스택 + D4 계열 + +브랜치 `codex/remote-hub-p6`: `207254fe0` → `ff2913297`. +베이스는 재스택된 `codex/remote-hub-p5@8bcfcaa8e`. 17커밋 / 95파일로 가장 크다. + +## 충돌 4건 + +`src/client/hub-client.ts`(2회), 터키어 관리 API 문서, +`tests/loopback-listener-admission.test.ts`. + +hub-client 충돌이 본질적이었다. p6가 스키마 검증과 `x-opencodex-key-id` 에코를 +추가하는데, 그 토대가 D2가 없앤 조건부 페치 경로 위에 있었다. validator 처리를 +걷어내고 추가분만 살렸다. + +커밋 `e7ca5bb89`("reject mismatched catalog validators")는 소스 변경 전체가 +사라진 경로 전용이라 적용할 대상이 없었다. 의도는 이미 더 강하게 흡수돼 있다 — +어떤 304든 거절하는 것이 "보낸 ETag와 다른 304를 거절"보다 넓다. 그 사실을 +테스트로 남겼다. + +`loopback-listener-admission`은 흥미로웠다. p6가 wp6에서 내가 고친 것과 +**같은 문제를 다르게** 고쳐뒀다: client role에 완전한 연결 블록을 채워 넣어 +세 role 전부를 정확한 메시지로 단언한다. p6 쪽이 낫다 — 내 버전은 client에 +대해 "거절된다"만 단언하고 별도 케이스로 보강했는데, p6는 한 루프로 끝낸다. +p6를 채택하고 내 중복 케이스를 제거했다. + +원본 17커밋 보존, 마커 0건. + +## T31 (P1) — abort 실패 시 토큰 identity + +롤백 경로가 로컬 토큰을 복원한 **뒤** 허브에 abort를 요청했다. abort가 +일시적으로 실패하면 로컬은 옛 키를, 허브는 새 키에 대한 pending 로테이션을 +들고 있다. 양쪽이 어느 세대가 현재인지 불일치하고, 이게 "rollback was +incomplete"라는 메시지로만 드러난다. + +순서를 뒤집었다. 어느 세대가 살아 있는지는 허브가 정하므로 먼저 확인하고, +동의한 뒤에만 로컬을 되감는다. 실패 시 두 후보와 pending 마커를 모두 디스크에 +남긴다 — 물어보지 않고는 정말로 판정할 수 없기 때문이다. + +## T32 (P2) — status가 인플라이트 백업을 삭제 + +orphan 정리 분기가 "백업 있음 + 토큰 있음 + pending 마커 없음"에서 발동한다. +그런데 `rotateConnectedClientKey`는 `.prev`를 쓴 **다음에** +`pendingOperation`을 저장한다. 그 사이에 `ocx connect status`가 돌면 정확히 +저 조건을 보고, 진행 중인 로테이션이 의지하던 롤백 대상을 지운다. + +게이트가 영속 상태를 다시 읽도록 했다 — 호출자의 스냅샷은 마커보다 앞설 수 +있다 — 그리고 로테이션이 기록돼 있으면 정리하지 않는다. + +## D4 — 이미 상당 부분 지켜지고 있었다 + +설계에서 요구한 "복구는 정지하고, 재개는 전이 권한을 가진 다음 rotate가"는 +`inspectClientRotationRecoveryGate`가 이미 그렇게 동작한다. probe 없이 +`recovery-required`로 멈추고 rotate를 안내한다. wp1에서 계약을 실행 가능하게 +다시 쓴 것이 코드와 일치했다. + +## 검증 + +- `bun run typecheck` 통과, `bun run privacy:scan` 통과. +- 포커스드 6파일 **302 pass / 0 fail**. +- 충돌 마커 0건. + +## 커밋 + +| 범위 | 내용 | +| --- | --- | +| `83c57609f`..`65c1f85a7` | 원본 17커밋 재적용 | +| `ff2913297` | fix(hardening): confirm the abort before rewinding, and never delete an in-flight backup (신규) | From 158d8b595d034a26f5da50b52308606d52ffd9d1 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 17:50:26 +0900 Subject: [PATCH 16/34] docs(devlog): record the rebased stack state across all seven phases --- .../072_stack_state.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/072_stack_state.md diff --git a/devlog/_plan/260901_remote_hub_restack/072_stack_state.md b/devlog/_plan/260901_remote_hub_restack/072_stack_state.md new file mode 100644 index 0000000000..08588fdefe --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/072_stack_state.md @@ -0,0 +1,40 @@ +# 스택 상태 — 7단계 재스택 완료 시점 + +| PR | 브랜치 | 이전 head | 새 head | 부모 | +| --- | --- | --- | --- | --- | +| #2771 | codex/remote-hub-design | `bad162407` | `36992baa9` | dev | +| #2772 | codex/remote-hub-p1 | `c10ef21a9` | `07d7f1006` | design | +| #2776 | codex/remote-hub-p2 | `7099760a5` | `b7282858b` | p1 | +| #2777 | codex/remote-hub-p3 | `aa2615953` | `ad1ab25d8` | p2 | +| #2781 | codex/remote-hub-p4 | `44f9973a2` | `95787b9bc` | p3 | +| #2786 | codex/remote-hub-p5 | `a62c8eba2` | `8bcfcaa8e` | p4 | +| #2789 | codex/remote-hub-p6 | `207254fe0` | `ff2913297` | p5 | + +각 단계는 직전 단계의 재스택된 head 위에 얹혔다. 원본 커밋은 전부 authorship과 +메시지를 보존했고, 계약 변경은 단계마다 조정 커밋 하나로 분리했다. + +## 원본 커밋 보존 + +| 단계 | 원본 커밋 | 조정 커밋 | +| --- | --- | --- | +| design | 9 | 3 | +| p1 | 5 | 1 | +| p2 | 6 | 1 | +| p3 | 11 | 1 | +| p4 | 8 | 1 | +| p5 | 8 | 1 | +| p6 | 17 | 1 | + +## 해소된 것 + +D1~D5 설계 계약 5건, 리뷰 스레드 중 P1 6건(T1, T20, T22, T25, T26, T31)과 +T32, 그리고 리뷰 본문이 지목한 테스트 실패 전부. + +stale 아티팩트였던 것들 — `release-version-line`, privacy 게이트, +`update-stop-first`, `cli-headless-parity`의 일부 — 은 재스택으로 소멸했고 +각 단계 head에서 실제로 확인했다. + +## 남은 것 + +P2/Minor 스레드들(T2/T3/T7/T10/T11/T19/T21/T23/T24/T27/T28/T29/T30/T33)과 +`#2771`의 마크다운 린트 6건. wp8에서 처리하거나 근거를 갖춘 반박을 남긴다. From 6be23bd02625083f2eacfc90b6f6e1650c29252f Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 18:18:45 +0900 Subject: [PATCH 17/34] docs(devlog): record the exact-head CI repairs and the dev-side macos flake finding --- .../081_wp8_ci_repairs.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md diff --git a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md new file mode 100644 index 0000000000..5ed78f6265 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md @@ -0,0 +1,70 @@ +# wp8 — exact-head CI 실패 규명과 수정 + +7단계 재스택 후 각 PR의 exact head에서 CI를 돌려 실패를 하나씩 규명했다. +네 갈래였고, 그중 셋이 진짜 결함이었다. + +## 1. `test 3/4` — sync 러너가 종료 코드를 삼킨다 (p3 소유) + +`tests/cli-transport-honesty.test.ts`가 "핸들러를 await한 뒤 리터럴 0을 +반환하는" 러너를 잡는다. 그 패턴은 핸들러가 `process.exitCode`에 기록한 +실패를 지우기 때문이고, 예외는 이름이 아니라 검증된 이유와 함께 allowlist에 +올려야 한다. + +connected sync 분기에는 그런 이유가 없다. `handleConnectedSyncCatalogWrite`가 +app-server 재시작을 구동하므로 거기서 난 실패는 살아남아야 한다. +다른 러너들과 같이 `process.exitCode`를 반환하게 했다. + +## 2. `hygiene` — suppression (p4 소유) + +`gui/src/connect-pairing.ts`가 `react-refresh/only-export-components`를 +eslint-disable로 막고 있었다. 룰이 옳았다 — 한 파일이 전송 함수와 컴포넌트를 +같이 export한다. 억제 대신 `connect-pairing-transport.ts`로 분리했다. +전송은 React 없이 테스트 가능하고, 폼은 그걸 호출하는 것 말고 로직이 없다. + +## 3. `gates` — 릴레이 pairing이 인증 없이 나간다 (p4 소유) + +`submitConnectPairing`이 `fetchImpl: typeof fetch = fetch`를 받았다. +기본 매개변수는 **모듈 평가 시점의** 전역을 묶는다 — `installApiAuthFetch`가 +`window.fetch`에 씌우는 래퍼가 아니라 원본이다. 릴레이는 그 래퍼가 붙이는 +머신 세션 헤더를 요구하므로 허브가 교환을 거부했다. 호출 시점에 해석하도록 고쳤다. + +## 4. `gates` — happy-dom에 없는 prompt (p2 소유) + +거부된 세션을 정리하는 테스트들이 admin 토큰 폴백에 도달하는데, +happy-dom은 `prompt`를 구현하지 않는다. 그래서 그 테스트들은 검증하려던 +동작이 아니라 TypeError로 죽었다. 대부분의 테스트는 폴백에 안 닿아서 +가려져 있었다. null을 반환하는 스텁이 "운영자가 프롬프트를 닫았다"에 +해당하는 정직한 대역이다. + +## 5. GUI 스위트 격리 — 제품 결함 아님 + +`tests/connect-pairing.test.ts`가 단독으로는 통과하고 전체 실행에서 실패했다. +App이 모듈 스코프에서 `installApiAuthFetch()`를 부르므로 최초 import에서만 +실행된다. 나중에 App을 import하는 테스트는 캐시된 모듈을 받고 설치가 일어나지 +않아, 래퍼가 **먼저 import한 테스트의 window**에 묶인 채로 남는다. + +테스트가 마운트 전에 자기 window로 래퍼를 다시 묶도록 했고, +`claude-toggle-race.test.tsx`는 window를 닫을 때 설치 latch도 함께 지운다. +둘 다 테스트 격리이지 제품 동작이 아니다. + +## macos 실패는 이 스택 탓이 아니다 + +`tests/server-auth.test.ts`의 websocket refresh 단언이 macos에서 실패했는데, +**dev의 HEAD도 같은 러너에서 같은 단언으로 실패한다.** #3139의 수정이 이미 +dev에 들어가 있는데도 그렇다. #2772는 동일 head를 재실행하니 그린이 됐다. +즉 dev에 남은 미해결 flake이고, 재스택이 유발한 것이 아니다. + +## 최종 체인 + +| 단계 | head | +| --- | --- | +| design | `36992baa9` | +| p1 | `07d7f1006` | +| p2 | `2b36ad496` | +| p3 | `38c361362` | +| p4 | `158424f05` | +| p5 | `ff3ce26bd` | +| p6 | `b6aa976e9` | + +6개 엣지 전부 부모가 자식의 조상이고, PR base ref도 같은 부모를 가리킨다. +오염 커밋 0건, 변경 범위는 devlog/docs-site/gui/src/structure/tests뿐이다. From 209a718ae212d6c19964e24f8d2c018603aaee26 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 18:32:39 +0900 Subject: [PATCH 18/34] docs(devlog): record the route-registry gap and the dev flake root cause split to #3147 --- .../081_wp8_ci_repairs.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md index 5ed78f6265..07fd5c5c10 100644 --- a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md +++ b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md @@ -68,3 +68,45 @@ dev에 들어가 있는데도 그렇다. #2772는 동일 head를 재실행하니 6개 엣지 전부 부모가 자식의 조상이고, PR base ref도 같은 부모를 가리킨다. 오염 커밋 0건, 변경 범위는 devlog/docs-site/gui/src/structure/tests뿐이다. + +## 추가로 드러난 두 건 + +### 6. `test 1/4` — 미선언 관리 라우트 4개 (p6 소유) + +`tests/management-route-registry.test.ts`가 선언 레지스트리를 소스와 대조해 +이 단계가 서빙하면서 등록하지 않은 라우트 4개를 찾았다: +`/api/keys/rotate`의 POST/POST commit/DELETE와 `POST /api/session/logout`. + +rotate 3개는 평범한 관리 뮤테이션이라 그대로 선언했다. +`/api/session/logout`은 session-only 예외로 이유와 함께 등록했다 — 현재 +gui-session을 끝내며 그 세션 자신의 Origin과 CSRF를 요구하므로 CLI verb가 +작용할 대상이 없다. CLI는 admin 토큰을 들고 있고, 이 라우트는 바로 그 +admin 토큰을 거부한다. 자기가 만들지 않은 동의 세션을 끝내지 못하게 하려는 +설계다. + +### 7. dev의 websocket flake 근본 원인 — PR #3147로 분리 + +`server-auth`의 websocket refresh 단언이 macOS와 Linux 양쪽에서 실패했고, +dev HEAD도 같은 실패를 낸다. 원인을 찾았다. + +`updateAccountQuota`가 `updatedAt: Date.now()`를 찍는데, 시드가 시계 고정 +**전에** 실행된다. 그래서 그 타임스탬프만 실제 벽시계이고 이후 모든 것은 +고정된 2027 값을 읽는다. 격차가 약 136일인데 신선도 창은 6시간이다 +(`QUOTA_DISK_MAX_AGE_MS`, `src/codex/quota.ts:491`). 러너가 아무리 빨라도 +시드는 stale로 읽히고, 시작 시 pool-quota 프라임이 첫 턴 전에 자격증명을 +갱신해 `seenAuth[0]`이 이미 새 토큰이 된다. 실패 diff가 항상 첫 원소였던 +이유다. + +#3139는 `startServer` 앞에 시계와 fetch를 고정해 프라임 자신의 읽기 창을 +닫았다. 하지만 그 둘이 놓이기 **전에** 쓰인 타임스탬프의 창은 닫을 수 없다. +시드를 고정 뒤로 옮기면 닫힌다. + +이건 dev 소유라 스택에 섞지 않고 **PR #3147**로 분리해 `dev`를 타깃하게 했다. +로컬에서는 수정 전후 모두 재현되지 않으므로 증거는 red-to-green이 아니라 +메커니즘이다 — 6시간 창에 136일 격차는 경쟁이 아니라 산술이다. + +## 남은 것 — 사람이 해야 하는 항목 + +`enforce-target`이 #2776/#2781/#2789에 대해 UI 스크린샷을 요구한다. +세 PR 모두 실제 GUI 변경(각각 3/34/15 파일)을 담고 있으므로 요구가 정당하다. +스크린샷은 사람이 캡처해 PR 설명에 붙여야 한다. From 2c021faf0a5465e6ea43560eb6778e2dcf98bb97 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 18:54:43 +0900 Subject: [PATCH 19/34] docs(devlog): close the remote hub restack unit --- .../260901_remote_hub_restack/090_outcome.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/090_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/090_outcome.md b/devlog/_plan/260901_remote_hub_restack/090_outcome.md new file mode 100644 index 0000000000..e0afd5c4af --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/090_outcome.md @@ -0,0 +1,72 @@ +# 결과 — remote hub 스택 재스택 + +## 상태 + +7단계 전부 현재 `dev` 위로 재스택되어 푸시됐고, 실질 CI 실패는 0건이다. + +| PR | 브랜치 | head | CI | +| --- | --- | --- | --- | +| #2771 | design | `36992baa9` | 그린 | +| #2772 | p1 | `07d7f1006` | 그린 | +| #2776 | p2 | `2b36ad496` | 그린 (스크린샷 대기) | +| #2777 | p3 | `38c361362` | 그린 | +| #2781 | p4 | `158424f05` | 그린 | +| #2786 | p5 | `ff3ce26bd` | 그린 | +| #2789 | p6 | `4ab13b463` | 그린 (스크린샷 대기) | + +체인 6개 엣지 전부 부모가 자식의 조상이고, PR base ref도 같은 부모를 가리킨다. +오염 커밋 0건. 원본 64커밋 전부 authorship과 메시지를 보존했고, 계약 변경은 +단계마다 조정 커밋으로 분리했다. + +## 닫은 것 + +**설계 계약 5건** — D1 평문 pairing 제거(설정 키까지, 4개 문서), D2 +identity-varying 응답의 validator 제거(서버+클라이언트), D3 Origin verbatim +전달(안전 읽기 허용 보존), D4 로테이션 크래시 복구를 실행 가능한 상태 기계로, +D5 릴레이 응답 no-store. + +**P1 리뷰 스레드 6건** — T1(공개 devlog 프레이밍), T20(미인증 바디 무제한 +버퍼링), T22(process 소유 journal이 연결을 가둠), T25(relay가 항상 throw), +T26(supervised 클라이언트가 disconnect 후 안 돌아옴), T31(abort 실패 시 토큰 +identity). T32도 함께 닫았다. + +**CI 실패 6건** — sync 러너 종료 코드, eslint suppression, 릴레이 pairing +미인증, happy-dom prompt, GUI 테스트 격리, 미선언 관리 라우트 4개 + +capability 미선언. + +## 실제로 스택 문제가 아니었던 것 + +리뷰가 지목한 실패 중 상당수가 상속된 staleness였다. +`release-version-line`, privacy 게이트, `update-stop-first`, +`cli-headless-parity`의 일부는 재스택만으로 사라졌고 각 단계 head에서 +확인했다. + +`server-auth`의 websocket refresh flake는 **dev 자체의 결함**이었다. +dev HEAD도 같은 단언으로 실패한다. 근본 원인(시계 고정 전에 찍히는 두 개의 +타임스탬프)을 찾아 **PR #3147**로 분리했다. 스택에 섞지 않은 이유는 소유가 +dev이기 때문이다. + +## 감사가 바꾼 것 + +로드맵 1차 감사가 FAIL 10건을 냈고 전건 수용했다. 그중 둘이 실제 작업 순서를 +바꿨다: 미해결 인라인 스레드 33건이 로드맵에 아예 빠져 있었고, +블로커 2건이 한 단계씩 늦게 배정돼 있었다(`/api/machine/*`는 p4가 도입, +`api-auth-memory`는 p2가 터치). diff로 실측해 재배정했다. + +설계 수정 1차에 대한 적대적 리뷰도 FAIL을 냈다. D1을 040에서만 지우고 +010/050/070에 계약이 살아 있었고, D4는 새 규칙을 쓰면서 옛 규칙을 안 지워 +문서가 자기모순이었다. 둘 다 리뷰 지적대로 닫았다. + +## 남은 것 — 사람이 해야 함 + +1. **UI 스크린샷** — `enforce-target`이 #2776/#2789에 요구한다. 세 PR 모두 + 실제 GUI 변경을 담고 있어 요구가 정당하다. +2. **리뷰 승인** — 7건 전부 `CHANGES_REQUESTED` 상태다. + `MAINTAINERS.md`가 비저자 메인테이너 승인과 보안 리뷰를 요구하고, + Ingwannu가 유일한 비저자 메인테이너다. CI가 초록이어도 이 상태로는 + 머지 버튼이 열리지 않는다. +3. **P2/Minor 스레드** — T2/T3/T7/T10/T11/T19/T21/T23/T24/T27/T28/T29/T30/T33과 + #2771의 마크다운 린트 6건. 수정하거나 근거를 갖춘 반박을 남기고 resolve한다. + +사용자 요청은 "머지 가능한 정도까지 세팅"이었다. 자동화 게이트 기준으로는 +도달했다. 승인은 우리 손 밖이다. From fa88164a0a5886f84a9448ba82d00a8a193d934b Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 18:55:10 +0900 Subject: [PATCH 20/34] docs(devlog): note the split-out dev flake PR in the outcome --- devlog/_plan/260901_remote_hub_restack/090_outcome.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/090_outcome.md b/devlog/_plan/260901_remote_hub_restack/090_outcome.md index e0afd5c4af..a6f80eaff2 100644 --- a/devlog/_plan/260901_remote_hub_restack/090_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/090_outcome.md @@ -70,3 +70,9 @@ dev이기 때문이다. 사용자 요청은 "머지 가능한 정도까지 세팅"이었다. 자동화 게이트 기준으로는 도달했다. 승인은 우리 손 밖이다. + +## 분리한 PR + +**#3147** `test(auth): seed the pool quota and credential after the clock is pinned` +— `dev` 타깃, 테스트 파일 한 개. 이 스택의 브랜치가 아니라 dev가 소유하는 +flake라서 섞지 않았다. 스택 7건과 독립적으로 리뷰·머지된다. From 1995193569ec750e90acf2c0e518590449cd0168 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:11:13 +0900 Subject: [PATCH 21/34] docs(devlog): audit remote hub exposure, requests, and rollback for a standalone user --- .../100_polish_audit.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/100_polish_audit.md diff --git a/devlog/_plan/260901_remote_hub_restack/100_polish_audit.md b/devlog/_plan/260901_remote_hub_restack/100_polish_audit.md new file mode 100644 index 0000000000..1e392bd9db --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/100_polish_audit.md @@ -0,0 +1,65 @@ +# 최종 폴리싱 감사 — 노출 / 요청 / 롤백 + +요구사항: 기능을 켜지 않은 일반 사용자에게 **UI가 노출되지 않고**, **API 요청이 +발생하지 않으며**, **로컬로 되돌리기 쉬울 것.** + +세 축을 코드로 추적했다. 서버는 깨끗했고, 클라이언트에 실질 위반 세 건이 있다. +전부 이 스택이 도입한 것이고 `dev`에는 없다. + +## 서버 — 위반 없음 + +머신 플레인 라우트(`/api/machine/*`)는 `src/client/machine-listener.ts`가 +서빙하고, 그 리스너는 `src/client/runtime.ts`가 **연결된 클라이언트 롤에서만** +띄운다. standalone 프록시의 `src/server/index.ts`에는 해당 라우트가 아예 없다. + +즉 standalone 사용자의 프로세스는 이 라우트를 열지 않는다. `AGENTS.md`의 +optional-subsystem 원칙과 같은 모양이다 — 켜지 않으면 코드가 돌지 않는다. + +## 위반 1 (요청) — 모든 부팅에서 나가는 discovery 요청 + +`gui/src/App.tsx:113-137`의 `useEffect`가 조건 없이 실행되고, +`gui/src/api-targets.ts:118-131`의 `discoverApiTargets()`가 +`GET /api/machine/status`를 친다. + +standalone에서는 그 라우트가 없으므로 404가 돌아오고 `:126`이 standalone +타깃으로 폴백한다. 동작은 옳다. 그런데 **요청 자체는 나간다.** remote hub를 +켠 적 없는 사용자의 브라우저가 매 로드마다 이 스택이 정의한 엔드포인트를 +한 번씩 두드린다. + +404 폴백은 "기능이 조용하다"가 아니라 "기능이 없다는 것을 매번 물어서 +확인한다"이다. + +## 위반 2 (노출) — 전체 페이지가 discovery 결과 뒤로 밀린다 + +`gui/src/App.tsx:390-393`이 페이지 본문 전체를 `targetsSettled` 뒤에 둔다. +정착 전에는 `connection.discovering`("로컬 및 공유 대상을 확인하는 중…") +배너만 보이고, 대시보드도 프로바이더도 로그도 렌더되지 않는다. + +standalone 사용자에게 이건 자기가 쓰지 않는 기능의 로딩 문구다. 그리고 +`dev`의 App에는 이 게이트가 존재하지 않는다 — 스택이 만든 것이다. + +## 위반 3 (노출) — discovery 실패가 대시보드 전체를 대체한다 + +같은 곳 `:392-393`. `targetError`면 본문 전체가 +`connection.machineUnavailable`("로컬 머신 연결을 사용할 수 없습니다. 공유 +요청을 로컬로 우회하지 않았습니다.")로 대체된다. + +`discoverApiTargets`는 fetch가 **throw할 때** 에러를 던진다(`:123-125`). +프록시가 재시작 중이거나 잠깐 느리면 standalone 사용자가 대시보드 대신 +원격 플레인 이야기를 하는 에러 화면을 본다. 자기가 켠 적 없는 기능 때문에 +쓰던 화면을 잃는 것이다. + +## 롤백 — 재검증 대상 + +`disconnect`의 원상복구 계약은 wp4에서 이미 한 번 고쳤다(process 소유 +journal을 충돌로 오독해 연결이 갇히던 문제). 이번 사이클에서 부분 복구가 +조용히 성공으로 보이지 않는지 재확인한다. + +## 방향 + +서버가 이미 GUI HTML에 세션 메타 태그를 주입한다(`src/server/gui-static.ts:69-75`). +같은 자리에 롤을 실어 보내면 클라이언트는 **묻지 않고도** 자기가 standalone인지 +안다. 요청이 사라지고, 게이트가 사라지고, 에러 화면이 사라진다. + +standalone은 아무것도 하지 않는 것이 기본값이어야 한다. 지금은 아니라고 +확인하는 절차가 기본값이다. From eb5245b1b97f5d1ca4cb55b3c1106403c9e4c572 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:42:12 +0900 Subject: [PATCH 22/34] docs(devlog): record the exposure, request, and rollback polish --- .../101_polish_outcome.md | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md b/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md new file mode 100644 index 0000000000..7fddc56b5d --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md @@ -0,0 +1,84 @@ +# 폴리싱 결과 — 노출 / 요청 / 롤백 + +감사(`100`)가 낸 위반 3건과, 적대적 리뷰가 추가로 잡은 4건을 닫았다. +리뷰 verdict는 **FAIL**이었고 내 감사가 불완전했다는 지적이 맞았다. + +## 내가 놓친 것 — 리뷰가 잡음 + +**연결 전 로컬 카탈로그가 복구되지 않았다.** 이게 가장 무거웠고, 사용자가 +말한 "다시 로컬로 롤백"의 정확히 그 지점이다. connect는 +`DEFAULT_CATALOG_PATH`에 있던 것을 덮어쓰는데, 원본 스냅샷을 메모리 +(`priorCatalog`)에만 뒀다. 그건 같은 실행 안에서 실패해 롤백하는 경우만 +커버한다 — disconnect는 다른 날 다른 프로세스다. 영속 상태에는 원격 카탈로그의 +지문만 있어서, disconnect는 원격 카탈로그를 **지우고** "native Codex state was +restored"라고 보고했다. 사용자가 원래 갖고 있던 카탈로그는 그냥 사라진다. + +토큰은 재발급되고 config는 저널에 있다. 카탈로그는 다른 어디에서도 복원할 수 +없는 유일한 아티팩트다. + +**부분 프로필 복구가 성공으로 위장됐다.** `restoreJournalState`가 삼켜진 +unlink 뒤에 `profileRestored = true`를 무조건 세팅했다. 원본 프로필이 없던 +경우 "우리가 만든 걸 지운다"가 실패해도 complete로 보고되고, 그러면 저널이 +지워진다 — 남은 프로필이 우리 것이라는 유일한 기록이. 사용자는 복구됐다는 +말을 듣고, 우리 프로필은 아무도 가리키지 않는 채로 디스크에 남는다. + +**standalone에 두 UI가 더 남아 있었다.** 키 로테이션 컨트롤(모든 API 키에)과 +"Source: local usage.jsonl" 줄. 둘 다 dev에는 없다. + +## 수정 + +### 요청 — 서버가 롤을 말한다 + +서버가 이미 세션 메타를 주입하니, 같은 자리에 `opencodex-runtime-role`을 +싣는다. 세션 블록과 **독립적으로** 내보낸다 — standalone은 GUI 세션을 발급하지 +않으므로, 세션에 묶으면 정작 필요한 경우가 빈다. + +클라이언트는 묻는 대신 읽는다. 태그가 없으면 standalone으로 읽는데, 구버전 +서버·별도 호스팅 GUI·Vite 개발 서버가 전부 여기 해당하고 셋 다 요청을 보내면 +안 되는 쪽이다. + +### 노출 — 기본이 "아무것도 안 함" + +`targetsSettled`가 standalone에서 `true`로 시작한다. 발견할 게 없으니 +기다릴 것도 없다. 그리고 발견 실패는 배너지 대체가 아니다 — 느리거나 재시작 +중인 프록시가 standalone 사용자의 대시보드를 앗아가지 않는다. + +rotation 핸들러는 연결된 런타임에만 전달한다(없으면 섹션이 렌더되지 않는다). +usage source 행도 연결됐을 때만 — "어느 저장소가 이 숫자를 줬나"는 저장소가 +둘일 때만 존재하는 질문이다. + +### 롤백 — 되돌리기지 지우기가 아니다 + +`priorCatalog`를 연결 상태에 영속화하고 disconnect가 되돌려 쓴다. +`""`는 "정말 없었다"라서 제거가 곧 복원이다. 필드가 없는 옛 연결은 기존 +동작을 유지한다 — 복원할 대상이 기록된 적이 없으니 그게 정직하다. +소유권 검사는 그대로다: connect 이후 편집된 카탈로그는 사용자 것이고 +`changed`로 거절한다. 결과에 `catalogRestored`를 더해 두 결과를 구별한다. + +프로필은 **확인된** 제거만 성공으로 친다. ENOENT는 성공인데, 파일이 이미 +없는 것이 제거가 원한 결과이기 때문이다. + +## 검증 + +- GUI 스위트 **1207 pass / 0 fail**. +- 포커스드: client-connect, codex-journal, config, cli-capabilities, + management-route-registry, gui-static, server-management-auth 전부 그린. +- `bun run typecheck`, `bun run lint:gui` 클린. +- 레드-퍼스트 확인: standalone 무요청(0 fetch), 카탈로그 복구, 프로필 계약 + 셋 다 수정 전 실패를 확인한 뒤 적용했다. + +## 정직하게 남기는 것 + +프로필 unlink 실패는 **런타임으로 재현할 수 없다.** unlink를 실패시키려면 Codex +홈에 쓰기를 막아야 하는데, 그러면 같은 함수의 앞선 atomic config 쓰기가 먼저 +던진다. 그래서 그 계약은 source-level로 고정하고 테스트에 이유를 적었다. +조작된 런타임 실패를 만들어내는 것보다 모양을 단언하는 쪽이 증명하는 바가 많다. + +## 리뷰가 지적했으나 이번에 다루지 않은 것 + +- `/healthz`의 `guiPairCapability`, `/readyz`의 프로토콜 메타데이터, 관리 + CORS의 GUI-세션 헤더 광고. UI가 아니라 프로토콜 표면이고, 롤 게이팅이 + 프로토콜 협상 자체를 깨뜨릴 수 있어 별도 판단이 필요하다. +- disconnect의 비트랜잭션성: 카탈로그 충돌 시 config는 복구됐는데 + `runtimeRole=client`가 남는 경로. 에러로 보고되므로 조용한 실패는 아니지만, + 복구 가능한 상태 기계로 만드는 것은 이번 스코프를 넘는다. From 4f1c1fbf0a1d04cf41b85a4ecc9f70b9544c0065 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:42:54 +0900 Subject: [PATCH 23/34] docs(devlog): record the polish commit map and final chain --- .../101_polish_outcome.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md b/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md index 7fddc56b5d..6cc8729823 100644 --- a/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/101_polish_outcome.md @@ -82,3 +82,25 @@ usage source 행도 연결됐을 때만 — "어느 저장소가 이 숫자를 - disconnect의 비트랜잭션성: 카탈로그 충돌 시 config는 복구됐는데 `runtimeRole=client`가 남는 경로. 에러로 보고되므로 조용한 실패는 아니지만, 복구 가능한 상태 기계로 만드는 것은 이번 스코프를 넘는다. + +## 커밋 + +| 단계 | 커밋 | 내용 | +| --- | --- | --- | +| p3 | `c5420db86` | 카탈로그 복구 + 프로필 계약 | +| p4 | `4aad8abbf` | 롤 메타 태그, standalone 무요청, 페이지 게이트 제거 | +| p6 | `2349d39e8` | standalone rotation UI + usage source 행 제거 | + +## 최종 체인 + +| 단계 | head | +| --- | --- | +| design | `36992baa9` | +| p1 | `07d7f1006` | +| p2 | `2b36ad496` | +| p3 | `c5420db86` | +| p4 | `4aad8abbf` | +| p5 | `072cc29c3` | +| p6 | `2349d39e8` | + +6개 엣지 전부 부모가 자식의 조상이고, 오염 커밋은 없다. From 89dfdcf5de3f30402e8168d759f1b9adc0af575c Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:52:52 +0900 Subject: [PATCH 24/34] docs(devlog): describe the lint suppression without writing the directive --- devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md index 07fd5c5c10..e86dd03044 100644 --- a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md +++ b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md @@ -17,10 +17,13 @@ app-server 재시작을 구동하므로 거기서 난 실패는 살아남아야 ## 2. `hygiene` — suppression (p4 소유) `gui/src/connect-pairing.ts`가 `react-refresh/only-export-components`를 -eslint-disable로 막고 있었다. 룰이 옳았다 — 한 파일이 전송 함수와 컴포넌트를 +린트 억제 주석으로 막고 있었다. 룰이 옳았다 — 한 파일이 전송 함수와 컴포넌트를 같이 export한다. 억제 대신 `connect-pairing-transport.ts`로 분리했다. 전송은 React 없이 테스트 가능하고, 폼은 그걸 호출하는 것 말고 로직이 없다. +(이 문서가 억제 지시자를 문자 그대로 적었더니 hygiene 게이트가 새 억제로 읽어 +draft를 유지시켰다. 게이트가 옳게 동작한 것이므로 문구를 바꿨다.) + ## 3. `gates` — 릴레이 pairing이 인증 없이 나간다 (p4 소유) `submitConnectPairing`이 `fetchImpl: typeof fetch = fetch`를 받았다. From d43ae71ba64734067b13e821144b7c7718b99637 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:58:50 +0900 Subject: [PATCH 25/34] docs(devlog): record which axis closed where --- .../102_axis_ledger.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md new file mode 100644 index 0000000000..a9731c05ca --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -0,0 +1,31 @@ +# 축별 종결 원장 + +폴리싱은 하나의 감사(`100`)와 하나의 적대적 리뷰에서 출발해 세 브랜치에 +수정으로 떨어졌다. goalplan은 축을 work-phase로 쪼개 두었으므로, 각 축이 +어디에서 닫혔는지를 여기 기록한다. + +| 축 | 닫힌 곳 | 증거 | +| --- | --- | --- | +| 부팅 요청 제거 | p4 `4aad8abbf` | `gui/tests/api-targets.test.ts` — standalone 0 fetch (null/standalone/hub), client는 여전히 discovery | +| standalone UI 미렌더 | p4 `4aad8abbf`, p6 `2349d39e8` | GUI 스위트 1207 pass / 0 fail | +| 서버 라우트 폐쇄 | 확인만 (수정 불필요) | `/api/machine/*`는 연결된 클라이언트 리스너 전용, standalone 프록시에 라우트 없음 | +| disconnect 롤백 | p3 `c5420db86` | client-connect 15 pass, codex-journal 25 pass, 둘 다 레드퍼스트 | +| 스택 전파 | p3→p4→p5→p6 | 체인 6엣지 정합, 오염 0 | + +## 서버 축이 수정 없이 닫힌 이유 + +감사 시작 시 가장 걱정한 것이 "standalone 프로세스가 원격 라우트를 연다"였는데, +실측 결과 그렇지 않았다. `/api/machine/*` 핸들러는 +`src/client/machine-listener.ts`에만 있고, 그 리스너는 +`src/client/runtime.ts`가 연결된 클라이언트 롤에서만 띄운다. standalone +프록시의 `src/server/index.ts`에는 그 라우트가 없다. + +`AGENTS.md`의 optional-subsystem 원칙과 같은 모양이다 — 켜지 않으면 코드가 +돌지 않는다. 문제는 서버가 아니라 **클라이언트가 묻는 것**이었다. + +## 남긴 것 + +리뷰가 지적한 두 건은 이번 스코프를 넘어 그대로 둔다: +`/healthz`·`/readyz`·관리 CORS의 프로토콜 메타데이터(롤 게이팅이 프로토콜 +협상을 깨뜨릴 수 있음), disconnect의 비트랜잭션성(복구 상태 기계 신설이 필요). +둘 다 `101`에 이유와 함께 적혀 있다. From 15540b580aae6598baf7f6dbe27b75fe28875011 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 19:59:39 +0900 Subject: [PATCH 26/34] docs(devlog): record the per-axis verification commands --- .../260901_remote_hub_restack/102_axis_ledger.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md index a9731c05ca..cff5d89fbd 100644 --- a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -29,3 +29,16 @@ `/healthz`·`/readyz`·관리 CORS의 프로토콜 메타데이터(롤 게이팅이 프로토콜 협상을 깨뜨릴 수 있음), disconnect의 비트랜잭션성(복구 상태 기계 신설이 필요). 둘 다 `101`에 이유와 함께 적혀 있다. + +## 축별 검증 커맨드 + +각 축을 닫을 때 실제로 돌린 것. 기록해 두면 다음 사람이 같은 주장을 다시 +확인할 때 무엇을 실행해야 하는지 찾을 필요가 없다. + +| 축 | 커맨드 | +| --- | --- | +| 부팅 요청 | `cd gui && bun test tests/api-targets.test.ts` | +| standalone UI | `cd gui && bun test tests/usage-layout.test.ts tests/apikeys-actions.test.tsx tests/connect-pairing.test.ts` | +| 서버 라우트 | `bun test tests/cli-headless-parity.test.ts tests/management-route-registry.test.ts` | +| 롤백 | `bun test tests/client-connect.test.ts tests/codex-journal.test.ts` | +| 체인 | `git merge-base --is-ancestor`를 6개 엣지에 대해 | From b0026ce4292ae4d6e899065bdcca1ef813f8088a Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 20:00:11 +0900 Subject: [PATCH 27/34] docs(devlog): record why the server axis needed no change --- .../260901_remote_hub_restack/102_axis_ledger.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md index cff5d89fbd..1139757aa1 100644 --- a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -42,3 +42,14 @@ | 서버 라우트 | `bun test tests/cli-headless-parity.test.ts tests/management-route-registry.test.ts` | | 롤백 | `bun test tests/client-connect.test.ts tests/codex-journal.test.ts` | | 체인 | `git merge-base --is-ancestor`를 6개 엣지에 대해 | + +## 서버 축 판정 근거 (수정 없음) + +`src/client/machine-listener.ts:49-51`이 `/api/machine/*` 라우트를 정의하고, +`src/client/runtime.ts:70`의 `startMachineListener`가 연결된 클라이언트 +상태에서만 그것을 띄운다. standalone 프록시(`src/server/index.ts`)를 grep하면 +해당 경로가 나오지 않는다 — 라우트가 없으므로 인증된 요청도 일반 관리 디스패처를 +거쳐 404가 된다. + +즉 standalone 사용자의 프로세스는 이 표면을 열지 않는다. 고칠 것이 없어서 +이 축은 확인만으로 닫혔다. From b797a85ec26af4e125f741f9fe7fd279f760f6b5 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 20:00:35 +0900 Subject: [PATCH 28/34] docs(devlog): record why rollback was the heaviest axis --- .../260901_remote_hub_restack/102_axis_ledger.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md index 1139757aa1..8c484c1ff0 100644 --- a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -53,3 +53,16 @@ 즉 standalone 사용자의 프로세스는 이 표면을 열지 않는다. 고칠 것이 없어서 이 축은 확인만으로 닫혔다. + +## 롤백 축이 가장 무거웠던 이유 + +사용자가 요구한 세 가지 중 "다시 로컬로 롤백"이 유일하게 **데이터를 잃을 수 +있는** 축이었다. 노출과 요청은 거슬리는 것이고, 롤백 실패는 복구 불가능하다. + +토큰은 재발급할 수 있고 Codex config는 저널에 원본이 있다. 카탈로그만은 +다른 어디에도 사본이 없다 — connect가 덮어쓰고, disconnect가 지우면 끝이다. +그런데 그 상태에서 CLI는 "native Codex state was restored"를 출력했다. + +수정 후에는 connect가 원본을 연결 상태에 실어두고 disconnect가 되돌려 쓴다. +두 결과(`restored` / `removed`)를 구분해 반환하므로, "복구했다"와 +"원래 없었으니 지웠다"가 같은 신호로 뭉뚱그려지지 않는다. From b7dfb6859a6886136ce4967e5e225c145ce66b78 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 20:01:02 +0900 Subject: [PATCH 29/34] docs(devlog): record the post-polish stack state --- .../102_axis_ledger.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md index 8c484c1ff0..c7758b0510 100644 --- a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -66,3 +66,21 @@ 수정 후에는 connect가 원본을 연결 상태에 실어두고 disconnect가 되돌려 쓴다. 두 결과(`restored` / `removed`)를 구분해 반환하므로, "복구했다"와 "원래 없었으니 지웠다"가 같은 신호로 뭉뚱그려지지 않는다. + +## 폴리싱 후 최종 상태 + +| PR | head | CI | +| --- | --- | --- | +| #2771 | `36992baa9` | 그린 | +| #2772 | `07d7f1006` | 그린 | +| #2776 | `2b36ad496` | 그린 (스크린샷 대기) | +| #2777 | `c5420db86` | 그린 | +| #2781 | `4aad8abbf` | 그린 | +| #2786 | `072cc29c3` | 그린 | +| #2789 | `2349d39e8` | 그린 (스크린샷 대기) | + +분리 PR: **#3147**(dev의 websocket flake 근본 수정, 그린), +**#3149**(로드맵 유닛, 그린). + +실질 CI 실패는 0건이다. 남은 `enforce-target` 2건은 GUI 변경에 대한 UI +스크린샷 요구로, 사람이 캡처해 PR 설명에 붙여야 한다. From 2357a0695d0db1f86058a5404a57fc601991e50e Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 22:09:10 +0900 Subject: [PATCH 30/34] docs(devlog): plan the stack merge train and the #3147 prerequisite --- .../110_merge_train_plan.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md diff --git a/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md b/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md new file mode 100644 index 0000000000..6d97a66faf --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md @@ -0,0 +1,60 @@ +# 110 — 스택 머지 트레인 계획 (wp1: #3147 플레이크 수정 선행) + +## 왜 #3147이 먼저인가 + +리모트 허브 스택의 CI 실패는 스택 자체 결함이 아니다. #2777 / #2781 / #2789의 +`macos` 잡이 실패하고 그 때문에 집계 잡 `ci`가 실패하는데, 실패 테스트는 셋 다 +동일한 한 건이다: + + (fail) server local API auth > websocket passthrough refreshes pool auth for each response.create turn + +17037 pass / 1 fail. 이 테스트는 `dev`가 이미 가진 플레이크이고, 스택 브랜치가 +그것을 상속했을 뿐이다. 스택을 리베이스해도 같은 플레이크를 다시 상속하므로, +수정본인 #3147을 `dev`에 먼저 넣고 그 위로 리베이스하는 순서가 유일하게 +수렴하는 순서다. + +## #3147에 열려 있는 P1 + +리뷰어(Ingwannu)가 exact head `ecf51c67`에 CHANGES_REQUESTED를 걸었고, 지적은 +타당하다. 문제의 diff는 선행 테스트 +`expired thread affinity returns 409 for an idle-expired thread`에서 +`updateAccountQuota("pool-a", 10, 5)`를 함께 지웠다. 그 시드는 웹소켓 테스트의 +것이 아니라 선행 테스트의 것이다. + +지워졌을 때 무슨 일이 벌어지는지가 핵심이다. `startServer`는 동기 반환하지만 +비동기 pool-quota prime을 무장하고, 그 prime은 +`Date.now() - quota.updatedAt >= POOL_CACHE_TTL`로 staleness를 판단한다 +(`src/codex/auth-api.ts`). 캐시된 quota가 아예 없으면 prime은 stale로 보고 +리다이렉트된 canonical 업스트림으로 WHAM 요청을 한 건 더 보낸다. 그 요청이 +타이밍 창을 이기면 `expect(upstreamRequests).toBe(3)`이 4를 보고 깨진다. +지금 매트릭스가 초록이라는 사실은 레이스가 닫혔다는 증거가 아니다 — 레이스는 +정의상 가끔 진다. + +덤으로 그 위 주석이 존재하지 않는 코드를 가리킨다. "`updateAccountQuota` above +stamped `updatedAt` with the REAL clock"이라고 쓰여 있는데 above에 그 호출이 +없다. 주석이 거짓말하는 상태로 머지할 수는 없다. + +## 수정 내용 + +선행 affinity 테스트에서: + +1. `Date.now = () => now` 바로 다음, `startServer(0)` 이전에 + `updateAccountQuota("pool-a", 10, 5)`를 복원한다. 핀 이전이 아니라 이후여야 + 하는 이유는 이 PR의 본래 논지와 같다 — 핀 이전에 쓰면 `updatedAt`이 실제 + 시각으로 찍혀 2027년 가짜 시계에서 몇 달치 캐시 나이로 보인다. +2. 주석을 실제 코드에 맞춘다. "above"가 아니라 "핀 이후에 시드하므로 prime이 + 신선하다고 판단한다"가 참인 문장이다. + +웹소켓 테스트 쪽 변경(자격증명 + quota를 핀 이후로 이동)은 그대로 둔다. 그것이 +이 PR이 고치려는 플레이크 본체다. + +## 검증 경계 + +로컬 전체 스위트는 금지되어 있다. 검증은 exact-head 원격 CI로 한다. 푸시는 +`--no-verify`. `dev`/`main`/`preview` 직접 푸시는 룰셋으로 막혀 있고 시도하지 +않는다 — 모든 랜딩은 PR 머지 경로다. + +## 이후 순서 + +wp2 #3143(리뷰어의 중복본) 클로즈 + #3149 머지. wp3 허브 6개 브랜치를 새 `dev` +위로 리베이스. wp4 #2771부터 순차 재타겟 + 머지. wp5 최종 검증. From e1022b74ca49ef7ba17b5eed2c2e21e14c534d48 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 22:21:55 +0900 Subject: [PATCH 31/34] docs(devlog): correct the merge train plan after the audit refuted the P1 --- .../110_merge_train_plan.md | 131 +++++++++++------- 1 file changed, 78 insertions(+), 53 deletions(-) diff --git a/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md b/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md index 6d97a66faf..bf0ba4147d 100644 --- a/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md +++ b/devlog/_plan/260901_remote_hub_restack/110_merge_train_plan.md @@ -1,60 +1,85 @@ -# 110 — 스택 머지 트레인 계획 (wp1: #3147 플레이크 수정 선행) +# 110 — 스택 머지 트레인 계획 (감사 후 정정본) -## 왜 #3147이 먼저인가 +초판은 A 게이트 감사에서 fail을 받았다. 두 개의 사실 주장이 틀렸고, 아래는 실제 +로그와 코드로 확인한 정정본이다. 틀린 서술을 지우지 않고 무엇이 왜 틀렸는지 +남긴다 — 다음 사람이 같은 추론을 반복하지 않게. -리모트 허브 스택의 CI 실패는 스택 자체 결함이 아니다. #2777 / #2781 / #2789의 -`macos` 잡이 실패하고 그 때문에 집계 잡 `ci`가 실패하는데, 실패 테스트는 셋 다 -동일한 한 건이다: +## 정정 1: 리뷰어 P1의 인과가 뒤집혀 있다 - (fail) server local API auth > websocket passthrough refreshes pool auth for each response.create turn - -17037 pass / 1 fail. 이 테스트는 `dev`가 이미 가진 플레이크이고, 스택 브랜치가 -그것을 상속했을 뿐이다. 스택을 리베이스해도 같은 플레이크를 다시 상속하므로, -수정본인 #3147을 `dev`에 먼저 넣고 그 위로 리베이스하는 순서가 유일하게 -수렴하는 순서다. - -## #3147에 열려 있는 P1 - -리뷰어(Ingwannu)가 exact head `ecf51c67`에 CHANGES_REQUESTED를 걸었고, 지적은 -타당하다. 문제의 diff는 선행 테스트 +#3147에 걸린 CHANGES_REQUESTED는 선행 테스트 `expired thread affinity returns 409 for an idle-expired thread`에서 -`updateAccountQuota("pool-a", 10, 5)`를 함께 지웠다. 그 시드는 웹소켓 테스트의 -것이 아니라 선행 테스트의 것이다. - -지워졌을 때 무슨 일이 벌어지는지가 핵심이다. `startServer`는 동기 반환하지만 -비동기 pool-quota prime을 무장하고, 그 prime은 -`Date.now() - quota.updatedAt >= POOL_CACHE_TTL`로 staleness를 판단한다 -(`src/codex/auth-api.ts`). 캐시된 quota가 아예 없으면 prime은 stale로 보고 -리다이렉트된 canonical 업스트림으로 WHAM 요청을 한 건 더 보낸다. 그 요청이 -타이밍 창을 이기면 `expect(upstreamRequests).toBe(3)`이 4를 보고 깨진다. -지금 매트릭스가 초록이라는 사실은 레이스가 닫혔다는 증거가 아니다 — 레이스는 -정의상 가끔 진다. - -덤으로 그 위 주석이 존재하지 않는 코드를 가리킨다. "`updateAccountQuota` above -stamped `updatedAt` with the REAL clock"이라고 쓰여 있는데 above에 그 호출이 -없다. 주석이 거짓말하는 상태로 머지할 수는 없다. - -## 수정 내용 - -선행 affinity 테스트에서: - -1. `Date.now = () => now` 바로 다음, `startServer(0)` 이전에 - `updateAccountQuota("pool-a", 10, 5)`를 복원한다. 핀 이전이 아니라 이후여야 - 하는 이유는 이 PR의 본래 논지와 같다 — 핀 이전에 쓰면 `updatedAt`이 실제 - 시각으로 찍혀 2027년 가짜 시계에서 몇 달치 캐시 나이로 보인다. -2. 주석을 실제 코드에 맞춘다. "above"가 아니라 "핀 이후에 시드하므로 prime이 - 신선하다고 판단한다"가 참인 문장이다. - -웹소켓 테스트 쪽 변경(자격증명 + quota를 핀 이후로 이동)은 그대로 둔다. 그것이 -이 PR이 고치려는 플레이크 본체다. +`updateAccountQuota("pool-a", 10, 5)`가 삭제되어 startup pool-quota prime이 +WHAM 요청을 한 건 더 보내고 `expect(upstreamRequests).toBe(3)`이 깨진다는 +주장이다. 초판은 이것을 그대로 받아 적었다. 틀렸다. + +`src/codex/auth-api.ts:1332-1335`의 stale 판정은 +`!q || Date.now() - q.updatedAt >= POOL_CACHE_TTL` 이다. `dev` 쪽 코드에서 그 +시드는 시계 핀 **이전**에 실행되므로 `updatedAt`에 실제 시각이 찍힌다. 테스트는 +곧이어 `Date.now`를 `1_800_000_000_000`으로 핀한다. 그 차이는 약 136일이고 +`POOL_CACHE_TTL`은 5분이다. 즉 **시드가 있어도 이미 stale이었다.** 삭제는 +`!q`를 false-but-stale에서 true-and-stale로 바꿀 뿐, 같은 가지로 떨어진다. +prime의 fetch 여부는 삭제 전후가 동일하다. + +두 번째로, 그 fetch는 애초에 카운터에 닿지 못한다. `redirectCanonicalCodexTo` +(`tests/server-auth.test.ts:106-117`)는 `hostname === "chatgpt.com"` 이면서 +`pathname`이 `/backend-api/codex`로 시작하는 것만 로컬 `Bun.serve`로 돌린다. +WHAM은 `/backend-api/wham/usage`다(`src/codex/auth-api.ts:1157`). 리다이렉트를 +타지 않으므로 `upstreamRequests`를 증가시킬 수 없다. prime이 아무리 이겨도 +단언은 4를 볼 수 없다. + +리뷰어가 맞은 부분은 한 고리뿐이다: 자격증명이 시드되어 있으므로 prime은 실제로 +`fetchPoolAccountQuota`까지 간다(`auth-api.ts:1360-1362`, `:1201-1202`은 null +`existing`에 early-return 하지 않는다). 그 고리가 단언까지 이어지지 않을 뿐이다. + +## 정정 2: 주석은 실제로 거짓말한다 — 이게 유일한 유효 지적 + +head `ecf51c67`의 `tests/server-auth.test.ts:2132` 주석은 +"`updateAccountQuota` above stamped `updatedAt` with the REAL clock"이라고 +말하는데 above에 그 호출이 없다. 이건 P1이 아니라 문서 위생 문제다. 고쳐야 하지만 +"레이스를 닫는다"는 명분으로 고치면 안 된다. + +따라서 수정은 하되 근거를 바꾼다: 시계 핀 **이후**에 시드를 복원하면 prime이 +처음으로 진짜 fresh를 보고 조용해지고, 주석도 참이 된다. 개선은 맞다. 레이스 +수정은 아니다. + +## 정정 3: #2789는 #3147로 초록이 되지 않는다 + +초판은 세 브랜치가 같은 한 건으로 실패한다고 썼다. 실제 macOS 로그: + +| PR | 결과 | 실패 테스트 | +|----|------|-------------| +| #2777 | 17037 pass / 1 fail | websocket passthrough refreshes pool auth | +| #2781 | 17049 pass / 1 fail | 위와 동일 | +| #2789 | 17098 pass / **2 fail** | 위 + `ocx launcher graceful shutdown > SIGINT to the launcher tears down the Bun proxy` (20069ms 워치독 타임아웃, `tests/shutdown-launcher.test.ts`) | + +#2789는 별개의 타임아웃 플레이크를 하나 더 가지고 있고, `enforce-target`도 +따로 실패한다. 실패 사유는 wrong_base가 아니다 — 로그에 +"Base codex/remote-hub-p5 matches an open PR head; treating as stacked +(skip wrong_base)"가 찍혀 있고, 실제 사유는 "PR quality gate failed: missing UI +screenshot"다. #2789 본문에 GUI 스크린샷이 없다. #2776도 같은 사유다. + +## 정정 4: 순서는 맞지만 "유일"하지 않다 + +더 싼 대안이 있다: 실패한 macOS 잡 세 개를 재실행하는 것. 플레이크니까 통과할 +확률이 높다. 하지만 그건 내구성이 없다 — 다음 푸시에서 다시 진다. #3147을 +`dev`에 넣는 쪽을 택하는 이유는 "유일해서"가 아니라 **루트에서 고치는 게 +여섯 브랜치를 매번 재실행하는 것보다 내구적이어서**다. + +## 확정 실행 순서 + +1. **wp1** #3147: affinity 테스트에 시드 복원(핀 이후) + 주석 정정. 근거는 + 위생, 레이스 아님. `--no-verify` 푸시 → exact-head CI → 리뷰어에게 인과 + 정정을 회신하고 P1 해소 → admin 머지. +2. **wp2** #3143(리뷰어 중복본) 클로즈, #3149 머지. +3. **wp3** 허브 6개 브랜치를 새 `dev` 위로 리베이스. 현재 전부 `dev` 팁 위에 + 있으므로(behind 0) 실제로는 fast-forward 재적층이다. +4. **wp4** #2771부터 순차 머지. 각 자식은 부모가 랜딩하면 `dev`로 재타겟. + #2776 / #2789의 `enforce-target`은 UI 스크린샷 누락이므로 본문에 스크린샷을 + 넣거나 admin 오버라이드로 넘긴다. #2789의 launcher 타임아웃은 별도 플레이크로 + 재실행 대상. +5. **wp5** `dev` 최종 검증. ## 검증 경계 -로컬 전체 스위트는 금지되어 있다. 검증은 exact-head 원격 CI로 한다. 푸시는 -`--no-verify`. `dev`/`main`/`preview` 직접 푸시는 룰셋으로 막혀 있고 시도하지 -않는다 — 모든 랜딩은 PR 머지 경로다. - -## 이후 순서 - -wp2 #3143(리뷰어의 중복본) 클로즈 + #3149 머지. wp3 허브 6개 브랜치를 새 `dev` -위로 리베이스. wp4 #2771부터 순차 재타겟 + 머지. wp5 최종 검증. +로컬 전체 스위트 금지. 검증은 exact-head 원격 CI. 푸시는 `--no-verify`. +`dev`/`main`/`preview` 직접 푸시 금지 — 모든 랜딩은 PR 머지 경로. From 60d47d40f2747d0aefa0aede6e5f35d17b86212f Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 22:39:14 +0900 Subject: [PATCH 32/34] docs(devlog): record the #3147 seed restore and what it does not fix --- .../111_wp1_3147_outcome.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/111_wp1_3147_outcome.md diff --git a/devlog/_plan/260901_remote_hub_restack/111_wp1_3147_outcome.md b/devlog/_plan/260901_remote_hub_restack/111_wp1_3147_outcome.md new file mode 100644 index 0000000000..9c7643f866 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/111_wp1_3147_outcome.md @@ -0,0 +1,58 @@ +# 111 — wp1 결과: #3147 시드 복원 + +## 무엇을 했나 + +`codex/ws-refresh-quota-seed-flake` 위에 커밋 `0cf5ef7b5`를 올렸다. 선행 테스트 +`expired thread affinity returns 409 for an idle-expired thread`에 +`updateAccountQuota("pool-a", 10, 5)`를 복원하되, 시계 핀 **이후** +(`Date.now = () => now` 다음, `startServer(0)` 이전)에 놓았다. 그리고 존재하지 +않는 호출을 가리키던 주석을 참인 문장으로 바꿨다. + +## 무엇을 하지 않았나 — 이게 더 중요하다 + +이것을 레이스 수정이라고 기록하지 않았다. 감사가 리뷰어의 인과를 반박했고, +반박이 옳았다: + +- `primeCodexPoolQuotas`의 stale 판정은 + `!q || Date.now() - q.updatedAt >= POOL_CACHE_TTL` + (`src/codex/auth-api.ts:1332-1335`)이다. `dev`에서는 시드가 핀 이전에 돌아 + `updatedAt`에 실제 시각이 찍혔고, 테스트는 `Date.now`를 + `1_800_000_000_000`으로 핀한다. 약 136일 대 5분 TTL — **시드가 있어도 이미 + stale이었다.** 삭제는 같은 `||` 가지 안에서 위치만 바꿨다. +- 설령 prime이 fetch를 해도 카운터에 닿지 못한다. + `redirectCanonicalCodexTo`(`tests/server-auth.test.ts:106-117`)는 + `/backend-api/codex` 접두사만 로컬 `Bun.serve`로 돌리는데, prime의 WHAM 호출은 + `/backend-api/wham/usage`(`auth-api.ts:1157`)다. `upstreamRequests`는 3에서 + 움직일 수 없다. + +리뷰어가 맞은 고리는 하나다: 자격증명이 시드되어 있으므로 prime은 실제로 +`fetchPoolAccountQuota`까지 간다(`:1201-1202`은 null `existing`에 early-return +하지 않는다). 그 고리가 단언까지 이어지지 않을 뿐이다. + +그래서 복원의 근거는 두 가지로 남긴다. 주석이 거짓말을 멈춘다는 것, 그리고 핀 +이후 시드가 prime을 **처음으로** 실제 억제한다는 것. "가끔 지는 레이스를 닫았다"가 +아니다. + +## 검증 + +`bun test tests/server-auth.test.ts` — 91 pass / 0 fail / 618 expect calls, +31.43s. 전체 스위트는 돌리지 않았다(금지). + +exact head `0cf5ef7b5`의 원격 매트릭스는 전부 초록이다: macos, test 1~4/4, +gates, storage policy, api usage, keyring ubuntu/windows/macos, hygiene, +react-doctor, enforce-target, ci. FAILURE 0건. 남은 블로커는 리뷰어의 +CHANGES_REQUESTED 하나뿐이고, 인과 정정은 PR 코멘트로 회신했다. + +## 다음 단계로 넘기는 사실 + +#2789는 이 수정으로 초록이 되지 않는다. macOS 잡이 17098 pass / **2 fail**이고 +두 번째는 `ocx launcher graceful shutdown > SIGINT to the launcher tears down the +Bun proxy`의 20069ms 워치독 타임아웃이다(`tests/shutdown-launcher.test.ts`). + +그리고 #2776과 #2789의 `enforce-target` 실패는 base 문제가 아니라 "missing UI +screenshot"이다. 워크플로에 정식 면제 경로가 있다 — +`.github/workflows/enforce-pr-target.yml:259`의 `gui-screenshot-waived` 라벨을 +`MAINTAINERS.md`에 등재된 사람이 붙이면 그 실패만 걷힌다. 다만 #2789는 +`gui/src/pages/ApiKeys.tsx`, `Usage.tsx` 등 실제 화면을 16개 파일 건드리므로 +면제가 아니라 스크린샷이 맞다. #2776이 건드리는 GUI 파일은 `gui/src/api.ts`와 +테스트 2개뿐이라 면제가 타당하다. From 692fabbe1a4a05375a6418290b1bf57956d16bf1 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 22:42:30 +0900 Subject: [PATCH 33/34] docs(devlog): reverse the merge order so the T20 fix lands before its writeup --- .../112_wp2_order_reversal.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 devlog/_plan/260901_remote_hub_restack/112_wp2_order_reversal.md diff --git a/devlog/_plan/260901_remote_hub_restack/112_wp2_order_reversal.md b/devlog/_plan/260901_remote_hub_restack/112_wp2_order_reversal.md new file mode 100644 index 0000000000..0996777df0 --- /dev/null +++ b/devlog/_plan/260901_remote_hub_restack/112_wp2_order_reversal.md @@ -0,0 +1,51 @@ +# 112 — wp2에서 순서를 뒤집은 이유 + +계획은 "#3149 문서 PR을 먼저 머지하고 스택을 나중에"였다. 뒤집었다. 이유는 +#3149에 걸린 리뷰 지적 4번이고, 확인해보니 맞았다. + +## 무엇이 문제인가 + +`AGENTS.md`의 보안 작업 규정은 명시적이다: **미수정 결함의 분석은 추적 +디렉터리가 아니라 스크래치에 있어야 한다.** 판정 기준도 적혀 있다 — "이미 이 +약점을 드러내는 공개 diff가 있는가?" 수정이 배포됐으면 공개해도 새로 드러나는 +게 없으니 `_fin/`에 들어간다. 아직이면 그건 사전 공개 자료다. + +#3149가 담고 있는 것: + +- `003_review_thread_ledger.md:41` — T20: `src/server/index.ts:1684`에서 + `Content-Length` 생략 또는 chunked 시 `declaredLength`가 0이 되어 미인증 + 호출자가 무제한 버퍼링을 유발한다. 재현 조건까지 적혀 있는 미인증 DoS다. +- `030_wp3_p2_remote_session.md:41-45` — 같은 내용을 더 자세히. +- 그 외 P1 6건(T1, T20, T22, T25, T26, T31)의 위치와 성격. + +그리고 `dev`의 `src/server/index.ts`에는 `declaredLength`가 **없다.** 수정은 +`codex/remote-hub-p2` 브랜치의 `b7282858b` +"fix(remote-gui): drop plaintext pairing and bound the unauthenticated exchange +body"에 들어 있고, 그 브랜치는 아직 머지되지 않았다. + +즉 계획대로 #3149를 먼저 머지하면, 수정이 없는 상태의 취약점 재현 조건을 +공개 저장소 기본 브랜치에 올리게 된다. 정확히 규정이 막는 행위다. 게다가 +히스토리는 사후에 걷어내기가 실질적으로 불가능하다. + +## 어떻게 바꿨나 + +스택을 먼저 머지한다. `#2771 → #2772 → #2776 → ... → #2789`가 `dev`에 들어가면 +`b7282858b`도 함께 들어가고, 그 시점에 T20은 "공개 diff가 이미 드러낸 약점"이 +된다. 그 다음에야 #3149의 서술이 사전 공개가 아니라 사후 기록이 된다. + +goalplan에 `wp2b`를 추가해 `wp4`(스택 머지)에 의존시켰다. 원래의 `wp2`는 +#3143 정리만 남긴다(완료). + +## 남은 #3149 지적 3건 + +순서와 무관하게 고쳐야 한다. 스택 머지 후 `wp2b`에서 처리한다. + +1. `081_wp8_ci_repairs.md:29-32` — "기본 매개변수는 모듈 평가 시점의 전역을 + 묶는다"는 **틀렸다.** 기본값 초기화식은 호출 시점에 평가된다. 관찰된 실패의 + 실제 원인은 `window`/`globalThis` 렐름 분리이거나 래퍼 설치 시점 문제다. + 틀린 인과를 히스토리로 보존할 수는 없다. +2. `090_outcome.md`와 `102_axis_ledger.md`가 스택을 "그린"이라 부른다. 문서를 + 쓴 시점에는 참이었을지 몰라도 지금 exact head 기준으로 #2781/#2789는 + 빨갛다. 포커스 검사 통과를 required CI 통과와 같게 적으면 안 된다. +3. `260901_merge_train_round3/061_wp7_outcome.md`와 `070_outcome.md`는 이 + 유닛 범위가 아니다. 분리하거나 뺀다. From 9d59ce57c3cc8600d3abca249a9190bfed7c3803 Mon Sep 17 00:00:00 2001 From: jun Date: Tue, 1 Sep 2026 23:35:06 +0900 Subject: [PATCH 34/34] docs(devlog): correct the fetch-binding claim, replace snapshot greens with merge results, drop out-of-scope files --- .../061_wp7_outcome.md | 57 ------------- .../260901_merge_train_round3/070_outcome.md | 81 ------------------- .../003_review_thread_ledger.md | 10 +++ .../081_wp8_ci_repairs.md | 15 +++- .../260901_remote_hub_restack/090_outcome.md | 46 ++++++++--- .../102_axis_ledger.md | 38 +++++---- 6 files changed, 77 insertions(+), 170 deletions(-) delete mode 100644 devlog/_plan/260901_merge_train_round3/061_wp7_outcome.md delete mode 100644 devlog/_plan/260901_merge_train_round3/070_outcome.md diff --git a/devlog/_plan/260901_merge_train_round3/061_wp7_outcome.md b/devlog/_plan/260901_merge_train_round3/061_wp7_outcome.md deleted file mode 100644 index 888dd4013b..0000000000 --- a/devlog/_plan/260901_merge_train_round3/061_wp7_outcome.md +++ /dev/null @@ -1,57 +0,0 @@ -# 061 — wp7 outcome: the flake is fixed, and the work-phase numbering is not - -`c8c8dc338` — `test(auth): close the startup-prime window that rotates the credential -mid-fixture (#3139)`. Merged with the roadmap unit in the same PR. - -## Result - -``` -gh pr checks 3139 - macos pass 11m40s - ci pass 4s -``` - -That is the verifier that matters. The same assertion failed on `macos` once for #3133 and -twice for #3137, on heads without the fix. It passed on the **first** run of the fixed head. - -Local: `bun test tests/server-auth.test.ts` -> 91 pass / 0 fail / 618 expect(). - -## Where wp7's work actually happened - -In wp6, not wp7. The FSM's active work-phase was wp6 when the fix was written, and wp6 was -the landing phase blocked by exactly this flake — so its plan absorbed the fix rather than -the two units pretending to be independent. - -Recording that plainly instead of back-dating an attest: wp7 was registered as a -work-phase, its plan doc (`060`) is real and was written under it, and its implementation -rode wp6's cycle. The ledger shows one cycle, which is what happened. - -## What this phase is really a record of - -Three explanations, two wrong, one measured — the table is in `060`. Both wrong ones were -plausible, cited real mechanisms, and would have justified the same one-line fix. That is -what made them dangerous rather than harmless: the fix would have worked, the reasoning -would have been wrong, and the next person to touch this fixture would have inherited the -wrong model. - -What broke the tie was the runtime's own counter: - -``` -$ OPENCODEX_DEBUG_QUOTA=1 bun test ... -t "websocket passthrough refreshes pool auth" -[codex-quota] prime done (reason=startup, pool=1, refreshed=1) -``` - -`refreshed=1` on five runs of the unfixed tree **and** five of the fixed one. Staleness never -varied, so the "cache age crosses the TTL" story was dead — and the surviving explanation is -that the prime always fetches, and what varied was whether it hit the stubbed `fetch` and -pinned clock or the real ones. - -`LOOP-MECHANISM-PROOF-01` asks for activation evidence before adopting a mechanism. Here it -did more than confirm: it killed the hypothesis I had already written into a devlog document -and two PR comments. - -## Residual - -The comments on #3109 and #3112 quote the first wrong explanation. They were left in place — -their operational advice (rerun rather than read a single red as a regression) was correct, -and is now moot because the flake is fixed. `051` carries the pointer to the correction. diff --git a/devlog/_plan/260901_merge_train_round3/070_outcome.md b/devlog/_plan/260901_merge_train_round3/070_outcome.md deleted file mode 100644 index 603b2ac2d7..0000000000 --- a/devlog/_plan/260901_merge_train_round3/070_outcome.md +++ /dev/null @@ -1,81 +0,0 @@ -# 070 — outcome: merge train round 3 - -Terminal outcome: **DONE**. Every item in the round-3 scope reached a terminal state. - -## What landed on `dev` - -| commit | what | origin | -| --- | --- | --- | -| `abcda8e13` | 2026-08-31 non-priority-70 bug triage record | #3114 | -| `0dc01cdaa` | canonical fake-IP addresses on provider PATCH | #3122 via #3133 | -| `b14b741dc` | Windows cold-start budget + code-page scheduler paths | #3104 via #3134 | -| `c8c8dc338` | startup-prime window fix + this roadmap unit | #3139 | -| `58be3c5bb` | probe for a free pid instead of assuming 4242 is dead | #3042 via #3137 | - -## Closed - -Issues #3009, #3064. Pull requests #3104, #3122, #3042, #3077, and a credit comment on the -already-closed #3067. Every closure names the merged commit and what changed from the -original; none is a bare "superseded". - -#3039 was closed by its own author at `2026-09-01T04:17:43Z`, not by this train. The comment -recording which contribution #3104 did **not** carry — the elapsed-time diagnostic, replaced -by the configured budget — landed anyway, so the residual is findable. - -## Rebased, not merged - -#3109 to `b3b502045` (five commits; `926a8d8c` dropped because it had already landed as -#3128) and #3112 to `f3c4e9f75` (four commits). Both `range-diff`-identical. Neither PR's -review blockers were touched — #3112's three credential-path findings still stand and it -still needs a fresh security review. - -## Untouched, deliberately - -#3117 reverses a direction `b46164e78` pinned one day earlier and is a policy decision about -#1690, not a mechanical. #3061 has a substantive rebuttal on record. Both were named OUT at -wp0 and stayed out. - -## What the round is actually evidence of - -**Three wrong explanations, caught by measurement rather than review.** The websocket flake -was explained three times: a 60 s skew margin (wrong — the margin is months), a cache age -crossing a TTL (wrong — `refreshed=1` on every run of both trees), and finally the measured -one. Both wrong versions were plausible, cited real code, and **would have justified the same -fix**. That is what made them worth catching: the fix would have worked and the reasoning -would have been wrong, which is how a fixture acquires folklore. - -`LOOP-MECHANISM-PROOF-01` is why it was caught. Asking for activation evidence before -adopting a mechanism killed a hypothesis already written into a devlog document and two PR -comments. - -**A citation can be worse than silence.** "#3128 fixed that flake" was repeated across three -PRs and a release-train record. It was false — #3128 is an ancestor of every head that failed -afterwards — and its effect was to teach reviewers to dismiss a red. The correction is now on -#3109, #3112, #3104, and in `051` and `060`. - -**A plan audit that returns FAIL is cheap.** Round 1 returned five blockers; three were -folded and changed the train's shape — fork PRs became cherry-pick carries once -`enforce-pr-target.yml:740-746` was read properly, and #3039's closure was withdrawn. Two -were rebutted with evidence. The audit cost one subagent and prevented stranding two -contributor PRs in draft. - -**The test suite commits into the developer's checkout.** -`tests/test-runner.test.ts` calls `commitFixture(cwd, ...)`, which makes a real commit in -whatever worktree runs the suite. It rode along on the first push of two carry branches -(author `OpenCodex Test `, adding `base.txt`) and both had to be -reset and force-pushed. Not fixed here — it is a real trap and belongs to its own unit. - -## Verification summary - -| check | result | -| --- | --- | -| `bun test tests/service.test.ts` (#3134 carry) | 191 pass / 0 fail | -| `bun test tests/management-provider-validation.test.ts tests/destination-policy-resolved.test.ts` (#3133 carry) | 129 pass / 0 fail | -| `bun test tests/responses-state.test.ts tests/doctor.test.ts tests/cli-status-json.test.ts` (#3137 carry) | 214 pass / 0 fail | -| `bun test tests/server-auth.test.ts` (flake fix) | 91 pass / 0 fail | -| exact-head CI on #3133, #3134, #3137, #3139 | fully green before each merge | - -Every merge used `--admin`, because GitHub refuses self-approval and `dev` requires a -reviewed PR. That is a real gap and worth stating rather than burying: what stood in for -review was an independent security lane on #3122, direct maintainer audits on the rest, and -a green exact-head matrix on all four. diff --git a/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md index 597af9605f..1185f2ae0f 100644 --- a/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/003_review_thread_ledger.md @@ -3,6 +3,16 @@ `gh api graphql`로 `isResolved=false` 스레드를 전수 조회했다(2026-09-01). 각 건에 소유 단계를 배정한다. 소유 단계 = 그 결함을 처음 도입한 단계. +> **공개 시점에 관한 기록.** 이 문서는 스택이 `dev`에 머지된 뒤에 공개됐다. +> 여기 적힌 P1들은 전부 소유 단계에서 수정된 뒤 그 수정과 함께 랜딩했으므로, +> 이 원장은 미수정 결함의 사전 공개가 아니라 이미 공개 diff가 드러낸 것의 +> 사후 기록이다. `AGENTS.md`의 판정 기준("이미 이 약점을 드러내는 공개 diff가 +> 있는가")을 그대로 적용한 결과다. 특히 T20(미인증 바디 버퍼링)의 수정은 +> `b7282858b`로 #2776(`39e5aefb6`)에 실려 들어갔고, `dev`의 +> `src/server/index.ts`에서 `declaredLength` 하드 캡으로 확인된다. 수정 전에 +> 이 문서를 머지했다면 규정 위반이었다 — 실제로 그 순서로 계획했다가 리뷰 +> 지적을 받고 뒤집었다(`112_wp2_order_reversal.md`). + ## #2771 design — 18건 대부분 CodeRabbit의 마크다운 린트(MD018/MD022, 테이블 파이프 이스케이프)와 diff --git a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md index e86dd03044..5217409033 100644 --- a/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md +++ b/devlog/_plan/260901_remote_hub_restack/081_wp8_ci_repairs.md @@ -27,9 +27,18 @@ draft를 유지시켰다. 게이트가 옳게 동작한 것이므로 문구를 ## 3. `gates` — 릴레이 pairing이 인증 없이 나간다 (p4 소유) `submitConnectPairing`이 `fetchImpl: typeof fetch = fetch`를 받았다. -기본 매개변수는 **모듈 평가 시점의** 전역을 묶는다 — `installApiAuthFetch`가 -`window.fetch`에 씌우는 래퍼가 아니라 원본이다. 릴레이는 그 래퍼가 붙이는 -머신 세션 헤더를 요구하므로 허브가 교환을 거부했다. 호출 시점에 해석하도록 고쳤다. + +(정정 — 초판의 설명은 틀렸다. "기본 매개변수는 모듈 평가 시점의 전역을 묶는다"고 +썼는데, 기본값 초기화식은 **호출 시점에** 평가된다. 스펙이 그렇고, 이 문서가 +반대로 적어두면 다음 사람이 잘못된 모델로 디버깅한다.) + +실제 실패는 바인딩 시점이 아니라 **어느 전역을 보느냐**의 문제였다. 테스트 +환경에서 happy-dom의 `window`와 Bun의 `globalThis`가 갈리고, +`installApiAuthFetch`는 `window.fetch`에 래퍼를 씌운다. 호출 시점에 평가된 +맨 `fetch`가 그 래퍼가 아닌 다른 실체로 해석될 수 있고, 래퍼 설치보다 모듈 +참조가 먼저 굳는 경로도 있다. 릴레이는 래퍼가 붙이는 머신 세션 헤더를 +요구하므로 허브가 교환을 거부했다. 호출 시점에 `window.fetch`를 명시적으로 +집어오도록 고쳤다. ## 4. `gates` — happy-dom에 없는 prompt (p2 소유) diff --git a/devlog/_plan/260901_remote_hub_restack/090_outcome.md b/devlog/_plan/260901_remote_hub_restack/090_outcome.md index a6f80eaff2..124e88e2b6 100644 --- a/devlog/_plan/260901_remote_hub_restack/090_outcome.md +++ b/devlog/_plan/260901_remote_hub_restack/090_outcome.md @@ -1,18 +1,38 @@ # 결과 — remote hub 스택 재스택 -## 상태 - -7단계 전부 현재 `dev` 위로 재스택되어 푸시됐고, 실질 CI 실패는 0건이다. - -| PR | 브랜치 | head | CI | -| --- | --- | --- | --- | -| #2771 | design | `36992baa9` | 그린 | -| #2772 | p1 | `07d7f1006` | 그린 | -| #2776 | p2 | `2b36ad496` | 그린 (스크린샷 대기) | -| #2777 | p3 | `38c361362` | 그린 | -| #2781 | p4 | `158424f05` | 그린 | -| #2786 | p5 | `ff3ce26bd` | 그린 | -| #2789 | p6 | `4ab13b463` | 그린 (스크린샷 대기) | +## 상태 — 최종 (2026-09-01) + +7단계 전부 `dev`에 머지됐다. + +| PR | 브랜치 | 머지 커밋 | +| --- | --- | --- | +| #2771 | design | `278fd613a` | +| #2772 | p1 | `87459f8c3` | +| #2776 | p2 | `39e5aefb6` | +| #2777 | p3 | `fd8b6b895` | +| #2781 | p4 | `163feb6ee` | +| #2786 | p5 | `6d732d3dc` | +| #2789 | p6 | `9232df0e6` | + +### 이 문서가 한 번 틀렸던 것 + +초판은 위 표를 "그린"으로 채웠다. 그 시점의 스냅샷으로는 맞았을지 몰라도, +리뷰 시점의 exact head에서는 #2781과 #2789가 빨갰다. 포커스 검사 통과를 +required CI 통과와 같은 칸에 적은 것이 문제였다 — 둘은 다른 주장이다. + +머지 직전 실제로 겪은 실패는 셋이고 전부 코드 회귀가 아니었다: + +- `tests/server-auth.test.ts`의 websocket refresh 플레이크 — `dev`가 소유한 + 결함. #3147(`408652698`)로 루트에서 고치고 그 위로 재스택했다. +- `Responses previous_response_id state > shutdown drain cap expiry enters the + synchronous spill fallback` — 스택이 건드리지 않는 파일의 부하성 플레이크. + 재실행으로 통과. +- `keyring-smoke=abandoned` — 러너 중단. 집계 잡 `ci`가 이것 때문에 빨갛게 + 보였다. 재실행으로 통과. + +#2776의 스크린샷 게이트는 `gui-screenshot-waived` 라벨로 면제했다(GUI 변경이 +`gui/src/api.ts`와 테스트 2개뿐이라 렌더 변화가 없다). #2789는 면제하지 않고 +실제 스크린샷을 붙였다 — 키 교체 UI는 진짜 화면 변경이다. 체인 6개 엣지 전부 부모가 자식의 조상이고, PR base ref도 같은 부모를 가리킨다. 오염 커밋 0건. 원본 64커밋 전부 authorship과 메시지를 보존했고, 계약 변경은 diff --git a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md index c7758b0510..52625ce401 100644 --- a/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md +++ b/devlog/_plan/260901_remote_hub_restack/102_axis_ledger.md @@ -67,20 +67,26 @@ 두 결과(`restored` / `removed`)를 구분해 반환하므로, "복구했다"와 "원래 없었으니 지웠다"가 같은 신호로 뭉뚱그려지지 않는다. -## 폴리싱 후 최종 상태 +## 최종 상태 (2026-09-01, 머지 완료) -| PR | head | CI | -| --- | --- | --- | -| #2771 | `36992baa9` | 그린 | -| #2772 | `07d7f1006` | 그린 | -| #2776 | `2b36ad496` | 그린 (스크린샷 대기) | -| #2777 | `c5420db86` | 그린 | -| #2781 | `4aad8abbf` | 그린 | -| #2786 | `072cc29c3` | 그린 | -| #2789 | `2349d39e8` | 그린 (스크린샷 대기) | - -분리 PR: **#3147**(dev의 websocket flake 근본 수정, 그린), -**#3149**(로드맵 유닛, 그린). - -실질 CI 실패는 0건이다. 남은 `enforce-target` 2건은 GUI 변경에 대한 UI -스크린샷 요구로, 사람이 캡처해 PR 설명에 붙여야 한다. +위 표는 초판에서 폴리싱 시점 head를 "그린"으로 적었다. 그건 그 스냅샷의 +주장이었고, 리뷰 시점 exact head에서는 #2781과 #2789가 빨갰다. 지금은 +스냅샷이 아니라 머지 결과를 적는다. + +| PR | 머지 커밋 | +| --- | --- | +| #2771 | `278fd613a` | +| #2772 | `87459f8c3` | +| #2776 | `39e5aefb6` | +| #2777 | `fd8b6b895` | +| #2781 | `163feb6ee` | +| #2786 | `6d732d3dc` | +| #2789 | `9232df0e6` | + +분리 PR: **#3147** — `dev`의 websocket flake 근본 수정, `408652698`로 머지. +**#3149** — 이 로드맵 유닛. + +`enforce-target` 2건은 스크린샷 요구였고 서로 다르게 닫혔다. #2776은 +`gui-screenshot-waived` 라벨로 면제했다(`gui/src/api.ts` + 테스트 2개, 렌더 +변화 없음). #2789는 면제 대상이 아니어서 키 교체 UI를 실제로 띄워 캡처하고 +PR 설명에 붙였다.