Skip to content

Latest commit

 

History

History
86 lines (51 loc) · 19.5 KB

runtime.md

File metadata and controls

86 lines (51 loc) · 19.5 KB

Runtime, trust and recovery

Created: 2026-09-06 Last updated: 2026-10-10

The file explorer loads the workspace root and the immediate children of expanded folders. Collapsed branches are not traversed, including during refresh; opening or revealing a file expands its ancestors. Listings are cached until a relevant filesystem change, explicit refresh, or collapse. There is no workspace-wide 20,000-entry cutoff. Quick Open searches unopened folders only when a query is entered, cancels superseded searches, and skips .git and node_modules. Over a runtime connection it calls search.files, which runs rg --files with .gitignore applied in Git repositories; a runtime without search.files falls back to the fs.list walk, which does not apply .gitignore; workspace export enumerates all folders when explicitly requested.

On macOS, dependency installation and the runtime's build, dev, and start scripts automatically repair missing execute permissions on node-pty's spawn-helper. The build and launch checks also cover installs that skipped lifecycle scripts. Run bun run repair:pty to repair an existing installation manually if terminals report posix_spawnp failed. The repair does nothing on other operating systems and leaves already executable helpers unchanged.

oxbit [path] starts a runtime for a workspace and opens the paired editor, oxbit --stop ends it, and oxbit --status reports it. See the README for the global install. bun run start with OXBIT_WORKSPACE=/absolute/workspace serves the same runtime in the foreground under Node 24. The runtime serves the production browser app on 127.0.0.1 at a port the operating system assigns; --port or PORT fixes it, and bun run dev uses 9277 for the web dev proxy. The launch URL carries #pair=<code> and, for a named file, &open=<path>; both are consumed once and stripped from the address bar. Without the fragment the app starts in the browser sample workspace, and the printed owner pairing code can be entered on the Runtime page. OXBIT_WATCH_POLLING=1 selects filesystem polling; the runtime accepts requests before the watcher's first scan finishes, watcher exhaustion (EMFILE/ENOSPC) switches to polling, and other watcher failures reach watching clients as fs.watchError and stderr. PORT, HOST, OXBIT_DATA_DIR, OXBIT_PAIRING_CODE, OXBIT_WEB_ROOT and comma-separated OXBIT_ORIGINS configure hosting; --port and --host override the first two. OXBIT_PAIRING_ATTEMPTS_PER_MINUTE sets the pairing attempts accepted per client address each minute (default 10); the browser test server raises it because every test pairs again. Each detached runtime records its runtime id, pid, host, port and pairing code in <data dir>/daemon.json at mode 0600 and appends output to <data dir>/daemon.log; reuse needs both a live pid and a healthy reply, so a stale record cannot hand you an unrelated process. Vite development access requires OXBIT_ORIGINS=http://localhost:9279,http://127.0.0.1:9279. Bind to loopback by default; use a TLS reverse proxy when exposing the runtime to a network.

POST /api/pair accepts {code} and returns a random session token with the current workspace capabilities and trust state. It also sets an HttpOnly, SameSite=Strict cookie. WebSocket /ws validates the exact Origin and requires auth.authenticate {token} within 10 seconds. Requests have a 2 MiB limit, except fs.write and collab.save, which allow the JSON representation of a supported 20 MiB file plus the request envelope, and collab.update, which allows a full base64 document update. The socket frame limit accommodates those file-save requests. The cookie substitutes for an omitted token. Pairing attempts are bounded per address. Persisted session records contain token hashes.

Owners issue grants through workspace.grant {capabilities} or POST /api/grants. Revocation through workspace.revoke {id} or DELETE /api/grants/:id closes sessions, cancels pending requests and terminates owned processes. Capability grants separate filesystem read/write, terminals, tasks, Git, LSP, collaboration and extensions. Only owners change trust or grants. workspace.trust {trusted:true} authorizes tool execution. Revoking trust terminates processes and all language server instances. One runtime instance exposes one authorized root, identified as default.

The runtime checks relative paths, ancestors and resolved symlink targets before filesystem operations. Its persisted authentication and collaboration state is excluded from file APIs and watching. Atomic saves serialize by resolved path, compare SHA-256 revisions of raw bytes and rename temporary files after encoding succeeds. Supported encodings are UTF-8, UTF-8 with BOM, UTF-16LE with BOM and Latin-1; LF and CRLF are explicit. Decoding errors, unpaired surrogates and characters outside Latin-1 produce errors. Files above 20 MiB produce an explicit size error. This boundary protects browser RPC access; workspace processes and trusted extension code run with the operating-system privileges of the runtime account. It does not sandbox those processes or defend against another operating-system process replacing directory entries during a filesystem syscall sequence.

Protocol v1 uses request IDs, responses, cancellation and named events. Each connection permits 64 concurrent requests, and durable operations share a runtime-wide limit of 64 running requests. The outgoing socket backlog permits one maximum-size file message (122 MiB); larger queued backlogs close with a resynchronization reason. Non-idempotent operations persist their request ID, owner, outcome and retained result before acknowledging completion. Clients query operation.status {id} after reconnect instead of repeating execution. Reusing a completed ID returns its retained result; expired results return OPERATION_RESULT_EXPIRED. An operation interrupted before its completion record survives as interrupted; inspect filesystem/Git state before intentionally issuing a new ID.

History retains two operation-ID epochs of 128 admissions each, running requests, and up to 1 MiB of result bodies. Authentication and operation.epoch events give clients the current epoch. RuntimeClient generates qualified IDs by default; createOperationId() supplies an ID for an intentional repeated request. Unknown retired IDs return OPERATION_EXPIRED without execution. Plain IDs from clients before 0.6.2 stay accepted; retained plain IDs return their recorded status, and a dropped one reads as unknown. See editor consistency contracts for retention and compatibility details.

New terminals use the runtime's inherited SHELL, then the operating-system account's login shell when SHELL is unset. If neither is available, the fallback is /bin/zsh on macOS, /bin/bash on other Unix systems, or powershell.exe on Windows. Terminals inherit the runtime's environment and start in the workspace directory, with SHELL set to the selected executable. Bash, zsh, and fish start as interactive login shells so their normal login profiles and interactive startup configuration can initialize the session; Bash follows its usual profile rules, including sourcing .bashrc when the login profile does so. Other shells use their native startup options. Restart an already running runtime after updating, then create a new terminal to use this behavior.

Terminal PTYs survive socket loss while the runtime is running. terminal.attach {id,afterSeq} returns retained chunks and their sequence numbers. terminal.ack {id,seq} or protocol ACK messages release flow control. Replay retains at most 1 MiB, signals truncated history, and pauses PTY output when consumers exceed 128 KiB of unacknowledged output or disconnect. At most 16 terminals and 16 tasks run concurrently. Tasks retain 1 MiB for tasks.attach, report process exit, and terminate process groups on cancellation. Runtime shutdown terminates these processes; after restart their IDs are absent from session lists. Session ownership is enforced independently of capability grants.

Git operations invoke argument arrays with credential prompting disabled. The runtime environment and Git credential helpers supply credentials. Commit request IDs prevent duplicate commits. Existing-branch checkout relies on Git's working-tree protection and reports errors without force/reset. Discard requires confirm:true. Clone accepts HTTPS, SSH and file URLs, creates a fresh workspace-relative destination, streams progress, and removes its incomplete destination on failure or cancellation. Runtime search uses ripgrep JSON output, supports case/word/regex/glob filters and cancellation, bounds output and result counts, and converts byte ranges to JavaScript UTF-16 positions.

Language servers run through vscode-jsonrpc Content-Length streams. Managed instances are keyed by preset, bounded project root and effective configuration. Lifecycle and request RPCs accept an optional instanceId; legacy requests still address the default TypeScript instance. lsp.attach/lsp.detach maintain per-client document attachments. Startup negotiates capabilities and document synchronization. Requests time out at 30 seconds and forward cancellation. Restart reopens tracked documents. URI validation restricts edits to the workspace. Read-only external navigation uses lsp.external.authorize and lsp.external.read handles for roots registered by trusted presets; arbitrary external paths remain denied. Server-initiated workspace edits route to the browser that initiated the language action. The browser applies them through the shared document service after permission and revision checks, then acknowledges the result. Requests without an owning browser are declined. Shared Yjs rooms own one canonical runtime document stream per attached LSP instance. Browser didOpen/didChange/didClose notifications for these URIs are ignored.

Collaboration rooms use Y.Text content. collab.join {path} returns the authoritative Yjs update, saved disk revision and saved text. Clients hydrate an empty Y.Doc with that update, then upload pending local changes. collab.update, collab.awareness and collab.save require room membership; text updates and saves also require filesystem write. Updates are persisted independently of file saves in the private runtime data directory. Only the runtime writes shared documents to disk. Clean disk changes enter the Yjs document. Dirty shared content produces collab.conflict when disk changes. Per-user undo belongs to the client binding's tracked local transaction origin. Presence client IDs are bound to their active connection and removed on disconnect. Persisted rooms preserve unsaved content across runtime restart.

apps/runtime/tests/runtime.test.ts exercises real filesystem conflicts and permissions, grant revocation, ripgrep, PTYs/tasks, Git commits, TypeScript LSP responses, collaboration convergence and runtime recovery.

Runtime extensions load through extensions.load {path} from a workspace-relative .mjs, ESM .js, .mts, or .ts artifact. The path and symlink target pass the filesystem root checks before import. The entry module exports a default extension with environments: ['runtime'] and public SDK manifest/activation contracts. extensions.list, extensions.disable {id}, extensions.activate {id}, extensions.remove {id} and extensions.update {path} expose lifecycle management. An authenticated extensions grant and workspace trust are required. Trust revocation disables active extensions; runtime shutdown disposes the kernel and its registrations.

Runtime extension services include filesystem, runtime.workspace, runtime.git, runtime.lsp and runtime.command. The filesystem service uses the checked workspace provider; direct Node imports retain the runtime account's operating-system privileges. An extension's imports and requested capabilities do not sandbox its module code. Own registrations with ctx.own, pass ctx.signal to asynchronous commands, and release timers/subscriptions through ctx.subscribe. Native Node 24 TypeScript loading supports erasable syntax; use a compiled ESM artifact for enums, parameter properties, or other TypeScript transforms. Runtime development through tsx also supports those development sources. Native and browser extension hosts use the same SDK lifecycle implementation.

The production response CSP computes a SHA-256 hash of each inline importmap's exact content. It permits that map without permitting arbitrary inline scripts. External extension artifacts resolve the public browser SDK through that importmap.

Filesystem watchers re-register after reconnect. Trust revocation aborts pending tool requests. Clone reserves its destination before work starts, so concurrent requests cannot remove another clone's output. Task output uses a streaming UTF-8 decoder to preserve characters split across process chunks.

Terminal lifecycle changes also reach the public SDK's event service.

Identity, LAN mode and keep-alive

Each runtime data directory holds a random runtime id in runtime.json, created once. GET /api/health returns {ok, protocol, id, name, version, startedAt}, where name is the host name and startedAt is when the process began listening. Successful POST /api/pair and auth.authenticate results carry the same object as runtime. oxbit --status prints the id.

oxbit --lan [path] binds 0.0.0.0 on a free port (or --port), detached or with --foreground. It prints the first LAN IPv4 URL, the workspace, the pairing code and the runtime id. A runtime bound to 0.0.0.0 or :: accepts the origin tauri://localhost for the iOS app, http://<address>:<port> for each non-internal IPv4 and bracketed IPv6 address, and http://<name>.local:<port>; the list is computed per request because interfaces change. The daemon record keeps host 0.0.0.0, and liveness probes and launch links use 127.0.0.1. LAN mode serves plain HTTP; the pairing code and session tokens cross the network unencrypted.

LAN mode advertises <host name> (<first 8 id characters>)._oxbit._tcp.local over multicast DNS from apps/runtime/src/mdns.ts, with no dependency. SRV points at the synthetic host oxbit-<first 8 id characters>.local because mDNSResponder owns the machine's own .local name. TXT carries id, name, version and port; A and AAAA records list LAN addresses, leaving out tunnel, AWDL and link-local ones. The responder binds UDP 5353 with reuseAddr, announces three times one second apart, answers PTR, SRV, TXT, A, AAAA and ANY queries, and sends TTL-0 goodbyes on shutdown. A socket error prints one line and turns advertising off while the runtime keeps serving. On macOS, multicast sends fail with EHOSTUNREACH for a process started from an SSH (Background) session that has no Local Network access; start it from Terminal. Check an advertisement with dns-sd -B _oxbit._tcp and dns-sd -L "<instance>" _oxbit._tcp. References: RFC 6762, Multicast DNS for the message format, cache-flush bit, announcements and goodbyes; RFC 6763, DNS-Based Service Discovery for the PTR/SRV/TXT layout and _services._dns-sd._udp enumeration.

--keep-alive <duration> or OXBIT_KEEP_ALIVE stops a CLI runtime after no authenticated WebSocket client has been connected for the duration: 75s, 15m, 1h, 8h, plain milliseconds, or forever. The default is forever, which runs until oxbit --stop. The detached daemon receives the value as an argument. There is no RPC to change it; clients cannot alter a CLI runtime's keep-alive. The server pings every WebSocket client every 30 seconds and terminates one that did not answer the previous ping. Desktop remote runtimes take their lease from the launch frame; see Remote workspaces over SSH.

The CLI exits with a message naming the found and required versions when Node is older than 24. node-pty loads on the first terminal request; when it cannot load, that request fails with node-pty failed to load: <reason> and the rest of the runtime keeps serving. A foreground runtime that cannot bind (port in use, permission denied, address unavailable) prints one line and exits 1.

Runtime page

The oxbit.runtime feature (packages/features/runtime) adds the Runtime tab, the runtime.open and runtime.cloud commands, a status bar item, and the runtime.keepAlive and runtime.autoReconnect settings. The page shows the connection state, host, port, version, uptime and workspace; Disconnect or Restart; workspace trust; keep-alive settings; saved runtimes; runtimes found on the network; SSH hosts with Start Oxbit on this server; a pairing form; and the last error, with runtime output under Details. Each app registers a RuntimeConnector (packages/workbench/src/runtime-connector.ts) as the runtimeConnector kernel service, and the page hides any control its connector does not implement.

runtime.cloud backs the cloud button and workspace.runtime. When connected or connecting, it opens the page. With a known target (the current runtime, the most recent saved runtime, or the current desktop project), it connects and shows progress on the button, and opens the page with the reason if that fails. With no target, it opens the page.

App Connector Targets and actions
Web apps/web/src/runtime-connector.ts Saved runtimes keyed by runtime id in browser storage; pairs by URL and code; explains failures by probing /api/health
Desktop packages/host-desktop/src/runtime-connector.ts The project's runtime; Restart restarts it; SSH recents become hosts for Start Oxbit on this server; runtime.keepAlive goes to desktop_connection
iOS apps/ios/src/runtime-connector.ts Saved and Bonjour runtimes, Keychain credentials by runtime id, SSH hosts; runtime.keepAlive goes to ios_ssh_runtime_start; settings are mirrored to app storage for the start screen

Collaboration

Collaboration is disabled by default on web, desktop and iOS. To enable it, open Extensions, select Collaboration and choose Enable; the choice is stored in the extension-enabled list and applies to later sessions. While it is off, runtime documents open with fs.read and save with fs.write against the document's saved revision, and the Participants status item, panel and remote cursors are absent. After it is enabled, newly opened documents join their rooms. Documents that are already open join at once when no text is lost: clean documents take the room text, and dirty documents join when the room has no unsaved edits at the same disk revision. Other dirty documents stay local; they join after their next save when the room has no unsaved edits, and otherwise when they are reopened. Documents restored at startup follow the same rules. Disabling it leaves every room. Open documents keep their text and save through fs.write, so a disk change since the last shared save is reported as a conflict.

Closing shared documents leaves their rooms and releases listeners.

Runtime storage

Runtime variables use OXBIT_*. Private workspace data, including daemon records, sessions, and collaboration data, lives in ~/.oxbit/workspaces/<id> unless OXBIT_DATA_DIR is set. Browser workspaces and recovery drafts use the oxbit IndexedDB database. JSON settings merge user preferences from ~/.oxbit/settings.json, private workspace overrides from ~/.oxbit/projects/<uuid>/settings.json, and manually created .config/oxbit/settings.json / settings.local.json files in the project root. Built-in formatter IDs are oxbit.prettier and oxbit.builtin-ts.