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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ gui/src/fetch-json.ts:28-43의 error boundary를 따른다.
| candidate/manual alias probe | POST /api/link/probe | { alias } | 응답 { alias, fingerprint, keyType }을 confirmation view에 고정. ocxVersion은 사용자가 지문을 확인한 뒤 POST /api/link/confirm-host 응답 { alias, fingerprint, ocxVersion }에서만 받아 표시 |
| fingerprint confirm | POST /api/link/confirm-host | { alias, fingerprint } | 확인된 alias를 apply 단계로 전환 |
| apply | POST /api/link/apply | { alias } | 즉시 status polling; 성공 toast와 connected row |
| disconnect | DELETE /api/link/{id} | 없음 | status 재조회; 204/JSON 양쪽의 성공 envelope을 허용 |
| disconnect | DELETE /api/link/{id} | 없음 | status 재조회; 204/JSON (무효: 감사 반영 Averroes 절 참조) 양쪽의 성공 envelope을 허용 |

확인 전에는 apply를 호출하지 않는다. probe 오류, malformed JSON, 401/403, 409, 5xx는 모두
localized error와 retry path로 매핑한다. HTTP error message를 logic discriminator로 사용하지 않는다.
Expand Down Expand Up @@ -581,13 +581,13 @@ headroom을 계산하지 않고, locale parity와 bun run lint:i18n을 gate로
| fingerprint cancel | confirmation에서 cancel | sheet가 alias step으로 돌아가고 confirm-host/apply 호출 0 |
| confirm-host 성공 | checkbox + confirm | POST body가 { alias, fingerprint }; apply 전 confirmed view 유지 |
| confirm-host 실패 | non-2xx | localized confirm error, apply disabled |
| apply 성공 | apply 201/200 후 status connected | POST { alias }, polling 결과 connected dot/label과 row 생성 |
| apply 성공 | apply 201/200 (무효: 감사 반영 Averroes 절 참조) 후 status connected | POST { alias }, polling 결과 connected dot/label과 row 생성 |
| apply timeout/failure | request reject 또는 failed status | failed label, retry visible, silent fallback 없음 |
| reconnecting | `links[].state = "reconnecting"` | amber label/live region, request action remains disabled or retry-only |
| reconnecting → connected | next poll connected | green label and connected row |
| reconnecting → failed | next poll failed | red label and actionable retry |
| disconnect confirm cancel | confirm dialog negative | DELETE 호출 0, row 유지 |
| disconnect success | confirm + DELETE 204/JSON | DELETE /api/link/{id}, row 제거, last row면 off state |
| disconnect success | confirm + DELETE 204/JSON (무효: 감사 반영 Averroes 절 참조) | DELETE /api/link/{id}, row 제거, last row면 off state |
| disconnect failure | DELETE non-2xx | error notice, row/status 유지 |
| workspace unavailable | App availability false + #remote-workspace | nav entry와 component가 없고 localized unavailable copy만 표시 |
| workspace available | App availability true | #remote-workspace nav row와 existing component가 표시 |
Expand Down Expand Up @@ -761,3 +761,73 @@ routes and does not weaken them”을 명시한다. MAINTAINERS.md:11의 securit
- `role`별 열 개 locale label과 link/listener state별 status dot mapping을 추가했다.
- 기존 실행 역할(`hub`/`client`) wire 가정과 status의 `hostKeyFingerprint`/`ocxVersion`
가정을 제거했다. (Pauli r2 반영) probe 응답은 K16대로 `{alias, fingerprint, keyType}`이고 `ocxVersion`은 confirm-host 응답에서만 받는다.

## wp5 P 재검증 (아키텍트 Descartes, gpt-6-sol high, 2026-09-25) — 이 절이 앞선 내용보다 우선한다

| ID | 제안 | 처분 |
|---|---|---|
| W5-1 | Remote Link 활성 조건은 `sharedSessionReady`만(standalone은 targets.connected=false, api-targets.ts:117-124) | 수용 |
| W5-2 | 권한 표: candidates/probe/confirm-host/apply는 페어링 대시보드 세션만, status·DELETE는 세션 또는 신뢰 루프백 관리자 토큰(link-routes.ts:388-434) | 수용. GUI는 세션 경로만 쓴다 |
| W5-3 | 응답 모양: candidates `{candidates:[...]}`, apply 202 `{linkId}`, DELETE 200 `{linkId}`, `child.state`는 `LinkWireState`(status-projection.ts:19-24) | 수용 |
| W5-4 | App의 가용성 조회가 RemoteWorkspace 3초 폴링(RemoteWorkspace.tsx:87-97)과 겹침 | 결정: App은 원격 워크스페이스 가용성을 마운트와 경로 변경 시 한 번만 조회한다. 3초 폴링은 RemoteWorkspace 화면이 열려 있을 때만 기존대로. Remote Link 화면은 자기 status를 5초 간격으로, 화면이 보일 때만 폴링 |
| W5-5 | gui/tests/locale-parity.test.ts:3이 vi를 빠뜨림 | 수용: 목록을 `LOCALES`에서 파생 |
| W5-6 | 번역된 docs 트리 7개(astro.config.mjs:64-72)에 Remote Workspace 번역본이 있음 | 결정: 영어 원문 + 7개 언어 번역 페이지를 모두 추가하고 사이드바에 등록. 자식 흐름은 wp6 전까지 "준비 중" 문장 |
| W5-7 | #remote → Remote Link, #remote-workspace 분리 | 유지 |
| W5-8 | Switch·Notice·확인 대화상자 재사용, 추가 시트는 네이티브 `<dialog>`(Escape, 포커스 가두기·복원, OAuthTosWarningModal.tsx:34-70 관행) | 수용 |
| W5-9 | 스크린샷 절차 | 결정: `gui/scripts/remote-link-fixture.ts`(Bun 서버: `gui/dist` 정적 제공 + 고정 `/api/link/*`·세션·워크스페이스 응답, 상태별 쿼리 스위치 off/role/home-connected/add-sheet/fingerprint)를 만들고, agbrowse로 1440×900과 390×844를 찍는다. 이미지는 저장소 브랜치에 커밋하지 않고 `pr-assets` 브랜치에 올려 커밋 SHA로 링크(AGENTS.md 규칙). 픽스처 스크립트는 저장소에 남겨 재현 가능하게 한다 |

- 반영 확인(Descartes MISALIGNED 2건):
- W5-4 확정: App의 효과는 `useEffect(..., [page, sharedSessionReady, sharedBase])`. `sharedSessionReady`가 false면 조회하지 않고 원격 워크스페이스 내비 항목을 숨긴다. 조회 실패도 숨김(가용성 불명 = 비노출). 페이지 이동마다 한 번, 중복 요청은 진행 중 요청을 재사용.
- W5-9 확정: 파일 변경 지도에 NEW `gui/scripts/remote-link-fixture.ts` 추가. 계약: `bun gui/scripts/remote-link-fixture.ts --port <n>`이 `gui/dist`를 제공하고, `GET /opencodex-session`·`/api/remote-workspace/status (무효: 감사 반영 Averroes 절 참조)`·`/api/link/status`·`/api/link/candidates`·`POST /api/link/probe`·`/api/link/confirm-host`·`/api/link/apply`에 고정 JSON을 돌려준다. 상태는 URL 쿼리 `?fixture=off|role|home-connected|add-sheet|fingerprint`로 고른다(서버는 Referer 쿼리 또는 쿠키 `ocx-fixture`로 판별). 재현 명령: `cd gui && bun run build && bun scripts/remote-link-fixture.ts --port 5199` 후 agbrowse로 각 상태를 1440×900, 390×844로 캡처. 검증 표에 이 명령과 `bun run lint:i18n`, `bun test tests`, `bun run lint`, `bun run build`, docs-site 빌드(`cd docs-site && bun run build`)를 추가한다.
- 실행 증거는 B/C 단계에서 만든다(계획 단계 문서는 결정 기록).

## 감사 반영 (Averroes FAIL r1, wp5 계획 감사) — 이 절이 앞선 모든 내용보다 우선한다

1. `#remote` 호환(반박 + 대안): 사용자가 `#remote`에서 링크 화면을 원한다고 명시했으므로(PRD 문제 정의) `#remote`는 Remote Link로 바꾼다. 옛 북마크 대책: 원격 워크스페이스가 사용 가능한 설치(App 가용성 조회가 true)에서는 Remote Link 화면 맨 위에 "원격 워크스페이스는 이제 별도 화면에 있어요 → 열기"(`#remote-workspace`) 카드를 항상 보여 준다. 테스트: 가용성 true면 카드와 링크가 렌더링되고 클릭 시 hash가 `#remote-workspace`, false면 카드 없음. `#remote-workspace` 직접 진입은 가용성과 무관하게 기존 RemoteWorkspace 화면(비활성 안내 포함)을 연다.
2. 오류 코드: `gui/src/remote-link-api.ts`(NEW)에 `readLinkJson<T>(res): Promise<T>`를 두고, 비정상 응답은 본문 `{error:{code,message}}`에서 code를 꺼내 `LinkApiError(code, status)`로 던진다(기존 `readJsonOrThrow`는 건드리지 않음). code → i18n 키 표: `invalid_body`, `invalid_alias`, `ssh_unreachable`, `probe_failed`, `host_confirmation_expired`, `host_fingerprint_mismatch`, `remote_ocx_missing`, `listener_unavailable`, `key_issue_failed`, `link_apply_failed`, `link_connect_timeout`, `compensation_failed`, `remote_disconnect_failed`, `key_revoke_failed`, `link_remove_failed`, `tailscale_session_refused`, `link_unavailable` + 알 수 없는 코드용 `remoteLink.error.generic`. 구현 시 link-routes.ts에서 실제 code 목록을 `rg -o 'fail\("[a-z_]+"'`로 뽑아 표와 대조하고, 표에 없는 코드가 있으면 추가한다. 테스트: 각 code가 해당 문구를 보여 줌, 알 수 없는 code는 generic.
3. 강제 해제: DELETE가 `remote_disconnect_failed`(502)면 두 번째 확인 대화상자("기기에 연결할 수 없어요. 이 컴퓨터에서만 링크를 지울까요? 그 기기는 직접 `ocx disconnect` 해야 해요")를 띄우고 확인 시 `DELETE /api/link/{id}` 본문 `{"force":true}`. `compensation_failed` 행은 실패 점(빨강) + 사유 문구 + "링크 지우기" 버튼(같은 DELETE 흐름). 테스트: 502 → 강제 확인 → force 본문 전송, compensation_failed 행 렌더링.
4. 상태 코드 고정: apply 202 `{linkId}`, DELETE 200 `{linkId}`, probe 200, confirm-host 200, candidates 200. 테스트·픽스처 모두 이 값만.
5. 픽스처 엔드포인트: 원격 워크스페이스 가용성은 `GET /api/remote-workspace`(remote-workspace-routes.ts:61). W5-9 표기의 `/api/remote-workspace/status (무효: 감사 반영 Averroes 절 참조)`는 폐기. 파일 변경 지도에 NEW `gui/scripts/remote-link-fixture.ts` 포함.
6. 문서 파일: NEW `docs-site/src/content/docs/guides/remote-link.md`와 7개 번역 `docs-site/src/content/docs/{fr,ko,zh-cn,zh-tw,ru,ja,tr}/guides/remote-link.md`, 그리고 `docs-site/astro.config.mjs` 사이드바의 Guides 그룹에 remote-hub 옆으로 등록(기존 remote-workspace 등록 방식과 같게). 기존 remote-hub/remote-workspace 가이드에서 새 페이지로 한 줄 링크.
7. 비차단 반영: 테스트에 `role: "home", links: [], child: null` 홈 빈 상태와, 꺼짐 스위치·역할 선택만으로는 GET 외 요청이 없음을 요청 기록으로 확인. CSS는 gui/design-system의 토큰(간격·반경·색)만 쓰고 새 원시 값이 필요하면 토큰을 먼저 추가한다.

## 감사 반영 (Averroes FAIL r2)

1. 오류 코드 목록은 구현된 라우트에서 뽑은 것이 전부다(`rg -o 'fail\("[a-z_]+"' src/server/management/link-routes.ts`, 2026-09-25 기준 24개). r1 절 2번의 목록은 이 표로 대체한다(`ssh_unreachable`, `remote_ocx_missing`, `link_connect_timeout`은 존재하지 않으므로 폐기). 구현은 이 목록을 `gui/src/remote-link-api.ts`에 `LINK_ERROR_CODES` 상수로 두고, GUI 테스트가 같은 명령에 해당하는 정규식으로 `src/server/management/link-routes.ts`를 읽어 뽑은 집합과 상수가 같은지 확인한다(서버가 코드를 추가하면 GUI 테스트가 실패).

| code | i18n 키 |
|---|---|
| `admission_timeout` | `remoteLink.error.admission_timeout` |
| `compensation_failed` | `remoteLink.error.compensation_failed` |
| `fingerprint_failed` | `remoteLink.error.fingerprint_failed` |
| `forbidden` | `remoteLink.error.forbidden` |
| `host_confirmation_expired` | `remoteLink.error.host_confirmation_expired` |
| `host_fingerprint_mismatch` | `remoteLink.error.host_fingerprint_mismatch` |
| `host_not_confirmed` | `remoteLink.error.host_not_confirmed` |
| `invalid_alias` | `remoteLink.error.invalid_alias` |
| `invalid_body` | `remoteLink.error.invalid_body` |
| `invalid_link_id` | `remoteLink.error.invalid_link_id` |
| `key_issue_failed` | `remoteLink.error.key_issue_failed` |
| `key_revoke_failed` | `remoteLink.error.key_revoke_failed` |
| `link_apply_failed` | `remoteLink.error.link_apply_failed` |
| `link_exists` | `remoteLink.error.link_exists` |
| `link_not_found` | `remoteLink.error.link_not_found` |
| `link_remove_failed` | `remoteLink.error.link_remove_failed` |
| `link_unavailable` | `remoteLink.error.link_unavailable` |
| `listener_unavailable` | `remoteLink.error.listener_unavailable` |
| `probe_failed` | `remoteLink.error.probe_failed` |
| `remote_connect_failed` | `remoteLink.error.remote_connect_failed` |
| `remote_disconnect_failed` | `remoteLink.error.remote_disconnect_failed` |
| `remote_port_failed` | `remoteLink.error.remote_port_failed` |
| `tailscale_session_refused` | `remoteLink.error.tailscale_session_refused` |
| `version_probe_failed` | `remoteLink.error.version_probe_failed` |
| (알 수 없는 code) | `remoteLink.error.generic` |

2. 새 문구 키(영어 원문, 10개 로케일 모두 추가, `bun run lint:i18n` 통과). 위 표의 오류 키도 모두 10개 로케일에 추가한다.
- `remoteLink.workspaceMoved.title`: "Remote Workspace has its own page now"
- `remoteLink.workspaceMoved.open`: "Open Remote Workspace"
- `remoteLink.forceRemove.title`: "This machine could not reach {alias}"
- `remoteLink.forceRemove.body`: "Remove the link only on this machine? Afterwards run {cmd} on {alias} yourself." ({cmd}는 `<Trans>`의 `ocx disconnect` 코드 칩)
- `remoteLink.forceRemove.confirm`: "Remove here only"
- `remoteLink.row.removeFailedLink`: "Remove link"
3. 앞 절의 `201/200`, `204/JSON`, `/api/remote-workspace/status` 표기는 본문에서 무효로 표시했다.
1 change: 1 addition & 0 deletions docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default defineConfig({
translations: { fr: "Guides", ko: "가이드", "zh-CN": "指南", "zh-TW": "指南", ru: "Руководства", ja: "ガイド", tr: "Kılavuzlar" },
items: [
{ label: "Remote Hub Deployment", translations: { fr: "Déploiement Remote Hub", ko: "Remote Hub 배포", "zh-CN": "Remote Hub 部署", "zh-TW": "Remote Hub 部署", ru: "Развёртывание Remote Hub", ja: "Remote Hub のデプロイ", tr: "Remote Hub Dağıtımı" }, slug: "guides/remote-hub" },
{ label: "Remote Link", translations: { fr: "Liaison distante", ko: "Remote Link", "zh-CN": "远程链接", "zh-TW": "遠端連結", ru: "Удалённая связь", ja: "リモートリンク", tr: "Uzak Bağlantı" }, slug: "guides/remote-link" },
{ label: "Response Inspection", translations: { fr: "Inspection des réponses et réponses volumineuses", ko: "응답 검사와 대용량 응답", "zh-CN": "响应检查与大型响应", "zh-TW": "回應檢查與大型回應", ru: "Проверка ответов и большие ответы", ja: "レスポンスの検査と大きなレスポンス", tr: "Yanıt incelemesi ve büyük yanıtlar" }, slug: "guides/response-inspection" },
{ label: "Remote Workspace", translations: { fr: "Espace de travail distant", ko: "원격 워크스페이스", "zh-CN": "远程工作区", "zh-TW": "遠端工作區", ru: "Удалённая рабочая область", ja: "リモートワークスペース", tr: "Uzak Çalışma Alanı" }, slug: "guides/remote-workspace" },
{ label: "Providers", translations: { fr: "Fournisseurs", ko: "프로바이더", "zh-CN": "提供商", "zh-TW": "供應商", ru: "Провайдеры", ja: "プロバイダー", tr: "Sağlayıcılar" }, slug: "guides/providers" },
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/fr/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Déploiement Remote Hub
description: Déployer un hub opencodex avec une gestion locale, Tailscale Serve et OAuth sans interface locale.
---

Pour les liaisons SSH entre machines, consultez [Liaison distante](/fr/guides/remote-link/).

Un hub conserve les identifiants fournisseur, le catalogue et l’usage sur un hôte. Les clients authentifiés appellent directement son plan de données. Le plan de gestion est distinct : son écoute facultative reste sur `127.0.0.1` et ne sert que le tableau de bord et `/api/*`. Elle ne sert jamais `/v1/*`, `/healthz`, `/readyz` ni WebSocket. Ne publiez pas le port `10101` et n’utilisez pas Tailscale Funnel.

## Rôles, connexion et sécurité
Expand Down
62 changes: 62 additions & 0 deletions docs-site/src/content/docs/fr/guides/remote-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Liaison distante
description: Connecter un ordinateur OpenCodex Home à un ordinateur Child avec SSH.
---

Une liaison entre machines connecte un ordinateur OpenCodex **Home** à un ordinateur **Child** avec SSH. Home sert le trafic de Child par le tunnel SSH, et les deux ordinateurs gardent leur service OpenCodex local sur le port `10100`. Le tableau de bord transmet la clé de liaison propre à Child par SSH, sans vous demander de saisir un jeton.

## Conditions requises

- Home peut se connecter à Child avec une clé OpenSSH.
- OpenCodex est installé sur Child.
- Les deux ordinateurs utilisent macOS ou Linux.
- Le tableau de bord Home dispose d’une session appairée complète.

SSH par mot de passe, Windows et la liaison initiée par Child ne font pas partie du flux actuel. Le flux initié par Child est **bientôt disponible**.

## Ajouter un Child depuis `#remote`

1. Ouvrez le tableau de bord sur `#remote` et activez Remote Link.
2. Choisissez **Home**.
3. Sélectionnez **Add child**.
4. Choisissez un hôte parmi les candidats SSH, ou saisissez un alias de configuration SSH.
5. Lancez le test de connexion et comparez l’empreinte proposée avec celle de l’ordinateur visé. Cette comparaison aide à détecter un mauvais hôte ou une clé d’hôte modifiée avant que SSH ne lui fasse confiance.
6. Confirmez l’empreinte, puis connectez Child.

Le tableau de bord ne demande pas de saisir un jeton. Il sonde d’abord l’hôte et ne peut appliquer la liaison qu’après votre confirmation explicite de l’empreinte.

## État de la liaison

- **Connected** signifie que le tunnel SSH est prêt et que Child peut utiliser la liaison Home.
- **Reconnecting** signifie que le tunnel est réessayé. Les requêtes peuvent temporairement renvoyer `503` avec `Retry-After`.
- **Failed** signifie que la liaison nécessite une intervention. Vérifiez l’authentification SSH, la clé d’hôte confirmée, la redirection ou le délai indiqué.

Une liaison en échec ne bascule pas silencieusement vers un fournisseur local.

## Supprimer un Child

Sélectionnez **Disconnect** pour Child et confirmez son alias. Home arrête le tunnel, révoque la clé de liaison de Child et supprime l’enregistrement enregistré.

Si Home ne peut pas joindre Child pour exécuter la déconnexion, choisissez **Remove here only**. Cela supprime le tunnel, la clé et l’enregistrement locaux. Connectez-vous ensuite à Child et exécutez :

```bash
ocx disconnect
```

## Sécurité

Child utilise les fournisseurs et les identifiants de fournisseur de l’ordinateur Home via la liaison. Home crée une clé distincte pour chaque Child ; la suppression de la liaison révoque cette clé. Comparez l’empreinte de l’hôte avant de confirmer afin de ne pas accepter par erreur une mauvaise machine ou une clé modifiée. Les sessions du tableau de bord émises depuis une identité Tailscale ne peuvent pas gérer les liaisons.

## Référence CLI

```text
ocx link port [--json]
ocx link issue --alias <alias> --tunnel-port <port> [--json]
ocx link status [--json]
ocx link revoke --link-id <id> [--json]
```

## Guides associés

- [Déploiement Remote Hub](/fr/guides/remote-hub/)
- [Remote Workspace](/fr/guides/remote-workspace/)
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/fr/guides/remote-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Espace de travail distant
description: Conservez Codex, Claude Code, Pi et leurs connexions sur un même OCX Hub, tandis que des ordinateurs équipés seulement d'OCX fournissent l'espace de travail et l'environnement de compilation.
---

Pour les liaisons SSH entre machines, consultez [Liaison distante](/fr/guides/remote-link/).

Remote Workspace permet à un OpenCodex Hub d'exécuter vos agents de programmation tandis qu'un
autre ordinateur fournit les fichiers du projet, les commandes, les tests et la puissance de
calcul. Un téléphone ou un troisième ordinateur peut piloter la session depuis le tableau de
Expand Down
Loading
Loading