Skip to content

feat(link): join a Home from a standalone Child dashboard - #5818

Merged
lidge-jun merged 13 commits into
devfrom
codex/remote-link-6-client-initiated
Sep 25, 2026
Merged

lidge-jun merged 13 commits into
devfrom
codex/remote-link-6-client-initiated

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

A standalone computer can now join a Home from its own dashboard. Before this layer the Child role on #remote was a placeholder, and a link could only start from the Home (ssh -R, #5801–#5807). This is layer L6 of the Remote Link stack and targets #5807.

On a standalone runtime the Child role opens Find Home: pick an SSH host, test the connection, confirm the host-key fingerprint, then Connect as Child. The computer restarts into the client runtime, which owns an ssh -N -L tunnel to the Home's link listener. ocx disconnect on the Child revokes the link on the Home over SSH.

  • Join route. POST /api/link/join {"alias"} accepts only a paired dashboard session on a standalone runtime. A Tailscale identity session gets 403, another role gets 409 standalone_required, and both checks run before link state is read. The sequence is: confirmed host, a local port of 1024 or higher, ocx link issue on the Home over SSH, the client-link.json sidecar (0600), the tunnel, an authenticated /readyz within 15 s, in-process connectClient, then a restart. A failure after the issue stops the tunnel, revokes the Home link, and clears the sidecar only if the revoke succeeded. Otherwise it returns join_rollback_failed with the link id, and the next join retries that revoke first. A restart that cannot be scheduled after the committed connect returns join_restart_failed and keeps the connection.
  • Client tunnel. src/client/link-tunnel.ts supervises the tunnel with the existing reducer and backoff. It stops the tunnel (TERM, 5 s, then KILL) before the listener on every shutdown path, and recycles to standalone once the link ends. A pidfile records the owner pid, so a tunnel is reaped only when its owner is dead: on Linux by exact /proc argv, while elsewhere it is reported as unresolved and never killed. An invalid sidecar fails closed and shows as a failed child with sidecar_invalid. A link client without a sidecar is the Home-initiated case and is unchanged.
  • Disconnect and revoke. ocx disconnect adds homeRevoke and tunnel to --json. When the Home revoke fails it prints Home revoke failed; run ocx link revoke --link-id <linkId> on the home. ocx link revoke is now idempotent: 404 link_not_found exits 0 because removal revokes the key before it deletes the record. The CLI keeps only a validated error code from an API error body, never the message.
  • Port contract. src/link/ports.ts (isLinkPort, 1024–65535) covers the client tunnel port wherever it is accepted. The config schema restates the range, because it sits on every install's core path and may not import link code.
  • GUI. The Child role is disabled with a reason unless the runtime is standalone. Find Home reuses the add sheet. Every request attempt has an identity and an abort signal, so a response that arrives after cancel changes nothing. Joining, failure with Retry, and the restart wait each have strings in all 10 locales.
  • Docs. The 8 remote-link guides replace "coming soon" with the Child flow, and structure/remote-link.md records the contracts above.

Security review is requested: this layer runs SSH commands, carries the issued data key in memory from the issue output into connectClient, and changes ocx link revoke error handling. The key is never logged, returned, written outside the existing client connection config, or placed in argv, and link-join-route.test.ts asserts that for every response and console line.

Screenshots

Standalone role choice, then Find Home with the host confirmed:

역할 선택 (독립형)
홈 찾기, 호스트 확인

A join failure with Retry, then the restart wait after a successful join:

연결 실패
재시작 대기

A Home runtime shows the Child role disabled with its reason; joining on mobile:

Child disabled on a Home
Joining Home, mobile

All 21 captures (en/ko desktop, en mobile) are under pr-assets@df24377da4/260925-remote-link-child, taken with gui/scripts/remote-link-fixture.ts in headless Chrome by clicking through the real flow.

Verification

  • Test receipt at 30ff19b: 34 root files covering every link, disconnect, layout, file-size, headless-parity, structure, core-lab and repo-hygiene suite, with 360 pass and 0 fail. cd gui && bun test tests: 2,367 pass, 0 fail. bun x tsc --noEmit, bun run structure:check, bun run privacy:scan, bun run lint, bun run lint:i18n, and bun run build pass. react-doctor@0.9.11 --scope changed reports no issues.
  • bun run test:changed fails locally in this worktree for environment reasons. L6 and its parent L5 fail the identical 946 test names, so L6 adds no new failure. The full suite is left to CI.
  • New tests: tests/server/link-join-route.test.ts covers the gates, the a→h order, every rollback point, stale-sidecar compensation, and the restart failure. tests/clients/client-link-tunnel.test.ts covers argv, TERM/KILL, the stop trigger within two ticks, sidecar_invalid, and the reap rules. The other new suites are client-link-teardown, client-link-state, and link-ports, plus revoke idempotency in cli-link. GUI tests cover the standalone gate, the exact join body, Retry, stale responses after cancel, and the new error codes. All five new root tests are registered in both layout maps.
  • The plan passed six audit rounds. Independent review of the server and client lanes went FAIL, then PASS after the fixes above; the GUI lane went NEAR-PASS, then fixed.
  • Not verified: a real two-machine SSH round trip. Every SSH, tunnel, and /readyz step runs against fakes. On macOS an orphaned client tunnel is reported, not reaped.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed. (8 docs-site guides, structure/remote-link.md, structure/runtime.md one sentence at its 600-line budget)
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. Maintainer security review is still requested for the SSH join, key handling, and the revoke change.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 25, 2026 04:21
@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 41ac930e-8351-41d2-9b48-b89f6a98a0a7

📥 Commits

Reviewing files that changed from the base of the PR and between 2858ac3 and 9caa83d.

📒 Files selected for processing (139)
  • devlog/_plan/260925_remote_home_child_link/000_prd.md
  • devlog/_plan/260925_remote_home_child_link/001_stack_plan.md
  • devlog/_plan/260925_remote_home_child_link/002_arch_plan.md
  • devlog/_plan/260925_remote_home_child_link/003_decisions.md
  • devlog/_plan/260925_remote_home_child_link/010_wp1_link_core.md
  • devlog/_plan/260925_remote_home_child_link/020_wp2_hub_link_listener.md
  • devlog/_plan/260925_remote_home_child_link/030_wp3_client_link_transport.md
  • devlog/_plan/260925_remote_home_child_link/040_wp4_supervisor_api_cli.md
  • devlog/_plan/260925_remote_home_child_link/050_wp5_remote_gui.md
  • devlog/_plan/260925_remote_home_child_link/060_wp6_client_initiated.md
  • docs-site/astro.config.mjs
  • docs-site/src/content/docs/fr/guides/remote-hub.md
  • docs-site/src/content/docs/fr/guides/remote-link.md
  • docs-site/src/content/docs/fr/guides/remote-workspace.md
  • docs-site/src/content/docs/guides/remote-hub.md
  • docs-site/src/content/docs/guides/remote-link.md
  • docs-site/src/content/docs/guides/remote-workspace.md
  • docs-site/src/content/docs/ja/guides/remote-hub.md
  • docs-site/src/content/docs/ja/guides/remote-link.md
  • docs-site/src/content/docs/ja/guides/remote-workspace.md
  • docs-site/src/content/docs/ko/guides/remote-hub.md
  • docs-site/src/content/docs/ko/guides/remote-link.md
  • docs-site/src/content/docs/ko/guides/remote-workspace.md
  • docs-site/src/content/docs/ru/guides/remote-hub.md
  • docs-site/src/content/docs/ru/guides/remote-link.md
  • docs-site/src/content/docs/ru/guides/remote-workspace.md
  • docs-site/src/content/docs/tr/guides/remote-hub.md
  • docs-site/src/content/docs/tr/guides/remote-link.md
  • docs-site/src/content/docs/tr/guides/remote-workspace.md
  • docs-site/src/content/docs/zh-cn/guides/remote-hub.md
  • docs-site/src/content/docs/zh-cn/guides/remote-link.md
  • docs-site/src/content/docs/zh-cn/guides/remote-workspace.md
  • docs-site/src/content/docs/zh-tw/guides/remote-hub.md
  • docs-site/src/content/docs/zh-tw/guides/remote-link.md
  • docs-site/src/content/docs/zh-tw/guides/remote-workspace.md
  • gui/scripts/remote-link-fixture.ts
  • gui/src/App.tsx
  • gui/src/api-targets.ts
  • gui/src/app-routing.ts
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/fr.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/tr.ts
  • gui/src/i18n/vi.ts
  • gui/src/i18n/zh-TW.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/RemoteLink.tsx
  • gui/src/remote-link-api.ts
  • gui/src/styles-remote-link.css
  • gui/tests/locale-parity.test.ts
  • gui/tests/remote-link-route.test.tsx
  • gui/tests/remote-link.test.tsx
  • gui/tests/remote-workspace.test.tsx
  • gui/tests/sidebar-rows.test.ts
  • scripts/test-layout/layout.json
  • skills/ocx/references/01_management_surface.md
  • src/cli/capabilities.ts
  • src/cli/connect.ts
  • src/cli/dispatch.ts
  • src/cli/help.ts
  • src/cli/link.ts
  • src/cli/registry.ts
  • src/cli/runtime-api.ts
  • src/client/connect.ts
  • src/client/hub-relay.ts
  • src/client/link-join.ts
  • src/client/link-relay.ts
  • src/client/link-state.ts
  • src/client/link-teardown.ts
  • src/client/link-tunnel.ts
  • src/client/machine-listener.ts
  • src/client/runtime.ts
  • src/client/state.ts
  • src/codex/inject/plan.ts
  • src/config/schema/leaf-validators.ts
  • src/link/admission-wait.ts
  • src/link/compensation.ts
  • src/link/fingerprint.ts
  • src/link/paths.ts
  • src/link/ports.ts
  • src/link/routes.ts
  • src/link/ssh-argv.ts
  • src/link/ssh-config.ts
  • src/link/ssh-runner.ts
  • src/link/status-projection.ts
  • src/link/store.ts
  • src/link/supervisor.ts
  • src/link/tunnel-state.ts
  • src/server/audio-client.ts
  • src/server/audio-upstream.ts
  • src/server/auth-cors.ts
  • src/server/hub-usage.ts
  • src/server/index.ts
  • src/server/index/link-listener.ts
  • src/server/index/optional-listeners.ts
  • src/server/index/serve-options.ts
  • src/server/management-api.ts
  • src/server/management-auth.ts
  • src/server/management/context.ts
  • src/server/management/link-routes.ts
  • src/server/management/oauth-account-routes.ts
  • src/server/management/route-registry.ts
  • src/types/config.ts
  • structure/INDEX.md
  • structure/gui-and-management-api.md
  • structure/manifest.json
  • structure/remote-link.md
  • structure/runtime.md
  • tests/cli/cli-headless-parity.test.ts
  • tests/cli/cli-link.test.ts
  • tests/clients/client-link-connect.test.ts
  • tests/clients/client-link-relay.test.ts
  • tests/clients/client-link-runtime.test.ts
  • tests/clients/client-link-state.test.ts
  • tests/clients/client-link-teardown.test.ts
  • tests/clients/client-link-tunnel.test.ts
  • tests/clients/link-admission-wait.test.ts
  • tests/clients/link-boundary.test.ts
  • tests/clients/link-compensation.test.ts
  • tests/clients/link-fingerprint.test.ts
  • tests/clients/link-ports.test.ts
  • tests/clients/link-routes.test.ts
  • tests/clients/link-ssh-argv.test.ts
  • tests/clients/link-ssh-config.test.ts
  • tests/clients/link-status-projection.test.ts
  • tests/clients/link-store.test.ts
  • tests/clients/link-supervisor.test.ts
  • tests/clients/link-tunnel-state.test.ts
  • tests/codex-integration/injection-link-websocket.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/lab/core-lab-boundary.test.ts
  • tests/lab/core-link-boundary.test.ts
  • tests/server/link-join-route.test.ts
  • tests/server/link-listener-admission.test.ts
  • tests/server/link-listener-lifecycle.test.ts
  • tests/server/link-management-routes.test.ts
 ______________________________
< Carpe Debug. Seize the bugs. >
 ------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T04:25:17.856056Z 30ff19b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30ff19b29c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/link-tunnel.ts
Comment on lines +273 to +275
if (isAlive(pidfile.ownerPid)) return { tunnel: "owned" };
const platform = deps.platform ?? process.platform;
if (platform !== "linux") return { tunnel: "unresolved", pid: pidfile.pid };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify unresolved tunnels before marking them connected

On macOS, after the runtime owner exits, this returns unresolved without checking whether the recorded tunnel PID is even alive. createClientLinkSupervisor.initialize() then marks every unresolved tunnel as connected and returns without owning or monitoring a child, so after a crash where SSH also exited—or whenever the orphan later exits—the client permanently relays requests to a dead port and never respawns the tunnel. Check liveness before returning unresolved, and represent a live-but-unverifiable orphan as failed rather than indefinitely connected.

AGENTS.md reference: src/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Comment on lines +419 to +422
} catch (error) {
return joinFailure(error);
} finally {
joinInProgress = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the join latch set after a successful commit

After a successful join, scheduleRestart only schedules shutdown after a response-flush delay, but this finally immediately clears the process-wide latch. A second authenticated POST /api/link/join during that window is admitted despite the already-committed client connection; it can overwrite the first sidecar, fail in connectClient, and then roll back by deleting that replacement sidecar, leaving the pending restart with a link connection but no tunnel state. Clear the latch only on join failure, and retain it until the scheduled restart terminates the process.

AGENTS.md reference: src/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Comment thread src/client/runtime.ts
Comment on lines +128 to +132
const supervisor = linkMode && existsSync(clientLinkStatePath())
? createClientLinkSupervisor({
onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
})
: null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start the link supervisor even when the sidecar is absent

When the persisted connection says transport: "link" but client-link.json is missing at startup, this condition disables the supervisor entirely. The machine listener still reports ready and relays requests to the saved tunnel port, but no SSH tunnel is created and no onLinkEnded recycle can run, so the client remains indefinitely connected-but-unusable after sidecar loss. Create the supervisor for every link-mode runtime and let it treat a missing sidecar as an ended or failed link.

AGENTS.md reference: src/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 64 / 80

이 PR은 원격 링크의 여섯 번째 층이다. 혼자 쓰는 컴퓨터가 자기 대시보드에서 집을 찾아 자식으로 붙는다. 지금까지 자식 역할은 자리만 있었고, 연결은 집에서만 시작했다. 혼자 쓰는 설치에서 자식을 고르면 SSH 호스트를 고르고, 접속을 시험하고, 지문을 확인한 뒤 연결한다. 이 컴퓨터가 ssh -L 터널을 연다. 끊으면 집의 링크도 SSH로 지운다. 베이스는 codex/remote-link-5-gui 이다. #5788, #5798, #5801, #5803, #5807 위에 쌓여 있다. types.ts 와 config.ts 를 나누는 다른 열린 PR은 없다.

열쇠는 명령 인자에 넣지 않는다. 응답과 로그에도 열쇠 본문은 안 남긴다. 집의 ocx link revoke 는 이미 없는 링크면 성공으로 끝낸다. 실제 두 대 SSH는 아직 가짜로만 확인했다.

라인 - src/server/management/link-routes.ts 406–423행 — 조인이 성공해도 finally 가 joinInProgress 를 바로 끈다. 재시작은 응답을 보낸 뒤에 잡힌다. 그 사이에 대시보드가 조인을 한 번 더 보내면 새 사이드카가 덮인다. 두 번째가 실패하면 그 파일을 지운다. 곧 재시작할 연결은 터널 기록을 잃는다.

라인 - src/client/link-tunnel.ts 274–275행, 417–424행 — 리눅스가 아니면 주인 프로세스가 죽은 터널을 unresolved 로만 돌려준다. 감독은 그 결과를 연결된 것으로 적고, 터널을 맡지 않은 채 돌아온다. 맥에서 런타임이 죽은 뒤 SSH도 죽었으면, 클라이언트는 죽은 포트로 요청을 보내고 터널을 다시 열지 않는다. SSH가 살아 있어도 나중에 죽으면 아무도 다시 띄우지 않는다.

라인 - src/client/runtime.ts 128–132행 — 사이드카 파일이 없으면 감독을 만들지 않는다. 집에서 연 링크는 사이드카가 없는 것이 정상이다. 자식이 연 링크인데 파일만 없으면, 리스너는 준비됐다고 하고 저장된 포트로 요청을 보낸다. 터널도 없고, 혼자 쓰기로 돌아가지도 않는다.

메인테이너의 판단이 필요한 지점

재시작이 끝날 때까지 조인 잠금을 유지해 달라. 성공한 조인에서 finally 가 잠금을 풀면 두 번째 조인이 들어온다.

맥과 윈도우에서 확인 못 한 터널은 실패한 연결로 남겨 달라. 그 프로세스를 죽이지 않는 정책은 그대로 둬도 된다.

집에서 연 링크는 사이드카 없이 그대로 두고, 자식이 연 연결인데 파일만 없으면 실패로 보라.

너의 추천

방향은 맞다. 스택에 두고, #5807 이 dev 에 들어간 뒤에 base 를 dev 로 바꿔라. 닫을 중복 PR은 없다. 머지 전에는 성공한 조인의 잠금을 재시작까지 유지하고, unresolved 를 연결됨으로 적지 마라. 프로세스가 살아 있는지는 보고, 명령줄을 확인 못 하면 실패한 자식으로 남겨라. 실제 두 대 SSH는 이번 층에서 한 번 확인해 달라.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/remote-link-5-gui branch from 2b47b70 to 6fa1256 Compare September 25, 2026 05:03
Base automatically changed from codex/remote-link-5-gui to dev September 25, 2026 05:03
The client runtime starts a supervised ssh -L tunnel when a link sidecar exists, stops it before the listener on every shutdown path, and recycles to standalone once the link ends. A pidfile with the owner pid lets a dead owner's tunnel be reaped on Linux by exact argv; other platforms report it as unresolved. An invalid sidecar fails closed and surfaces as a failed child with reason sidecar_invalid.
…nt port contract

ocx disconnect on a Child makes one SSH attempt to run ocx link revoke on the Home, reports homeRevoke and tunnel in --json, and prints the manual revoke command when the attempt fails. disconnectClient removes the sidecar under the lifecycle lock only when it names the disconnected link. The client tunnel port is 1024-65535 everywhere it is accepted.
POST /api/link/join {alias} runs the client-initiated sequence: confirmed host, local port, remote ocx link issue over SSH, sidecar, tunnel readiness with the issued key, in-process connect, then a restart into the client runtime. Failures after the issue roll back the tunnel, the Home link, and the sidecar. Only a paired dashboard session on a standalone runtime may call it.
The Child role is available only on a standalone runtime and reuses the add sheet: candidates, probe, fingerprint confirmation, then join. Joining, failure with Retry, and the restart wait have their own states and strings in all ten locales, and the fixture server can show each of them.
The eight Remote Link guides describe the Child-initiated flow, its SSH requirement, restart, and disconnect behavior. structure/remote-link.md records the join gate, sidecar, tunnel ownership, orphan rule, teardown, and port contract.
…empotent

A join rollback clears the sidecar only after the Home revoke succeeds; otherwise it keeps the sidecar and returns join_rollback_failed with the link id, and the next join retries that revoke first. A restart that cannot be scheduled after the committed connect returns join_restart_failed and keeps the connection. ocx link revoke exits 0 for a link the Home no longer has, and disconnect reaps an orphaned tunnel even when the sidecar is corrupt.
Each candidates, probe, confirm, apply, or join attempt has an identity and an abort signal, so a response that arrives after the sheet closed or a newer attempt started changes nothing. The role hint describes the Child flow as it now works, the obsolete childPending notice is gone, the screenshot fixture no longer reaches into the page component, and the two new join errors have their own messages.
The Find Home panel reused the Home empty text, so a standalone Child read that it had no child computers.
The add-child and Find Home sheet was pinned to the right edge with only its left corners rounded, so on a wide screen it floated at the side, detached from the page. It now opens centered like the disconnect confirmation; narrow screens keep the bottom sheet.
… sheet

The SSH host alias field rendered as a bare browser input with a bold label, unlike every other form in the dashboard. It now uses the shared .input and .field-label styles, matching the candidate cards and the fingerprint box beside it.
@lidge-jun
lidge-jun force-pushed the codex/remote-link-6-client-initiated branch from 9caa83d to 9aa953e Compare September 25, 2026 05:07
@lidge-jun
lidge-jun merged commit 88b9da8 into dev Sep 25, 2026
3 checks passed
@lidge-jun
lidge-jun deleted the codex/remote-link-6-client-initiated branch September 25, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant