Commit graph

8 commits

Author SHA1 Message Date
Pascal André
f90cbbc32e
feat(desktop): connect directly to remote servers from launch arguments (#1240)
Add --connect-server <URL|saved-server-name-or-id> and the inline form to
Electron and Tauri. Desktop launches can now open a remote CodeNomad
window directly, without leaving an empty local window behind. Existing
instances receive the launch intent; explicit folder and new-window
arguments retain their normal behavior.

Reuse the UI connection flow, including saved credentials, certificate
checks and sign-in. Reject credentials and pairing fragments in command
line URLs. Resolve saved entries by id, name and URL, with translated
errors for missing, unknown or ambiguous values. Windows Tauri refuses
arguments containing its forwarding delimiter rather than guessing their
boundaries; the help describes this limit in all ten locales.

Connect-only startup uses a hidden helper excluded from user-window
capacity, restoration, last-window decisions and state flushing. Success
disposes it; errors, timeouts or interactive prompts fall back to normal
startup without losing saved windows or the deferred Preferences restore.
Electron waits for the initial remote page load before reporting success.
Helpers close without a renderer flush; ordinary windows keep their
state-preservation safeguards.

Validation: Electron native suite (319 pass, 4 skipped), Tauri tests
(302 pass), UI connection tests, typecheck and help checks. Live isolated
Electron runs cover cold success, unreachable and unknown servers, page
failure, second-instance forwarding, Preferences restoration and backend
startup failure. Four review rounds resolved all P0-P3 findings; the final
head passed the complete CI, including all desktop builds.

Closes #1234
2026-11-01 01:54:05 +01:00
Pascal André
4b163382bc
feat(remote-control): serve web previews to paired devices (#1131)
Remote Control blocked `/previews/*`, so a phone or browser paired through
the tunnel had no web preview at all: the entry was hidden and the agent's
dev server stayed out of reach. Previews now work there, behind a host
setting that is on by default.

A preview's sandboxed document sends neither the device cookie nor a usable
`Origin`, so the Remote Control gate admits a live `/previews/<token>/…`
capability on its token alone, as local listeners already do, while the
host's Web previews switch is on. The ingress, which refused every
WebSocket, now admits preview upgrades only: `admitRemoteUpgrade()` parses
the target exactly as the preview proxy does and the manager hands the
socket to the application's path-preview upgrade handler, never to the
shared upgrade event (so `/previews/<token>/../../sidecars/…` cannot reach
another handler). Every other upgrade still gets 404.

The switch lives in Settings → Remote Access → Remote Control. Only the
host can change it (`PUT /api/remote-control/previews`); the registry
stores only the opt-out. Turning it off refuses later preview requests,
including the Referer-based root-relative asset fallback, and closes live
preview WebSockets; remote `/api/meta` then reports `previewsUnavailable`,
which hides Open web preview on paired devices.

Exposing previews through a public tunnel makes their address a bearer
credential, so path-proxied preview responses now carry
`Content-Security-Policy: sandbox` with the iframe's own flags (one shared
constant, `previews/sandbox.ts`) and `Referrer-Policy: same-origin`, on
every content type. A preview document opened outside its iframe stays an
opaque origin instead of running with CodeNomad's origin and device cookie,
and the capability path stays out of Referer headers to other sites. Such a
standalone document then behaves like the preview iframe: its root-relative
resources rely on the runtime bridge's path rewriting, without the Referer
fallback. The per-token preview.localhost host keeps its own origin and
headers.

Help (Remote Control, mobile and client overview pages, all ten locales),
dev-docs/REMOTE_CONTROL.md, architecture.md and UI_SHELL.md describe the
behavior; the switch label and caption are translated in all locales.

Validation: a Remote Control surface test drives the real application
through the ingress: a preview page and an `Origin: null` form post work
without a device cookie and carry the sandbox and referrer headers, unknown
tokens and non-preview or dot-segment upgrades get 404, a preview WebSocket
upgrades, a paired device cannot change the switch, and turning it off
closes the live WebSocket, refuses the preview and its Referer asset
fallback, and flags the metadata.
Registry, metadata and proxy tests cover persistence of the opt-out, the
flag and the headers per entry point; browser tests cover the switch and
the conversation header with previews on and off.
2026-10-24 20:25:25 +02:00
Pascal André
9ac792fffe
fix(server): authenticate the path the router routes, not the raw target (#1124)
The authentication hook decided whether a request needs a session from the
raw request target, while Fastify's router matches a decoded path. A
percent-encoded or absolute-form target therefore reached protected routes
without a session: "/%61pi/workspaces" routed as "/api/workspaces", and
"/%73idecars/<id>/..." reached a SideCar, on any listener the server
exposes, including LAN listeners protected by the server password. Remote
Control was not affected: its gate already judged a decoded path.

Both checks now share `routedPath()` (server/routed-path.ts), which returns
exactly the path find-my-way matches: cut at the first "?" or ";" (Fastify's
default semicolon delimiter) and decoded with decodeURI, so reserved
characters such as %2F stay encoded and %25 is never decoded twice. Targets
the router would reinterpret (absolute-form, a leading "//", a fragment) or
reject (malformed percent-encoding) are refused with 400 before any access
decision. The Remote Control gate used a broader decodeURIComponent
variant; sharing the router-exact helper keeps the two classifications
identical to routing.

Validation: `routed-path.test.ts` checks the helper against the decoder of
the router Fastify itself loads and its refusals. `request-path-auth.test.ts`
drives the real HTTP application over raw sockets: encoded /api, /sidecars
and /workspaces targets, queries and semicolons get 401 without a session,
reinterpreted or malformed targets get 400, public routes stay public and a
session still reaches encoded paths. It fails on the previous hook
("/%61pi/test/echo" answered 200). The Remote Control surface tests and the
whole server suite pass, and the server typecheck is clean.
2026-10-24 10:17:31 +02:00
Pascal André
7a1fc90310
docs(agents): keep AGENTS.md to universal rules and agent docs to the current state (#1082)
AGENTS.md loaded about 33,000 tokens into every agent session, subagent and reviewer, mostly per-feature contracts, and the agent documentation around it had accumulated duplicates, dated logs and history of removed designs. This PR makes AGENTS.md a short set of rules that apply everywhere plus an index of area documents, and leaves AGENTS.md, dev-docs and the repository skills describing only the current state.

Three commits, reviewable separately:

1. **Move (lossless).** Every per-area rule moved verbatim from AGENTS.md into its area document (existing ones, or six new ones: UI_CONVENTIONS, UI_SHELL, COMPOSER, TRANSCRIPT, SETTINGS, PROJECTS). A script split the old file into 208 units, required one destination for each and checked all 199 content units verbatim at their destination. AGENTS.md went from 134,000 to 16,000 characters.
2. **Trim.** A per-area ponytail review removed rules stated two or three times (keeping the more precise copy), dated validation logs, per-version qualification sections below the current minimum, abandoned designs and self-references; seven documents whose surviving rules live elsewhere were deleted. It also fixed statements the code contradicts (locale registration, the Mission play/recovery condition, plugin package updates, cleanup progress UI, timeline index persistence, automation restart/targets, stale pins and minimums).
3. **Current state only.** The new AGENTS.md rule: no history (removed features and their names, former behavior, provenance, dated logs) and no denials; correct a contradicted statement at its source. DEVELOPER_MODE.md is merged into BROWSER_AUTOMATION.md and every Developer Mode mention is gone (including the README and the browser help page in ten locales). Names that only made sense against older designs are renamed with every reference updated, MISSIONS_ENVIRONMENT_ADMISSION is folded into SESSION_ENVIRONMENT, and INDEX lists every document. The server npm README now states the real OpenCode minimum (2.1.2).

AGENTS.md is now about 14,000 characters (roughly 3,500 tokens); AGENTS.md, dev-docs and the vendor README went from about 11,200 lines to under 4,700.

Validation: every relative Markdown link and every cited dev-docs path resolves; no reference to a renamed or deleted document remains; `npm run test:help` passes for all ten locales.

Review: three independent GPT-6.1 Sol (high) reviews by area found binding rules lost in the trim and current-state passes; 25 were restored as present-tense facts or prohibitions and four contradicting statements corrected, each checked against code. A Fable 5.1 gatekeeper review of the whole PR passed; its follow-up routes session recovery from the index, points to dev-docs/INDEX.md and documents the family identity read.
2026-10-21 16:59:43 +02:00
Pascal André
53bdcf4fbd
feat(remote-servers): pair a desktop window through a Remote Control link (#928)
## Problem

A CodeNomad desktop app could not connect to another CodeNomad through
Remote Control. Remote windows authenticate with the server password,
which the Remote Control surface refuses. Saving the tunnel address
strips the `#code` fragment, so the window then landed on the pairing
page with no code, and that page offers no field to type one.

## Change

- **Add server accepts a Remote Control pairing link**
(`https://…/remote-pair#code`). `lib/remote-pairing-link.ts` splits it
into:
  - the tunnel origin, which is saved as the server;
  - a single-use entry URL, which only the window opened now receives.
- The code is never sent to the probe and never stored with the server.
Hosts keep only the window origin.
- A pairing link opens the window even after **Save**, because a code
that no window consumes would be lost. On hosts that cannot open remote
windows it is saved as an ordinary address.
- Pairing a saved server again reuses that profile, and with it its
persistent window storage, instead of adding a duplicate.
- The pairing window skips the Tauri TLS proxy: Remote Control
certificates are publicly trusted.
- After pairing, the remote window holds the device cookie in the
profile's persistent storage (Electron `persist:` partition, Tauri
per-profile WebView data). Later launches reconnect like any paired
device.
- A localized hint under the URL field mentions pairing links (10
locales).

No server, gate or host change. Probing an unpaired Remote Control
origin already succeeds (`authenticated: false, pairingRequired: true`),
as covered by `remote-surface.test.ts`.

## Tests

- `remote-pairing-link.test.ts`: splitting, query removal, rejection of
non-pairing or incomplete links and of `http:` links.
- `tests/browser/remote-server-pairing.test.ts` exercises the real
dialog and hook in a simulated local Electron window:
- pasting a link and pressing Save opens one window on the link, with
only the origin probed and saved and no code in the persisted state;
  - re-pairing a saved server reuses its id;
  - an ordinary Save opens no window.
- The first browser test fails without the fix. Root typecheck passes.

Not exercised in a packaged desktop build against a live host.
2026-10-13 02:20:37 +02:00
Pascal André
52133e2148
chore(remote-control): update OpenTunnel to 0.5.1 (#922)
Adopt the upstream fix for the hosted API's intermittent Cloudflare 525s (anomalyco/opentunnel#45): transient API failures are retried with capped backoff, errors name the HTTP status, and an unexpired saved certificate keeps serving when the check still fails. The PROXY protocol stays disabled. The adapter declares the workspace's single stable Effect 4.0.1, and the opt-in live test resolves the SDK wherever npm places it. Validated typechecks, build, 38 Remote Control tests, packaging checks, packaged Node imports and the live hosted-relay run; merged without waiting for CI at the user's request.
2026-10-12 17:52:07 +02:00
Pascal André
887808c97e
fix(remote-control): resume after restart and keep paired devices visible (#921)
Remember Start until an explicit Stop and resume at launch without issuing a pairing code, in one backend per config directory through a live-PID owner lease. Never-started installations still write nothing and never connect. Settings keeps paired devices listed and revocable while Remote Control is off, polls only while the tunnel is (re)connecting, keeps pairing links across reconnects, removes them on expiry or stop, and offers Stop after a failed resume. Covered by manager, lease, registry and browser regressions; merged without waiting for CI at the user's request.
2026-10-12 17:17:53 +02:00
Pascal André
9dd4b3f059
feat(remote-control): add Remote Control over OpenTunnel alongside direct access (#668)
Adds Remote Control alongside direct/LAN access. A phone or another computer pairs with CodeNomad and continues its sessions from anywhere, without an inbound port. The transport is OpenTunnel, the blind TLS relay used by OpenCode: TLS ends inside the CodeNomad backend, and nothing has to be deployed.

- packages/remote-tunnel wraps @opentunnel/client. The server build bundles it with its own Effect 4.0.x, so the published server has no runtime dependency on a private workspace package. CodeNomad claims its own route on the device's shared identity and never deletes it.
- A remote-only loopback ingress marks its sockets as remote, so local privileges come from isLocalRequest(), never the peer address. The gate runs before CORS, auth and routing:
  - exact tunnel Host, and the public Origin on mutations;
  - stripped forwarding headers;
  - origin-form targets only, judged as routed (decoded, cut at ';');
  - host-only paths denied;
  - a paired device required, except for the public UI bundle.
- Host-owned pairing: a single-use five-minute code, hashed device credentials with 30-day idle expiry, and a __Host- cookie. Revocation or eviction closes live streams. A revoked device is asked to pair again, keeping drafts.
- The Cloudflare Worker/Durable Object relay, the custom E2EE protocol and the browser transport shims are removed. Electron and Tauri keep the backend alive after the last window only while Remote Control is enabled.

Validation:
- the real createHttpServer is exercised through the ingress, plus raw-socket canonicalization tests and device registry tests;
- recovery unit and browser tests;
- an opt-in end-to-end run through the hosted relay;
- a packaged smoke import of the bundled runtime;
- five gatekeeper rounds to zero findings, and green CI on all desktop targets.
2026-10-09 12:43:25 +02:00