Repository navigation
Conversation
Port of roboco's remote-access listener (hoangvu12/roboco, the feat/remote-access and web-client lines: 6c42d32, 420bf07, 8366467), adapted to upstream zeron: the same RPC surface as IPC, served over HTTP + WebSocket to browsers and native remote clients, with every bearer verified as a WorkOS AuthKit access-token JWT against the WorkOS JWKS — the same checks the edge applies to relay clients — or accepted as the dev-mode user id when no WorkOS client id is configured. Local IPC, the edge, iOS, and sync are untouched; remote access stays off by default and is enabled per engine through the remote-access settings file or ZERON_NETWORK. - rpc: bounded WebSocket pump (twin of zeron-sync's session pumps, specialized to ndjson frames), serve_websocket over an already upgraded connection, and a Bearer-authenticated native dial - engine: remote listener (credential-gated health, browser upgrades authenticate with a first-frame Auth envelope, 4401 closes), remote-access settings/startup-override controller, WorkOS JWT verifier with cached JWKS and rotation refetch, daemon wiring - tests: listener integration tests in dev mode (HTTP gate, native Bearer dial, first-frame envelope, refused credentials) and remote-access settings resolution round-trips
Port of roboco's web client foundation (hoangvu12/roboco, the web-client line: 8366467, 3f6b069, 78e001b, b47fa24), adapted to upstream zeron with zeron naming throughout: the pnpm workspace (@zeron/{app,engine-client,proto,theme,icons}), the Vite + React app the engine serves from its remote listener, the generated TS wire types and theme artifacts (committed; regeneration scripts target the zeron crates), and the engine's rust-embed staging (build.rs stages into web-staging/: placeholder pages in debug, the Vite build in release, overridable via ZERON_WEB_DIST). The web shell loads ahead of the credential gate; the RPC socket and API routes stay WorkOS-gated. The engine-client still carries roboco's direct-pairing sign-in flow, which does not work against this engine's WorkOS listener yet — swapping it for the AuthKit sign-in flow is the next change.
GET /auth/config reports the sign-in mode and the AuthKit authorize URL (the same URL auth.rs builds for device sign-in, redirecting to the edge's hosted paste-code callback, with a fresh state nonce per call); POST /auth/exchange proxies the secret-bearing code exchange to the edge's /auth/exchange, so the browser stays same-origin and the WorkOS API key never leaves the edge. RemoteAuthorizer now builds from the engine's WorkOS client id plus edge URL; dev mode reports mode "dev" with no authorize URL and never exchanges.
The browser client no longer redeems pairing codes against the engine.
Connecting an engine is now a sign-in: GET /auth/config reports the mode,
and the browser either enters a dev-mode user id or exchanges a WorkOS
AuthKit code through the engine's proxied POST /auth/exchange (the
secret-bearing exchange stays between engine and edge).
- engine-client: auth.ts (fetchSignInConfig, exchangeSignInCode,
parseEngineUrl) replaces pairing.ts; the pairing RPC method names and
the PairedSession/PairingLink/RemoteAccessSnapshot/SessionGrant proto
types are gone with it
- app: EngineStore.signInEngine stores the credential directly
(re-signing in to an origin replaces its credential; the
damaged-config refusal is preserved); /pair is now the connect page
(address -> config -> dev id or AuthKit code); the Devices page's
pairing box and the Remote-access settings page give way to a link
into the connect flow ("Sign in again" for parked engines)
- engine: web_conformance/web_smoke examples run the real listener in
dev-auth mode (no pairing store); proto advertises the web-client
capability (roboco parity) so the web client can gate on it
- tests: the conformance/smoke suites sign in with the dev bearer and
resolve example binaries from CARGO_TARGET_DIR (an external target dir
previously hung beforeAll to its 600s hook timeout); the app
store/registry/session-provider suites follow signInEngine; one
rename artifact fixed (add-space prefix length 6 -> 5)
Verification: pnpm -r build green; engine-client 43/43 (unit 33,
conformance 5, smoke 5); app 1873/1873; zeron-proto 43/43; engine
remote_listener 11/11.
Two jobs, matching the repo's workflow conventions (pull_request path filters, concurrency group, read-only permissions): - build-and-tests: pnpm install (frozen lockfile), `pnpm -r build` (typecheck + Vite bundle for every package), the engine-client unit project, and the app test suite (1,873 tests). - engine-conformance: builds the web bundle so the engine's rust-embed staging embeds the real pages, runs the remote_listener Rust tests (WorkOS credential gate, web routes, dev bearer), builds the web_conformance / web_smoke examples, and runs the conformance and smoke vitest projects against those live engines — the full sign-in, watch, and send flow over the wire.
The stale /pairing/redeem proxy entry goes (the pairing surface was deleted with the sign-in migration). The browser now calls GET /auth/config and POST /auth/exchange over plain HTTP before any credential or WebSocket exists, and the catch-all proxy serves non-WebSocket requests from Vite itself — so without explicit entries the connect page can never reach the engine in dev. Verified against a live WorkOS-mode engine: vite on :3000 proxies both routes (auth config reports workos mode with the AuthKit authorize URL) and the RPC WebSocket at `/` traverses the proxy to the engine's remote listener.
…very The web client now uses PR zeronsh#319's browser-session model end to end; the manual connect flow (engine address, dev user id, pasted AuthKit code) is gone. - engine-client: `device-frame.ts` (the DeviceRoom binary framing ported from crates/rpc/src/device_frame.rs plus `RelaySocket` — the browser transport with ping/pong/echo liveness and buffered-amount backpressure) and `edge.ts` (the /api/browser/* session, login, devices, and logout API). EngineClient skips the first-frame Auth envelope when the credential is empty: the relay authenticates the browser session cookie at the WebSocket upgrade. - app: `state/fleet.ts` is rewritten around the edge — boot checks /api/browser/session and redirects signed-out visitors straight to WorkOS (PKCE authorize URL from the edge, callback returns to the app root with the HttpOnly session cookie); signed in, the owner-scoped device list is polled and every device is supervised through the cookie-authenticated relay WebSocket. `/pair` is now an automatic connect gate, and the Devices settings page describes the zeron-login-driven flow instead of manual pairing. - dev serving: vite allows the dev.embedez.com tunnel host and proxies /api/browser (HTTP + WS) to the local staging edge (ZERON_DEV_EDGE, default http://127.0.0.1:27641). Verification: pnpm -r build green (engine-client + app typecheck, Vite bundle); app suite 1873/1873 (one session-provider expectation updated: registry restart re-pins identity to the device-id key).
… dev edge Through the dev tunnel the Vite proxy forwarded the public Host (dev.embedez.com) to the local `wrangler dev` edge, whose dev paths gate on `loopback(url)` — every /api/browser/* request came back 403 forbidden before Origin was even consulted. Set `changeOrigin: true` so the upstream sees its loopback Host while the browser-side Origin header still validates against WORKOS_BROWSER_ORIGIN, matching what upstream's scripts/browser-dev-proxy.mjs does for its own proxy hop. Verified live: POST https://dev.embedez.com/api/browser/login now returns the AuthKit authorization URL for the staging WorkOS client with redirect_uri=https://dev.embedez.com/api/browser/callback, and GET /api/browser/session reports {"authenticated":false}.
The post-login crash: useFleet() derived a fresh engines array with
fresh StoredEngine objects on every render, and the session provider
reconciles on that array — reconcileEngineSessions clones every session
whose wrapper identity changed, so the effect looped setSessions
forever ("Maximum update depth exceeded" at session-provider.tsx:85,
taking TerminalProvider/RightPane down with the churn).
Two guards:
- useFleet memoizes the derived engines on (devices, ownerId), so the
array identity changes only when the device list actually changes.
- refreshDevices skips the state update when a poll reports the same
(id, name) list: the store's devices array is the derivation input,
and a new-but-equal array every 10s would churn every reconciled
session and catalog consumer on each poll.
Regression test (tests/fleet.test.ts) fails on the old code and locks
both contracts: re-renders and unchanged polls must keep engines (and
entry) identity. Full app suite 1874/1874.
The browser session now carries the signed-in account's profile, and the sidebar's bottom identity control renders it: the WorkOS user's picture in the avatar circle, name and email in the menu card, with the engine's device name as the fallback while no profile exists. The account menu also gains a Sign out row, which revokes the browser session through the edge and reloads into the login gate. The right pane's resize seam now parks on the pane's left edge when the docked files column is open: the CSS offsets by the pane plus the files column's width, and the drag commits the pane's own width with the open files column subtracted. `vite preview` mirrors the dev server's proxy and host allowlist so the dev domain can serve the optimized bundle — the unbundled dev server pays the tunnel's per-module latency on ~500 modules. Comment scrub: fork-attribution narration removed from web sources; no reference to any prior project name remains. Full app suite 1876/1876.
The compact f956339 trigger (21px button, 13px avatar) rendered the WorkOS profile picture as a speck and left no readable hit target. Now that the edge carries the avatar (gratenes/zeron c07070fd), size the row for it: a 44px avatar (16px mono initial fallback) with the account's name to its right, truncated at the sidebar's edge — the whole row is the clickable trigger, and the name rides both the visible label and the aria label.
The legacy pairing-era Engines card (ticket 45's folded drawer row) listed the fleet's engines again above the device rows, so every engine appeared twice — under the WorkOS browser-session fleet the two lists describe the same engines, so the device rows are the registry of record. The engines card, its registry wiring, and the section header are gone, and the pairing hint now carries the rhythm the header used to provide (a 16px gap before the device list — the two cards touched without it). Reproducer: tests/settings-devices.test.ts mounts the page with one engine over one device and asserts exactly one device row renders.
The composer-defaults store (zeron.composer-defaults.v1) already persisted every model pick per harness, but a new chat never saw it: same-harness picks didn't remember the harness (a fresh chat resolved to the first enabled harness, so the remembered model was never consulted), and the new-chat draft seeding ignored the sticky picks entirely — both deviations from the desktop's pickers.rs effective_harness/effective_model_id resolution. pickModel now remembers the harness on every pick, and the draft initializer seeds the remembered harness + model when the catalog and model list still offer them, falling back to today's first-enabled default otherwise. Ten new composer-draft tests pin the sticky seeding, the fallbacks, and the versioned store key.
The web client shipped no favicon at all — /favicon.ico 404'd and index.html declared no icon <link>. Port the landing site's set from the public repo (zeronsh/zeron, apps/landing/public): favicon.svg (the Zeron mark) for modern browsers, zeron-favicon-v3.png as the 128px PNG fallback, and zeron-app-icon.png as apple-touch-icon. The bundled default new-thread background becomes Purple_Nebula_4K (3840x2160) — the hero artwork behind the composer on empty new threads. No code changes: resolveNewThreadBackground already falls back to this bundled asset, and a user-installed image still overrides it.
Gratenes
marked this pull request as draft
September 25, 2026 00:43
hoangvu12
added a commit
to hoangvu12/roboco
that referenced
this pull request
Oct 9, 2026
Port zeron 5cd23bd (PR zeronsh#526): fleetSpaceRows now actually dedups — the base implementation compared the engine scopes of space.id and space.deviceId, which projectRegistrySnapshot always scopes with the REPORTING engine's key, so mirrors were never dropped. Roboco keys engines by baseUrl (not device id like zeron), so the owner's engine is resolved through engine.info.deviceId: a projected space survives only when the device named in its owner field is the reporting engine's host. fleetDeviceRows was already correct at base; its tests pin it, including the deliberate no-row-when-info-unloaded deviation from zeron's '?? engine.key' fallback (a baseUrl is never a device id).
hoangvu12
added a commit
to hoangvu12/roboco
that referenced
this pull request
Oct 9, 2026
Zeron 97f8611 (PR zeronsh#526) ported onto 05faf5a, ticket 91 (.scratch/web-parity/issues/91-settings-engine-labels.md): - lib/settings-engine.ts: settingsDeviceName(engine, registry) takes the fleet engine (baseUrl + pinned deviceId, engineDisplayName's parameter type) and names it from its OWN registry entry's WatchDevices row — renames included, blank names are missing names, entry?.info?.deviceId ?? engine.deviceId resolving the row before EngineInfo lands, and never a peer entry's copy of the row. settingsEngineLabel names the active engine only when the fleet is plural and the active key has a fleet engine (else null — the indicator hides). - settings-engine-indicator.tsx: popover rows name engines through settingsDeviceName(engine, registry); the trigger keeps 05faf5a's unprefixed label form. - NEW components/settings-engine-page.tsx: SettingsEnginePage keys the engine-addressing pages' bodies on settingsEngineKey(fleet) — Remote access, Agents and Accounts now REMOUNT on a settings-engine switch, dropping engine-local state (in-flight sign-ins, install/update replies, expanded rows, error strips, the DeviceSwitcher passthrough target) with its engine instead of reconciling it onto the new one. The old setTarget(null)-on-fleet.active reset effects were subsumed and removed. - settings-devices.tsx: the host row title call adapts to the new signature (stored engine, synthetic identity for registry-only rows). Deliberate deviations from the zeron commit (recorded in the ticket): the fallback is engineDisplayName (host · short id), not zeron's engine.label || baseUrl — roboco's StoredEngine.label IS the pair-session label this ticket forbids; the DeviceSwitcher stays on Accounts/Agents for intra-engine targetDeviceId routing (CAUTION b — not ported, zeron's relay transport has no device addressing); the canvas engine chip is untouched. Tests: settings-engine.test.ts extended (own row vs pair label/peer copy, rename, pinned identity, blank-name fallback, fleet-missing null), settings-engine-indicator.test.ts ported, and the new settings-fleet-routing.test.ts (mounted pages: re-list on the picked engine with no passthrough target, the Agents sign-in failure dropped by the remount — red at base — and the DeviceSwitcher's kept intra-engine targetDeviceId pick). tsc --noEmit clean, pnpm build green, focused + neighbor suites green (11 + 85).
hoangvu12
added a commit
to hoangvu12/roboco
that referenced
this pull request
Oct 9, 2026
Port zeron 4928e1b (PR zeronsh#526) onto the landed ticket-86 wiring, plus 4510bab's composer-side label comments: - DeviceCard.pick branches on targetForDevicePick's noProject result: a projectless engine switch clears the visible sidebar filter through rememberNoProject (last-project history stays navigation state) while same-owner projects are retained. - ProjectChip/ProjectCard take currentDeviceId (the resolved engine the chips show) so 'Don't work in a project' never silently restores the remembered device of a stale host; the footer and the canvas selector row both pass their effective device id. - tests/browser-engine-picker.test.ts (new, ported from zeron's 4510bab suite): engine identity labels ('Selected engine' tag on the effective row, 'Select engine' empty state, registry presence through the real engineStatesOf, one offline glyph) under both viewports, plus the canvas pick reconciliation (foreign project + sidebar filter cleared on engine switch, project pick routes to its owning engine, no-project keeps the visible engine).
hoangvu12
added a commit
to hoangvu12/roboco
that referenced
this pull request
Oct 9, 2026
Port zeron 779cc2e (PR zeronsh#526) onto the integration base. The IA change that commit makes — deleting the pairing-era Engines card that listed every fleet engine again above the device rows — was already roboco's state (05faf5a removed the card and folded the engine actions into the device-style EngineHostDeviceRow; 87 landed fleetDeviceRows; 91 the settingsDeviceName titles), so the delta is the remainder of the commit's intent: - settings-devices.tsx: the page doc comment still described the removed engines card and the pre-05faf5a0 local-row wording; rewritten to the post-commit shape (one row per engine; pairing URL box kept, WorkOS sign-in hint not ported). No behavior change. - app.css: the commit's rhythm fix — .settings-pairing-box gains margin-bottom 16px, the gap the section header used to provide before the flat device card (roboco's pre-identity arm; the sectioned arm is unchanged through sibling margin collapse). - NEW tests/settings-devices.test.ts: the mounted reproducer, adapted to this ticket's two-engine acceptance shape — exactly two device rows, names of record, the section split, one Active engine badge, no 'Roboco web on' pair-session label anywhere; a parked engine's host row carries Pair again (routes to /pair) and Forget (confirm-in-place, forgetEngine(baseUrl)). Mutation-checked: re-adding an engines card fails the test. Ticket: .scratch/web-parity/issues/92-devices-single-list.md
hoangvu12
added a commit
to hoangvu12/roboco
that referenced
this pull request
Oct 9, 2026
…ds (wpn-93) Port zeron zeronsh#526's 631a8e0 + 6e4f363 (ticket 93) on the integration head: - selectedHarnessUnavailable gains the settled offered-on-the-selected- engine arm (d57b27f's two-arm form); selectedModelUnavailable replaces newChatNoAgents — a fresh send waits for the resolved engine's OWN harness and model catalog (pending/empty/error all block; the sticky draft is never substituted) and for the reasoning/options reconciliation of a model carried from another engine. - The fresh draft seeds the sticky picks (remembered harness + its remembered model, pickers.rs:713-745) through StickyDraftPicks and defaultDraftHarness; the harness reconcile effect becomes reconcileFreshDraftHarness (offered-set membership, not raw rows) and discovery targets offered harnesses only. - reconcileFreshDraftModel + the reconciliation hook's fresh flag drop option picks the new engine's metadata does not offer; established chats keep their committed picks. - The pickers attribute catalog states to the selected engine: engineLabel, the "{label} unavailable" chip/card takeover for a committed harness the engine no longer offers, and modelDiscoveryErrorMessage (missing_executable names the engine and its override; the browser cannot fix it). The compact card (the app default presentation) gets the same states. - PickerCatalog tracks per-harness model flight identities so a reset/retry drops a stale discovery reply instead of repainting the new target's catalog. Adaptations (documented in the ticket): roboco's per-model reasoning memory (zeronsh#471) stays the preference layer at both seeding sites; the per-engine canvas drafts (wpn-86) change the blocked-draft assertion to a switch-back round trip; durableCalls includes upload RPCs. Tests: composer-availability 15/15 (engine-switch suite + offered-arm bite checks, red first), composer-draft 49/49, composer-reasoning 46/46 (availability states + wiring pin), composer-send 29/29, picker-catalog 20/20; adjacent ring 28 files 501/501; tsc --noEmit and pnpm run build clean. Full suite deferred to the orchestrator.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Web client
Summary
This adds an opt-in browser client for zeron: the engine's remote listener serves an embedded web app over HTTP + WebSocket, with every bearer verified as a WorkOS AuthKit access token — the same checks the edge applies to relay clients.
Remote access stays off by default. Local IPC, the edge, iOS, and sync are untouched.
How it works
Engine side
serve_engine_remotebinds an HTTP+WebSocket listener exposing the same RPC surface as IPC. Every request's bearer is verified byremote_auth::RemoteAuthorizer: WorkOS access-token JWTs against the WorkOS JWKS (with caching + rotation refetch), or the dev bearer (userId/userId@orgId) when no WorkOS client id is configured. The same verifier protects the WebSocket data plane (first-frameAuth).GET /auth/configreports the sign-in mode and the AuthKit authorize URL;POST /auth/exchangeproxies the secret-bearing code exchange to the edge, so the WorkOS API key only ever lives engine↔edge, never in the browser.build.rsstages the Vite build intoweb-staging/(placeholder pages in debug,ZERON_WEB_DISToverride). The HTML shell loads before the credential gate; the data plane stays gated.remote-access.json/ theZERON_NETWORKstartup override.web_conformance/web_smokeexamples run the real listener in dev-auth mode for the test suites.Web side (
web/— a pnpm workspace,@zeron/{app,engine-client,proto,theme,icons})@zeron/engine-client: the RPC client (reconnect + re-auth, generation-based watch streams), the watch cache, the fleet registry (one supervised connection per engine, merged under scoped ids), and the sign-in flow (fetchSignInConfig/exchangeSignInCode) — a dev engine takes a local user id; a WorkOS engine redirects through AuthKit and the pasted code exchanges through the engine's proxied route.@zeron/app: the full web app — chat transcript, composer, fleet sidebar, settings, dialogs, command palette, mobile layouts — adapted to the sign-in model:/pairis the connect page, the Devices settings links into it, and the pairing-code surface (methods, proto types, remote-access settings page) is removed.Commits
feat(engine): WorkOS-authenticated remote engine listener— listener, remote_auth verifier, rpc WebSocket pump, off-by-default remote accessfeat(web): web client workspace + engine-served bundle— the portedweb/workspace + rust-embed stagingfeat(engine): pre-auth sign-in routes for the web client—/auth/config+ proxied/auth/exchangefeat(web): sign-in flow replaces engine pairing codes— the browser sign-in migration; pairing deletedci(web): web client build, unit, and live-engine conformance jobsTesting
cargo test -p zeron-engine --test remote_listener— 11/11 (HTTP credential gate, first-frame envelope auth, web routes, dev bearer)cargo test -p zeron-rpc— 12/12;cargo test -p zeron-proto— 43/43pnpm -r build— typecheck + Vite build greenWeb clientworkflow runs the build, unit suites, remote_listener tests, and the live-engine conformance/smoke suites onweb/**-touching PRsNotes for reviewers
c914b319) with no merges; every commit is a forward port.remote-access.json/ZERON_NETWORK, the listener never binds and the web bundle is never served.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.