diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7eadfda1..79f909f9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,9 +6,9 @@ Roamgate is an independent community Web and PWA client for Herdr. It has two pa - `server/src`: Bun-powered local bridge server (HTTP + WebSocket) that talks to the local Herdr socket. -- `web/src`: React + Vite frontend dashboard. Reusable UI lives in - `web/src/components`, assets in `web/src/assets`, global styling in - `web/src/styles.css`. +- `web/src`: React + Vite frontend. Reusable UI lives in `web/src/components`, + assets in `web/src/assets`, and styles in `web/src/styles` plus co-located + `web/src/components/*.css` files. - `scripts/`: release and packaging helpers. Generated build output lives in `server/public`, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd695679..5cd57b19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ ## Development Setup -Use Bun 1.4.3+ (CI: 1.4.3), Node ^20.19.0 or >=22.12.0 for Vite 8, -and a running local Herdr server: +Use Bun 1.4.3+, Node ^20.19.0 or >=22.12.0 (for Vite 8), and a running local +Herdr server: ```bash bun install --frozen-lockfile @@ -14,79 +14,59 @@ bun run dev:web ``` Open . Vite proxies `/api`, `/ws`, and `/login` to the dev -bridge at `127.0.0.1:8788`; installed services and `start:server` default to 8787. -`dev:server` sets `NODE_ENV=development`, so its loopback listener needs no login. -To test authentication, use `NODE_ENV=production bun run start:server` and log -in with your configured password or the saved token at the reported file path. -See [Security](./SECURITY.md#trust-model) before forwarding a listener. +bridge on `127.0.0.1:8788` (services and `start:server` use 8787). `dev:server` +runs in development mode, so loopback needs no login. To test authentication, +run `NODE_ENV=production bun run start:server`. Never forward a development +listener; see [Security](./SECURITY.md#trust-model). -Keep the only lockfile (`bun.lock`) and shared tooling at root; runtime -dependencies belong in their web/server workspace. After changes, run root -`bun install` and commit manifests/lockfile. +`bun.lock` is the only lockfile. Shared tooling belongs in the root manifest and +runtime dependencies in the `web` or `server` workspace. Commit manifest and +lockfile changes together. ## Validation -**Fresh checkout:** run `bun run typecheck` once to generate web assets needed by -process tests. Then use focused checks while iterating: +On a fresh checkout, run `bun run typecheck` once to generate the web assets that +process-level tests need. Then iterate with focused checks: -| Check | Command / limits | +| Check | Command | | --- | --- | -| Format | `bun run format ` or `format:check`; omit paths for the whole repo. | -| Types | `bun run typecheck:quick` checks scripts/web/server without rebuilding assets or checking production bundles. | -| Lint | `bun run lint` runs Oxlint without a cache. Rule/scope regression checks: `bun test scripts/lint.test.ts`. | -| Tests | `bun test `, `bun run test` (serial), or `bun run test:quick` (four workers). Both full-suite commands include unit and server integration tests. | -| Submission | The local Git hook runs lint and native Bun checks for scripts/web/server. | - -Oxlint's explicit rules live in `.oxlintrc.json`; formatting stays in Biome. -Existing `eslint-disable` comments are supported, including unused-directive -warnings. Declare globals in the config rather than inline `/* global */` -comments. Duplicate parameters remain checked by `no-redeclare` or the parser; -legacy octal literals in non-module JavaScript have no dedicated lint check. - -The installed `.githooks/pre-commit` runs lint and native Bun type checks: let it -run when committing rather than repeating it manually on the same revision. -Without the hook, run `bun run lint` and `bun run typecheck:bun` before committing. -Further edits require revalidation; never bypass the gate. Run formatting and -related tests separately. - -PR CI runs format/lint/types, site build, and the complete `test:quick` suite. -Release preparation and publication run `bun run precommit`, retaining formatting, -lint, full type checks, and the complete test suite. -After the existing TypeScript checks generate web assets, CI also runs -`bun run typecheck:bun`. This invokes Bun 1.4.3's standalone `bun check` command -for the root, web, and server tsconfigs. Running only `bun check` at the root -checks the scripts project, not all three projects. The native check supplements -the existing TypeScript and lint gates; it does not emit files or rebuild assets. -On a fresh checkout, run `bun run typecheck` before `bun run typecheck:bun`. -Automated tests do not launch Chrome or WebKit. Browser-specific focus, layout, -input, accessibility, and security enforcement require manual validation against -a real backend for affected changes: - -- Check dialog focus/keyboard navigation, scaled layout, and HTML preview - iframe/direct-navigation isolation, including workspace assets and CSP. - -- On iOS Safari and Android Chrome, check keyboard opening/dismissal, IME, - selection/copy while output streams, and short-landscape/scaled menu bounds. - Check mixed mouse/touch input on actual hybrid hardware. -- Exercise connection recovery and profile controls, notification permission and - delivery with the page closed, and settings/integration changes on the server. -- Check desktop focus after app switching, OS clipboard/rectangular selection, - and responsiveness during sustained terminal output and real network latency. -- Open two tabs at the same backend/origin. Change appearance, layout, font, - shortcut, diff/wrapping, notification and automatic-update preferences; confirm - the other tab updates without reloading. Remove/clear saved preferences and - confirm defaults return. Keep each tab's workspace, file selection, draft, - focus and active input mode independent, including across different connections. - -Record device/browser and results in the PR. Passing unit and server integration -tests does not establish browser behavior or real-user-experience acceptance. - -For the opt-in Windows SSH end-to-end test, run a disposable Roamgate bridge -with two connected Windows profiles pointing at dedicated Herdr 0.9.1+ sessions. -Use session names beginning with `roamgate-ssh-`, and open a PowerShell pane in -each. Close browser terminal viewers before the test so they cannot compete -for terminal sizing. The loopback bridge must allow this test WebSocket without -login (for example, a separate development server). Run: +| Format | `bun run format ` or `bun run format:check` | +| Types | `bun run typecheck:quick` (no asset rebuild) | +| Lint | `bun run lint` (Oxlint; rules in `.oxlintrc.json`) | +| Tests | `bun test `, `bun run test` (serial), or `bun run test:quick` (four workers) | +| Frontend build | `bun run build:web` (required for frontend changes) | + +The pre-commit hook runs lint and `bun run typecheck:bun` (Bun's native +`bun check` across the root, web, and server projects). Let it run instead of +repeating it manually; without the hook, run both yourself. Never bypass it. Run +formatting and related tests separately. + +PR CI runs format, lint, types, the site build, and the full `test:quick` suite. +Releases run `bun run precommit` and must package and inspect every platform +archive and checksum; see [builds](docs/DEPLOYMENT.md#build-a-standalone-executable) +and the [release policy](AGENTS.md#release-notes). + +### Manual browser checks + +Automated tests do not launch browsers. For affected UI, check against a real +backend and record the device/browser and results in the PR: + +- Dialog focus and keyboard navigation, scaled layouts, and HTML preview + isolation (CSP, direct navigation). +- iOS Safari and Android Chrome: keyboard open/close, IME, selecting while output + streams, and landscape/scaled menus. +- Connection recovery, notifications with the page closed, and server-side + settings changes. +- Two tabs on the same origin: preferences sync without reload, while workspace, + files, drafts, and focus stay independent. + +### Windows SSH end-to-end test + +This opt-in test needs a disposable bridge (allowing unauthenticated loopback +WebSockets, such as a dev server) and two Windows profiles connected to dedicated +Herdr 0.9.1+ sessions named `roamgate-ssh-*`, each with a PowerShell pane. Close +browser viewers first. It sends commands, resizes, and reconnects, so **never +point it at real sessions**: ```bash ROAMGATE_LIVE_WINDOWS_SSH_URL=http://127.0.0.1:8788 \ @@ -95,120 +75,67 @@ ROAMGATE_LIVE_WINDOWS_SSH_OTHER_SESSION=roamgate-ssh-other \ bun test server/src/connections/windows-ssh-live.test.ts ``` -The test sends commands to those panes, changes terminal size, disconnects and -reconnects the first profile, and checks session/generation isolation. It leaves -the Herdr servers running. Never point it at working sessions. It is skipped -without these explicit environment variables. - -Workspace types: `bun run --filter roamgate-web typecheck` or -`bun run --filter roamgate-server typecheck` (builds web assets first). -Frontend changes also need `bun run build:web`; bundling needs `bun run build`. -Releases must package/inspect every supported archive/checksum; see -[builds](docs/DEPLOYMENT.md#build-a-standalone-executable) and -[release policy](AGENTS.md#release-notes). - ## Security Checks and Dependency Updates -CodeQL runs the default JavaScript/TypeScript and GitHub Actions query suites on -PRs targeting `main`, pushes to `main`, weekly, and by manual dispatch. It reads -source without building the app or installing dependencies. Only the analysis -job can upload code-scanning results; checkout credentials are not persisted. -Use the `pull_request` event for fork and Dependabot analysis, never -`pull_request_target` with untrusted code. If GitHub's CodeQL default setup is -already enabled, a maintainer must resolve the -[default/advanced setup conflict](https://docs.github.com/en/code-security/reference/code-scanning/sarif-files/troubleshoot-sarif-uploads/default-setup-enabled) -before this workflow can upload results. - -Dependency Audit runs on dependency/config changes in PRs, weekly on the default -branch, and by manual dispatch. Run the same check locally with `bun run audit`. -It audits the root `bun.lock`, including development dependencies, without -installing packages, executing lifecycle scripts, or changing dependencies. -The JSON log includes all reported severities; high/critical findings and -registry request failures return a failing exit code. Review lower-severity -findings too. Fix findings in a reviewed PR; do not hide them with ignored -advisories or a blanket successful exit. This path-filtered workflow is not an -always-running required check for unrelated PRs. - -Dependabot checks the Bun workspace and SHA-pinned GitHub Actions weekly with -small open-PR limits. Minor/patch updates are grouped by package family; major -upgrades remain separate except for the coupled `@xterm/xterm` and -`@xterm/addon-fit` pair. This pair has its own unfiltered group because -Dependabot's minor/patch filters do not match same-core beta increments. Review -all updates to this pair together, including future major upgrades, and check -terminal rendering, sizing, input, and touch scrolling. Other xterm addons keep -the usual minor/patch grouping. Update PRs require normal review and validation; -nothing auto-merges. Bun version updates do not provide Dependabot security -updates, so retain the scheduled audit for newly published advisories against an -unchanged lockfile. - -Coupled runtime packages must be reviewed as a resolved graph, not just direct -manifest bumps. CodeMirror/Lezer, diff2html/highlight.js, and Radix/cmdk have -separate minor/patch groups. Groups collect eligible updates; they do not force -transitive upgrades or override exact upstream pins. After a grouped update, -inspect the lockfile for duplicate runtime copies and run the highlighting and -overlay regression tests. Use a targeted `bun update '@lezer/highlight' --filter -roamgate-web` or `bun update '@radix-ui/*' --filter roamgate-web` when compatible -transitive dependencies need refreshing, then review the resulting diff. Do not -refresh unrelated packages or raise asset limits to make an update pass. - -Major build-tool upgrades remain individually reviewed migrations. Preserve the -browser target and lazy-loading boundaries, validate worker initialization and -the built asset graph, and keep the existing size/count budgets. A failing CI -check is a signal to repair or review the update, not to disable the check. - -The root highlight.js override keeps diff2html's optional, exact-pinned runtime -aligned with the directly imported extra grammar. Update it together with -`web/package.json` and validate the syntax tests; remove the override when -upstream diff2html accepts the supported version. Do not upgrade only the direct -manifest entry while leaving the renderer on its old transitive version. +- **CodeQL** scans JavaScript/TypeScript and GitHub Actions on PRs, pushes to + `main`, and weekly. Use `pull_request`, never `pull_request_target`, for + untrusted code. +- **Dependency Audit** (`bun run audit`) checks `bun.lock` on dependency PRs and + weekly. High/critical findings fail. Fix findings in a PR; do not suppress them. +- **Dependabot** opens weekly grouped minor/patch PRs; majors are separate. + Nothing auto-merges. + +When reviewing updates: + +- Review `@xterm/xterm` and `@xterm/addon-fit` together and check terminal + rendering, sizing, input, and touch scrolling. +- CodeMirror/Lezer, diff2html/highlight.js, and Radix/cmdk are coupled: check the + lockfile for duplicate copies and run the highlighting and overlay tests. Use a + targeted `bun update '' --filter roamgate-web` if transitive versions need + refreshing. +- The root `highlight.js` override keeps diff2html aligned with the directly + imported grammar; update it together with `web/package.json`. +- Treat major build-tool upgrades as migrations: keep the browser target, + lazy-loading boundaries, and asset size budgets. A failing check means fix the + update, not disable the check. ## Style Organization | Path under `web/src/` | Responsibility | | --- | --- | | `styles/tokens.css` | Theme variables (`:root`, `data-theme`, `data-accent`) | -| `styles/base.css` | Resets/shared primitives: modals, forms, badges, statuses, panels, loading | -| `styles/vendor.css` | Shared syntax/diff overrides; consumer-specific overrides stay with components | +| `styles/base.css` | Resets and shared primitives: modals, forms, badges, panels | +| `styles/vendor.css` | Shared syntax/diff overrides | | `styles/layout/*.css` | App-shell regions, imported once by `App.tsx` | -| `components/.css` | Component-owned styles, imported/deleted with the component; same for `components/ui/` | +| `components/.css` | Component styles, imported and deleted with the component | -Prefix classes with the component name; keep media queries beside their rules, -not in a separate mobile stylesheet. Shell/Suspense fallback styles must load -before lazy content. Independently loaded features need explicit shared imports -or global base styles; never depend on another feature having opened. Shared -co-located CSS is valid when static imports cover every rendering path. +Prefix classes with the component name and keep media queries beside their +rules. Lazily loaded features must import the shared styles they need rather +than rely on another feature having loaded them. ## Pages Website and Tutorial -For repeatable product images, use the [screenshot workflow](docs/SCREENSHOTS.md) -with an independent clone of the public -[Northstar demo](https://github.com/powerfooI/northstar-demo). - -Edit `site/` for the landing page; **only `docs/TUTORIAL.md`** for tutorial text. -`scripts/build-pages.ts` renders the tutorial template, rewrites references, and -checks links/fragments in `.pages-dist/`: +Edit `site/` for the landing page and **only `docs/TUTORIAL.md`** for tutorial +text. `scripts/build-pages.ts` renders the tutorial and checks links in +`.pages-dist/`: ```bash bun test scripts/pages-content.test.ts scripts/pages-workflow.test.ts bun run build:site ``` -Serve `.pages-dist/` to check `/tutorial/`, narrow layouts, keyboard navigation, -and JavaScript-disabled reading. Canonical/social/sitemap URLs use -. Never commit generated output. - -**Deploy Pages** runs on `main` pushes or manual retry. Upload requires a published -Roamgate release as GitHub Latest; the installer probe blocks missing assets, -HTTP/network failures, and source-only builds. After repo renames, align the -site installer URL and workflow probe. +Serve `.pages-dist/` to check `/tutorial/`, narrow layouts, and keyboard +navigation. Never commit generated output. **Deploy Pages** runs on pushes to +`main` and requires a published Latest release. For product images, follow the +[screenshot workflow](docs/SCREENSHOTS.md). ## Pull Requests -Use focused imperative commits. PRs describe behavior, verification, and -compatibility impact. Screenshots are not required for UI changes; capture or -upload them only when explicitly requested. Do not commit screenshots solely for -PR review. Keep generated assets/binaries out of Git. +Use focused, imperative commits. PR descriptions cover behavior, verification, +and compatibility impact. Screenshots are not required for UI changes unless +requested; never commit them just for review. Keep generated assets and binaries +out of Git. -Unlabeled PRs receive `documentation`, `dependencies`, `bug` (fix titles), or -`enhancement`; release preparation uses `skip-changelog`. Override with a -`.github/release.yml` category before merging. Contributions are MIT-licensed. +Unlabeled PRs are labeled automatically (`documentation`, `dependencies`, `bug`, +or `enhancement`); correct the label before merging if needed. Contributions are +MIT-licensed. diff --git a/FEATURES.md b/FEATURES.md index bdc212d2..e3d1ea81 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -6,608 +6,215 @@ A Web/PWA client for a running [Herdr](https://herdr.dev) server. ## Workspace, Tab, and Pane Navigation -- Create, rename, pin, switch, and close workspaces/tabs. Linked Git worktrees - group under their repository; pins and collapsed groups stay in this browser. - Double-click a tab to rename it, or use its context menu. -- Workspace creation expands `~/` on the connected host, checks that the - directory exists, and suggests subdirectories as you type. Use an absolute - path or `~/`; leave the field blank to preserve Herdr's default cwd policy. -- Pinned tabs lead the tab strip and cannot be closed from Roamgate; their - panes still close while another pane remains. Herdr keeps tab ids across - restarts, so pins persist until the tab closes elsewhere, such as in the TUI. -- Drag a tab with the mouse to reorder it within its pinned or unpinned group; - Escape cancels. The order is saved in Herdr (`tab.move`), and numbered tab - shortcuts address tab-strip positions. Dragging is enabled after the bridge - confirms Herdr 0.7.2+ support, including legacy connections. -- Split right/down, resize, focus neighbors, move panes to swap with a - neighbor, zoom, or close panes. Hover over a pane to reveal a drag handle at - its top center. Drag it onto another pane in the same tab to swap positions; - Escape cancels. Focus the handle and use arrow keys for directional moves, - or click the toolbar's **Move pane** button. -- Search workspaces, worktrees, files, tabs, panes, and agents in the command - menu. Enter a relative/absolute path to open a file. -- Desktop **Zen mode** hides app chrome and runs the terminal flush to every - edge. Hover **Exit Zen** at the top to pull the topbar back, or click it to - leave; the sidebar restores its previous state on exit. - -Endpoint connections use **Local navigation** per browser/connection; legacy -connections use **Shared navigation**. Reconnect preserves selections; reload -starts from Herdr's selection. Same-tab pane focus, topology, and terminal sizes -remain shared. Creation preserves Herdr's cwd policy and requires a connected -source terminal, except for the first workspace in an empty session. -See [compatibility](docs/DEPLOYMENT.md#herdr-compatibility). +- Create, rename, pin, reorder, switch, and close workspaces and tabs. Linked Git + worktrees group under their repository. Pinned tabs lead the tab strip and + cannot be closed from Roamgate. Drag-to-reorder needs Herdr 0.7.2+. +- New workspace paths accept absolute paths or `~/` and suggest subdirectories + as you type. Leave the field blank to use Herdr's default directory. +- Split, resize, focus, swap (drag the handle at a pane's top center), zoom, and + close panes. +- The command menu searches workspaces, worktrees, files, tabs, panes, and + agents, and opens typed file paths. +- Desktop **Zen mode** hides app chrome so the terminal fills the window. + +Each browser keeps its own workspace and tab selection on endpoint connections; +older (legacy) connections share navigation. Pane focus, layout, and terminal +sizes within a tab are always shared. See +[compatibility](docs/DEPLOYMENT.md#herdr-compatibility). ### Recent Pane Switcher -Switch across tabs/workspaces using the 12 most recently focused live panes plus -the current layout. Use Up/Down; release the opening modifier or press Enter to -switch, Esc to cancel. On macOS, hold Ctrl and repeat Tab (Shift reverses). -Entries show workspace, tab, pane ID, cwd, and agent status; closed panes leave -history. Press K while the switcher is open to start searching, even while -holding the opening modifiers (and Shift when cycling backwards). +Switch between the 12 most recently focused panes across tabs and workspaces. +On macOS hold Ctrl and press Tab (Shift reverses); release the modifier or press +Enter to switch, Esc to cancel. Press K to switch to search mode. ### Pane Search -`Alt+K` opens the same switcher in search mode over **every** live pane, not -just recent ones, so a pane never visited stays reachable. Filter on workspace, -tab, cwd, or agent name/status; terms match in any order, all must match, and -results keep their recent-first order. Up/Down or Tab/Shift+Tab move, Enter -switches, `Alt+K` or Esc closes. Search never commits on a modifier release, so -it stays open while typing. K (including with the opening modifiers held) or -`Alt+K` inside the recent switcher converts it in place without opening the -command menu. The current pane stays listed for context, but selection starts on -the first pane a jump can reach. +`Alt+K` searches **every** live pane by workspace, tab, cwd, or agent +name/status. All terms must match, in any order. Enter switches; `Alt+K` or Esc +closes. ## Full Browser Terminal -- Server-rendered terminals support normal input, modified Enter, macOS editing - keys, multiline paste, IME, and CJK punctuation. -- Scroll by wheel, trackpad, touch, or explicit half-page history shortcuts. - Full page keys route to terminal apps or Herdr history; unavailable endpoint - history controls explain missing support. -- Mouse-aware apps receive pane-local clicks/drags/wheels. Select browser text - with Option-drag (macOS) or Shift-drag (elsewhere); ordinary output needs no - modifier. Selection freezes presentation until cleared. Desktop edge-drag - captures offscreen rows; release, blur, or changed content stops scrolling. -- Paste images or drop files from the desktop onto a terminal pane to upload - them to that pane's host and insert their paths without sending Enter. Drop - explorer rows to insert paths without uploading. The Input Composer accepts - any file through its picker or drop target and inserts paths into its draft. - Uploads are stored in private per-user temporary folders. Expired folders - are swept when the connection starts and hourly after 24 hours by default. - Inserted paths are absolute and quoted when needed. POSIX hosts use POSIX - shell quoting; Windows uses forward slashes and quoting for PowerShell and - common Git Bash paths. Windows paths containing an apostrophe together - with `$` or a backtick require PowerShell; that combination is not supported - in Git Bash. Windows `cmd.exe` built-ins are not supported by this path format. - OSC 52 clipboard writes follow Herdr's foreground recipient, not proven source - pane ownership; see [clipboard limits](docs/DEPLOYMENT.md#herdr-compatibility). -- `Cmd/Ctrl`-click HTTP(S) links to open a browser tab; file/directory paths, - including Windows drive and backslash paths, open preview/workspace actions. - Touch uses long-press, then **Open link** or - **File actions**. Nothing opens on hover or ordinary touch. - -Herdr 0.9.1 supports read-only wrapped-link resolution; OSC 8 keeps explicit -full destinations. Viewport-clipped plain URLs cannot be recovered safely. -Older servers retain local URL/path detection; legacy touch lacks explicit OSC 8 -metadata. See [link contracts](docs/ARCHITECTURE.md#links). +- Server-rendered terminals with full keyboard input, IME, CJK punctuation, and + multiline paste. +- Scroll with wheel, trackpad, touch, or page shortcuts. Mouse-aware apps + receive clicks and drags; hold Option (macOS) or Shift to select text instead. +- Paste images or drop files onto a pane to upload them to that pane's host and + insert their quoted paths (without pressing Enter). Dropping explorer rows + inserts paths without uploading. Uploads expire after 24 hours by default. +- `Cmd/Ctrl`-click opens URLs in a browser tab and file paths in the preview. + On touch, long-press and choose **Open link** or **File actions**. +- OSC 52 clipboard writes go to the browser that most recently typed into the + foreground session, which is not necessarily the pane that produced the + write; see [clipboard limits](docs/DEPLOYMENT.md#herdr-compatibility). + +Herdr 0.9.1+ resolves URLs that wrap across lines. See +[link contracts](docs/ARCHITECTURE.md#links). ## Workspace Inspector -Open **Files**, **Changes**, **Commits**, or **Agent History** from the Inspector button, -keyboard shortcuts, or workspace/agent menus. - -- Dock right/bottom, resize, or expand without unmounting terminals. Narrow - layouts use drill-down/overlay views; Esc dismisses transient UI, not Inspector. -- Tab switches keep Inspector open; workspace switches restore checkout-scoped - selection/layout. Files and Changes save separate widths. Closing returns to - the originating tab if it still exists. -- Agent browsing starts at its cwd only inside the checkout. Terminal links use - their pane's workspace. Changes describe the checkout, not agent ownership. -- Single-clicking a file opens a temporary preview tab; the next single-click - reuses it. Double-click a file or its tab name to keep it open, or press Enter - on the focused preview tab. Reopening Inspector or reloading restores the open - files and active tab per connection and checkout. Reopening a file focuses its - existing tab; identical filenames show their parent paths. Tabs scroll - horizontally on compact layouts. Use Left/Right, Home/End on a focused file tab - to switch, and Delete, middle-click, or the close button to close it. Unfinished - review comments stay attached to their file when switching tabs. -- Files searches names/paths or text across the selected checkout, including - unopened directories. Content results open the preview at the matching line. - Git ignore rules and binary files are respected; results stop at 100. -- Commits browses the current branch in pages. Select a commit to see its - metadata, changed files, diffs, and historical file previews. Merge commits - compare with their first parent; shallow history boundaries are labeled. -- Closed worktrees must open before browsing; missing ones offer cleanup, never - sibling files. Directory previews can prefill **New workspace**. -- The expandable **PR/MR status** card shows the current branch's GitHub pull - request or GitLab merge request, including draft/state, author, branches, - checks/pipeline and provider-reported review/approval status. `origin` is the - default source remote when available; select another remote or a matching - request when needed. **Refresh PR/MR** shows its last refresh time, and the - linked PR/MR number opens the provider. Missing status - is shown as unavailable. This card performs no remote mutations. - Install and authenticate `gh` or `glab` on the checkout host (the SSH host for - SSH connections); configured self-hosted instances are detected through the - CLI. Credentials stay on that host. Fork lookups include the parent project. - -Inspector and Annotations leave the sidebar unchanged. Preview is read-only; -[resource ownership](docs/ARCHITECTURE.md#workspace-resource-ownership) prevents -cross-worktree state mixing. +Open **Files**, **Changes**, **Commits**, or **Agent History** from the +Inspector button, shortcuts, or workspace/agent menus. + +- Dock right or bottom, resize, or expand. Narrow layouts use drill-down views. +- Single-click opens a temporary preview tab; double-click keeps it. Open files + are restored per connection and checkout. +- Files search covers names, paths, and file contents across the checkout + (respecting `.gitignore`, up to 100 results). +- Commits browses the current branch with per-commit diffs and file previews. +- The **PR/MR status** card shows the branch's GitHub pull request or GitLab + merge request: state, checks, and reviews. It needs `gh` or `glab` + authenticated on the checkout host and never changes anything remotely. + +Inspector state is scoped to each checkout; see +[resource ownership](docs/ARCHITECTURE.md#workspace-resource-ownership). ## Agent Awareness and Session Inspection -- See recognized agents and working/blocked/done/idle status; focus their panes - from the tree, switcher, command menu, Agent panel, or notifications. -- Choose nested agents, **Agents: Separate**, or **Agents: Compact**. Separate - supports attention-first, workspace/manual ordering, grouping, and dragging - in ungrouped manual mode. Idle recency uses session-file activity, falling - back to Herdr state changes. Preferences are browser-local; manual order is - per connection. -- **Agent History** has User/Agent/Tool filters, loaded-text search, a minimap, - and on-demand tool details. Its recent window is 200 conversation entries plus associated tools; - exports stay complete. **Session Inspector** adds metadata, Timeline, - searchable ATIF/raw transcripts, and original/normalized export. -- **Menu > Configuration > Integrations** installs, updates, or uninstalls - Herdr's bundled integrations with confirmation, not agent applications. - Changes affect the connected server user across sessions, including SSH. - Restart agent sessions if reporting has not started. Version metadata uses - Herdr's API with a same-host CLI fallback; “available” means bundled, not an - online release. See [integration version lookup](docs/ARCHITECTURE.md#agent-integrations). +- See recognized agents and their working/blocked/done/idle status in the + sidebar, switcher, command menu, Agent panel, and notifications. +- Choose nested, **Separate**, or **Compact** agent views with optional + attention-first ordering and grouping. +- **Agent History** shows the latest 200 conversation entries with + User/Agent/Tool filters, search, a minimap, and tool details. Exports are + always complete. **Session Inspector** adds metadata, a timeline, raw/ATIF + transcripts, and export. +- **Menu > Configuration > Integrations** installs, updates, or removes Herdr's + bundled agent integrations (not the agents themselves). See + [version lookup](docs/ARCHITECTURE.md#agent-integrations). Session inspection supports **Codex, Claude, Kimi, Grok Build, Pi, Muse Code, -and Antigravity CLI** with readable records. Missing metadata shows integration -guidance where an integration is available. Muse uses the newest retained session -matching the foreground working directory under -`$XDG_DATA_HOME/muse/sessions` (default `~/.local/share/muse/sessions`), or a -Herdr-reported session ID/path. Start Muse without `--no-session-log` to retain -transcripts; sessions in the same directory require an explicit ID/path to -distinguish them reliably. SSH reads Herdr-reported paths and Pi ID lookups -remotely; Muse requires a reported path over SSH and never searches local files -for a remote pane. Other ID/directory fallbacks remain local and require -accessible transcripts. Muse token totals use per-run provider routing; when a -cached-token convention is unknown, totals show `-` rather than an estimate. +and Antigravity CLI**. Transcripts must be readable by the bridge. Over SSH, +Roamgate reads the session path Herdr reports; it never searches local files for +a remote pane. Muse needs session logging enabled (no `--no-session-log`). See [History synchronization](docs/HISTORY.md). ## Ranger -Ranger is an experimental workspace management assistant. - -Open **Ranger** from the global topbar button on desktop or the navigation -capsule on mobile, or use its configurable keyboard shortcut. Desktop uses a -floating window like Annotations; **Pin Ranger** reserves space beside the -current workspace. Mobile uses a full chat surface and hides the navigation -capsule until Ranger closes. -Pinning keeps Ranger's width. Drag its left edge, or focus the edge and use -Left/Right, to resize the docked panel; double-click the edge to reset its width. -Use **Maximize Ranger** to fill the workspace area and **Restore Ranger** (or -Escape) to return to the previous layout. -The conversation stays available when switching workspaces or connections. - -- Connect a provider and choose a model through the window's **Settings** button. - Use a separate Ranger connection or explicitly reuse the bridge account's - saved Pi credentials with **Shared Pi credentials**. Settings is one page with - **Model** and **Workspace access** sections and a single **Save settings** - button, which notes unsaved changes. The searchable **Provider** picker lists - saved credentials first; the selected provider's status and its sign-in or - API-key action appear below it. Under **Custom models**, **Add** opens an - editor for new model IDs (keeping the selected custom endpoint) and **Edit** - opens the selected custom model. Enter IDs separated by commas or newlines and - check the supported efforts from Off, Minimal, Low, Medium, High, Extra high, - and Maximum. Leaving the efforts unchanged preserves existing capabilities. - **Save custom models** returns to Settings; **Back** or **Cancel** abandons - unsaved editor changes. Shared mode also reads Pi's custom models. - See [model setup](docs/DEPLOYMENT.md#ranger-model-connection). -- Change the model and **Thinking effort** with the compact selectors below the - chat input. Search models across connected providers. Effort choices come from - the selected model; **Default** shows its effective level, and models without - adjustable thinking show a disabled control. Changes are saved on the bridge - and apply to the next message, including when a reply is still streaming. - They preserve your draft, conversation, and workspace permissions. -- Allow workspaces under **Workspace access** in Settings. Each question - automatically uses saved authorized workspaces that are currently available. - **Select all** adds the currently available workspaces; **Clear** removes every - selection. Use **Save settings** to apply permission changes. - Workspace choices use a compact grid that scrolls when the list is long. - No workspaces are authorized by default. In normal mode, new workspaces need - to be selected separately. Confirmed deleted workspaces are removed from the - list and saved selection; temporary disconnects preserve saved choices. - The scope stays fixed while Ranger streams an answer. -- Type **@** in the message input to search authorized workspaces and concrete - Agent sessions. Candidates show their host and workspace so names can be - distinguished across connections. Use Up/Down and Enter or Tab to select; - Escape closes the picker and Shift+Enter inserts a new line. Selected objects - appear as removable references below the input and remain bound when switching - chats. Editing a reference's text removes its binding; ordinary pasted names - are plain text. Click a reference in the conversation to open its verified - workspace or Agent pane. - References focus the question without changing workspace permissions or - sending prompts. Ranger reads evidence as needed. Changed or unavailable Agent - sessions require reselection, and monitoring tasks preserve the selected - session instead of following a replacement Agent in the same pane. -- Expand **Work performed**, then an individual tool card to inspect its - **Arguments** and **Result** or **Error**, including tools that manage tasks or - propose actions. Long details are marked when truncated; older calls show - details when their execution logs can be located. Ranger can read - workspace status, agent history, changes, and recent terminal output. Source - links show when the evidence was read and open the corresponding workspace - or pane; unavailable or replaced connections require fresh evidence. - Reads and source cards collapse together when each answer finishes; expand - them again to inspect the evidence. - Ranger chooses how many recent terminal lines to read (1-1,000; default 120), - with the newest 32,000 characters retained if the output exceeds its budget. -- Ask Ranger to create a workspace, worktree, or terminal tab, split a pane to - the right or below, start an agent in a pane, or send a prompt and supporting - context to an agent. Ranger can also close an agent, pane, or workspace. - Closing an agent closes its terminal pane and stops its running processes; - closing a workspace closes its panes. These operations do not delete Agent - history, checkout files, or Git worktrees. Individual pane/Agent closes in a - multi-pane worktree-root workspace are unavailable because Herdr cannot safely - prevent an implicit linked-workspace group close. Each operation - appears as a preview with its host, workspace, parameters, and full text to be - sent. **Confirm action** executes that exact proposal; **Cancel** dismisses it. - Tab and split previews show the working directory and source pane when needed; - completion receipts identify the new tab or pane. Ask Ranger to read the updated - status before starting an agent there. - Worktree previews include configured setup hooks. Results distinguish verified - completion from partial or uncertain outcomes. An uncertain operation must be - checked at its target before proposing another one; reconnects never replay it. - In normal mode, newly created workspaces must be explicitly allowed before - Ranger can use them. -- **High-permission mode** is in the permissions menu below the chat input and - is off by default. Enable it explicitly - to authorize all current and newly discovered workspaces and let Ranger - execute supported management operations and manage scheduled - tasks without individual confirmations. Results remain visible in the chat. - Each question and task keeps its concrete captured scope. Disable the - mode to restore normal-mode selections and require confirmation for subsequent - operations. Older saved High-mode settings keep their selected-workspace scope - until the broader permission is explicitly confirmed. Existing - tasks keep their saved permission mode; edit a task through the Tasks form - while this mode is enabled to allow automatic operations in its future runs. -- Closing the window hides it while work continues; the topbar indicator shows - activity. **Stop** cancels the current turn. **New chat** saves the current - conversation and starts an empty one, keeping the model connection and workspace - permissions. **History** opens a full-panel list of saved conversations by their - first question; select one to read its messages and continue chatting. Switching chats retires - unconfirmed action previews. -- The wave bar marks messages and highlights the visible portion of the - conversation. It sits below the messages on narrow panels and mobile, and on - the right in wider desktop panels. Hover to preview a message, click to jump, - or focus it and use Left/Right for the bottom bar or Up/Down for the right bar; - Home and End jump to the first and last messages. Jumping back pauses automatic - following until you return to the bottom or send another message. - -The bridge stores saved conversations and one active chat shared by its -authenticated browsers/devices; messages survive bridge restarts. A reconnect -fetches current progress without resending a question. Unfinished questions can -resume after a bridge restart once the original connections and workspace -identities have been verified. Stopping the bridge pauses a question; **Stop** -cancels it. Confirmed management operations are never automatically replayed. -Unsent drafts stay in the -current browser tab's memory, separately for each conversation. -The **Chat / Tasks** switch keeps scheduled work separate from conversations. -The Chat toolbar provides direct **History** and **New chat** buttons. - -Create a task in **Tasks**, or ask Ranger for a schedule. Confirm its preview in -manual mode; high-permission mode enables it directly. Schedules support one UTC -timestamp, a daily time in an explicit -timezone, or an interval in whole minutes. The task fixes its workspace scope and -model at creation. It runs with the browser closed and retains separate results -and tool activity for each run; reopening Tasks shows the current progress. -Daily schedules skip nonexistent daylight-saving times and run only once at a -repeated time. Missed occurrences are combined into one run after restart. - -Enable **Task notifications** to receive Ranger alerts. The task's -**Notifications** setting defaults to **Notify when each run finishes**. -Choose **Let Ranger decide** for monitoring: routine successful checks stay -quiet, and Ranger can send a custom title and message when the condition in -your task prompt warrants attention. Failed Ranger runs and pending action -confirmations still use fixed alerts in either mode. - -For example, ask Ranger to check an Agent every minute and notify you only -when its work finishes or fails, then confirm the proposed task in manual mode. -Ranger reads -the authorized workspace context and reports the evidence it finds; an idle -Agent alone does not establish success. Each run can issue one custom notice, -and repeated events are deduplicated across checks and restarts. -**Task completed** controls completion alerts; **Task needs attention** controls -attention alerts. Click **Open Ranger task** or a system notification to view -the associated run. Web Push delivers alerts with the page closed while the -bridge is running. Stopped or cancelled runs and already recorded results stay -silent. - -Ask Ranger to edit a task's title, prompt, schedule, or notification mode, or -pause, resume, cancel, or delete a task. Chat edits preserve the saved workspace -scope, model, and Agent references. In manual mode, review and confirm the -exact task change; a manually confirmed edit makes its future runs manual too. -High-permission mode applies supported changes directly without escalating a -task previously saved in manual mode. A changed or expired preview requires a -fresh proposal. - -**Pause** prevents future runs without interrupting the current run. -**Stop run** ends only the current run. **Cancel task** ends current work and -future scheduling while preserving its history. **Run now** starts an extra run; -paused tasks stay paused after a manual run or an edit. Cancelled tasks can be -deleted with their history. Each task retains the latest 20 runs, with at most 50 -stored tasks. -Task plans, run receipts and durable execution data are persisted in SQLite on -the bridge host. -Automatic runs may read and propose operations. Management operations need -confirmation in the run details unless the task was saved with high-permission -mode and that mode is still enabled globally. Task execution does not replace -the active chat. Workspace permissions cover both reads and management operations. -Ordinary Roamgate access still grants the -[administrative authority](SECURITY.md#trust-model) described by its trust model. +Ranger is an **experimental** assistant that answers questions about your +workspaces and can manage them. Open it from the topbar, the mobile navigation +capsule, or its shortcut. + +- **Setup:** connect a model provider in **Settings** and choose which + workspaces Ranger may access (none by default). See + [model setup](docs/DEPLOYMENT.md#ranger-model-connection). +- **Ask:** Ranger reads workspace status, agent history, diffs, and terminal + output, and cites its sources. Type **@** to point it at a workspace or agent. +- **Act:** Ranger can create workspaces, worktrees, tabs, and splits, and start, + prompt, or close agents and panes. You confirm each action unless + **High-permission mode** is on. Closing never deletes files or history. +- **Schedule:** the **Tasks** view runs a prompt once, daily, or every N minutes + while the bridge is running, and can notify you when a run finishes or when + Ranger decides something needs attention. + +Conversations are saved on the bridge and shared across your devices. Ranger +has the same [full authority](SECURITY.md#trust-model) as any Roamgate user. ## Git Worktree Lifecycle Open **Worktree Lifecycle** from a workspace menu or the command menu to: -- Create a linked worktree from `origin`'s freshly fetched default branch, - without changing the source branch or dirty files; discover/open existing - worktrees. The default branch is queried from the remote, not cached `origin/HEAD`; - an unavailable remote or unresolved default branch stops creation. -- Inspect paths, open/closed state, branch status, and uncommitted counts. -- See GitHub PR / GitLab MR merge badges in **Worktree Lifecycle** and **Open - Worktree**, including checkouts without an open workspace. Expand a badge for - details and its last check time; use the list's Refresh button to query again. - No match, ambiguous matches, unsupported hosts, and unavailable CLI/auth or - requests are explicit states. Status uses host-side `gh` / `glab` authentication - and refreshes when the connection, repository, or checkout branch changes. -- Focus, pull, configure branch updates, or remove with confirmation, hooks, - process cleanup, and preservation of residual files when safe removal fails. - -Worktree creation and automatic branch updates fetch the advertised default -commit by its object ID without rewriting `FETCH_HEAD`. They refresh the matching -`origin/` tracking ref so ahead/behind badges reflect that commit; -a conflicting concurrent tracking-ref change stops sync rather than overwriting it. -Symbolic, case-conflicting, and prefix-conflicting tracking refs are preserved; -pruning remains an explicit Git operation. Stale tracking-ref names do not block creation or sync, -and both operations use the resolved commit rather than a ref that another fetch -could change. Concurrency checks compare commit IDs; do not reconfigure symbolic -tracking refs while sync is running. +- Create a worktree from `origin`'s freshly fetched default branch without + touching the current checkout. Creation stops if the remote is unavailable. +- See each worktree's path, branch status, uncommitted changes, and PR/MR badge. +- Focus, pull, configure branch updates, or remove worktrees (with confirmation + and hooks). ### Worktree Hooks -`roamgate.json` (with legacy `paseo.json` compatibility) supports `setup`, `opened`, `teardown`, and `removed` hooks. -**Hooks are enabled by default and execute trusted, unsandboxed repository code** -on the connected host. Review them or disable them under **Worktree hooks** / -**Worktree Lifecycle** before acting. Failed teardown stops removal; other hook -failures do not undo completed operations. -See [configuration and variables](docs/DEPLOYMENT.md#worktree-hooks). +`roamgate.json` (or legacy `paseo.json`) defines `setup`, `opened`, `teardown`, +and `removed` hooks. **Hooks are on by default and run unsandboxed repository +code** on the connected host; review or disable them under **Worktree hooks**. +A failed teardown stops removal. See +[configuration](docs/DEPLOYMENT.md#worktree-hooks). ### Automatic Branch Updates -Opt in per checkout through workspace menus, Worktree Lifecycle, or -**Configuration > Connection > Automatic branch updates**. Updates fetch and -merge `origin`'s default branch every 10 minutes by default, only while the -workspace is open in that connection. Each update queries the remote's current -default branch; if it cannot be resolved or fetched, the update fails without -merging. Updates skip dirty/detached checkouts, recheck branch/HEAD and worktree -after fetch, and abort conflicts. They never push. +Opt in per checkout to fetch and merge `origin`'s default branch every 10 +minutes while the workspace is open. Dirty or detached checkouts are skipped, +conflicts are aborted, and nothing is ever pushed. ## File Explorer and Preview -- Browse cached trees, toggle hidden files, and filter **loaded** names/paths - with case-insensitive substrings or globs (`*`, `?`, `[]`, `{}`, `**`). - Git badges mark changes; ignored files are dimmed. -- **Browse filesystem** opts into read-only host browsing outside the checkout. - Parent/absolute-path navigation allows preview, copy path, and download. - **Workspace only** restores the tree; refresh or checkout/connection changes - reset this mode. -- Text previews provide highlighting, line numbers, search, and refresh. - Markdown has a Preview/Source switch with the active mode highlighted; - Mermaid fences and `.mmd`/`.mermaid` files - have zoomable diagrams, fullscreen viewing, and source views. Markdown - diagrams expand to their content height. Markdown links navigate within - Inspector; external links open a browser tab. -- Workspace `.html`/`.htm` files open in Preview mode with a Preview/Source switch. - Static previews support workspace CSS, images, and fonts, including relative - CSS imports, plus HTTPS CDN stylesheets. Scripts, external images and external - fonts are blocked; HTML source is limited to 512 KiB. Files outside the workspace - remain available as source or downloads. -- Preview images (including SVG), PDFs, and local Markdown images with zoom/Fit. - Audio files (`.mp3`, `.wav`, `.ogg`/`.oga`/`.opus`, `.m4a`, `.aac`, `.flac`, - `.weba`) up to 25 MiB play in a native audio player with byte-range seeking, - including on SSH profiles. Larger audio files remain download-only. - Unsupported binaries are download-only; decoding depends on the browser. -- Upload by dragging onto a checkout directory; download files or workspace - `.tar.gz` directories; copy paths or delete with confirmation via right-click - or long-press. Upload/delete stay checkout-scoped. Operations work over SSH. - Desktop layout downloads directly, including installed web apps. Mobile - layout keeps native iOS sharing and its browser fallback behavior. -- Create folders in the checkout root with **New folder**, or inside a folder - through its actions menu. Right-click the file tree background to create a - folder in the checkout root or refresh files. Select files with Cmd/Ctrl-click - or Shift-click, then drag them onto a folder (or empty tree space for the - checkout root) to move them. Cmd/Ctrl+A selects visible files; Space selects - the focused file. - Moves never overwrite existing names; failed files remain selected. Moves - require source and destination to be on the same filesystem; SSH moves also - require Python 3 on the remote host. Read-only host browsing does not allow - creating folders or moving files. -- With [host file reveal enabled](docs/DEPLOYMENT.md#host-file-reveal), local - profiles over loopback offer **Reveal on host** and **Open folder on host** - in explorer and Changes menus. These open the Roamgate host's desktop, not - necessarily the browser's: files are selected in Finder/File Explorer; - Linux opens their containing folder. Changes uses the nearest existing - ancestor for deleted paths, including removed directories. Disabled by - default; SSH profiles and non-loopback peers are refused. -- Drag a file or folder from the tree with the mouse to insert its absolute path - into the active terminal pane or the terminal composer. +- Browse the tree with Git status badges, hidden-file toggle, and name/glob + filtering of loaded entries. +- **Browse filesystem** opens read-only browsing outside the checkout. +- Preview text with highlighting and search, Markdown (with Mermaid diagrams), + static HTML (workspace CSS/images/fonts; scripts blocked; 512 KiB limit), + images, PDFs, and audio up to 25 MiB. +- Upload by drag and drop, download files or folders (`.tar.gz`), copy paths, + create folders, move files by dragging, and delete with confirmation. All of + this works over SSH (moves need `python3` on the remote host). +- With [host file reveal](docs/DEPLOYMENT.md#host-file-reveal) enabled, local + loopback clients can open files in the host's Finder/File Explorer. +- Drag a file into a terminal or the composer to insert its path. ## Review Annotations -Open **Annotations** independently of Inspector. Desktop supports floating or -pinned layouts; mobile has a dedicated touch surface. +Open **Annotations** independently of the Inspector. -1. Comment on diff line numbers, source gutters, rendered Markdown selections, - or selected terminal text using **Add comment**. -2. Edit/reorder checkout-scoped comments, then copy feedback or pre-fill a chosen - agent pane. **Delivery never submits**: review and press Enter manually. -3. Use **Go to agent** after pre-fill. Copy retains drafts; successful pre-fill - removes only unchanged delivered comments. Failed delivery, concurrent edits, - or leaving the workspace/connection preserves the original draft. +1. Comment on diff lines, source lines, rendered Markdown, or selected terminal + text. +2. Edit and reorder comments, then copy them or pre-fill an agent's input. + **Pre-fill never submits**; review and press Enter yourself. -With focus inside Annotations, use the configurable shortcuts shown on **Copy** -and **Pre-fill agent**. Copy confirms success with a brief notification. -Drafts stay in this browser and survive panel closure. Blank comments cannot be -sent. Refresh re-anchors matching file/diff content and marks unresolved anchors -stale without losing quotes. Terminal quotes are not re-anchored; missing panes -are marked unavailable. +Drafts stay in this browser and re-anchor to changed files on refresh. ## Diff Viewer -- Review **Working tree**, **Against main**, or **Last step** (the latest completed - activity snapshot, not proof of agent ownership). See staged, unstaged, - untracked, conflict, and branch-diff badges with added/deleted counts. -- Use side-by-side/unified desktop views; mobile is unified. Search and syntax - highlighting work across loaded diffs; images have previews. Non-SSH - connections load nearby diffs as you scroll; SSH loads selected files on - demand. The file index jumps to a file without closing other local diffs. - Generated files marked by `linguist-generated` or `gitlab-generated`, files - of at least 256 KiB, and diffs with at least 1,000 changed lines or 128 KiB of - patch content start collapsed with a **View diff** action. Git attribute - overrides can unmark generated files. Skipped files remain in change totals; - patches exceeding 512 KiB are truncated and labeled. -- File/folder context menus offer open, copy path, stage, unstage, mark resolved, - discard unstaged, and delete untracked, plus repository-wide bulk actions. - Destructive actions require confirmation. Status/content is rechecked to reject - stale menus instead of destroying newer work. - -Scope, view, wrapping, and selection persist per checkout/browser; wrapping is -separate for desktop/mobile. Jump from a diff to its file preview. +- Compare **Working tree**, **Against main**, or **Last step** (the latest + completed agent turn; not proof of which agent made the change). +- Side-by-side or unified view, search, syntax highlighting, and image previews. + Generated and very large files start collapsed. +- Stage, unstage, mark resolved, discard, or delete from file/folder menus. + Destructive actions require confirmation and are rejected if the file changed. ## Mobile and PWA -- **Configuration > Appearance > Layout** selects Automatic/Mobile/Desktop. - Automatic defaults to 768 CSS px (adjustable 320–2560); `?layout=mobile`, - `desktop`, or `auto` overrides saved mode until a menu choice clears it. - Agent/workspace panel order is saved separately for each layout. -- Touch reads output without opening the keyboard. In Direct mode, a light - terminal tap opens the device keyboard. Long-press selects - text for **Copy**, **Add comment**, or link actions; **Done**/Esc exits. - Scroll first to select older output. Selection freezes the displayed frame, - not the connection; legacy streams resume at a 1 MiB buffered UTF-16 limit. -- Customize the floating `2×8` grid and up to four side buttons under - **Configuration > Behavior > Mobile terminal shortcuts**. The Tabs sheet and - pane controls work when the tab strip is hidden. - Choose **Preset** or **Custom keyboard**. The US keyboard lets you toggle Ctrl, - Alt, and Shift and pick one letter, number, symbol, or basic key; expand **More keys** - for navigation keys and F1-F12 in separate groups. Set a label and edit or clear individual slots; - defaults stay available. Buttons send only that combination, with no extra - Enter. Shift uses US symbols. Endpoint terminals preserve ambiguous modified - keys such as Ctrl+/, Ctrl+Backspace, Ctrl+Shift+letters, and Alt+Escape; - ordinary Ctrl aliases (such as Ctrl+I for Tab) remain available. Legacy/shared - terminals retain their traditional byte encodings; combinations available only - on endpoint terminals are disabled there with an explanation. Modified Enter - requires application support. Custom PageUp/PageDown sends application input; the presets scroll - history. Unsupported combinations, such as Ctrl+1, identify the affected slot - and block saving. Buttons bypass browser keyboard - shortcuts, but application keybindings still apply. Cmd/Meta, text macros, - commands, and multi-step sequences are not supported. -- Drag the `⋯` controls button to move the floating controls; release snaps - them to the nearer side edge at that height, mirrored on the left. The - position is kept per browser and stays clear of the header and tab strip - when space allows. - The capsules remain available with Composer, Direct input, and the device - keyboard open. They temporarily move above the input dock without changing - the saved position. Short/scaled viewports use a horizontally scrollable row; - the input dock can scroll too when space is tight. In extremely short views, - reachable controls and input take priority over header clearance. Folding or dragging the - controls preserves typing focus; choosing another view can dismiss the keyboard. -- The bottom-right **Type** button opens one input dock with a - **Composer / Direct** mode switch and the configured two shortcut rows. - The keyboard button shows or hides both rows in either mode and remembers - the choice per browser, including after reload. Swipe the - terminal up or down to scroll with the dock open in either mode; gestures - inside the Composer editor keep their native text-editing behavior. - Composer supports IME, dictation, - multiline text, and images. Tap its editor to open the device keyboard. **Insert** - does not execute; **Send** adds one Enter. Drafts are in-memory per - connection/pane; closing their pane/tab/workspace asks before discarding. - Direct sends keys immediately through the terminal's keyboard input and - preserves the Composer draft. Explicit mode switches are remembered per - browser, including after reload; reopening input restores that choice - (Composer by default). Composer opens the keyboard only when its editor is - tapped; opening the dock, scrolling, and shortcut actions leave it closed. - **Type** opens the keyboard immediately when restoring Direct. - Connection and pane safety resets return the live dock to Composer - without changing the saved choice. Shortcut keys act on the terminal in either mode. -- In the mobile composer, the **terminal command icon (`>_`)** opens a floating - command picker above the input without shrinking the editor or terminal. - In the compact controls layout it stays inside the scrollable input dock so - commands remain reachable without covering the navigation row. - It browses built-in catalogs for Claude Code, Codex, Pi, Kimi Code, Grok Build, - and Antigravity CLI (`agy`) using the pane's agent identity. - Type a leading `/` for prefix completion. Up/Down selects - a suggestion, Tab prepares it, and Escape dismisses; Enter still - inserts a newline. Selection never sends input. Inline completion preserves - arguments; browsing requires confirmation before replacing a non-empty draft. - Catalogs are static; command availability depends on the installed agent's - version and configuration. Unknown agents have no built-in suggestions; - custom commands remain sendable. -- Install as a PWA for an app window; a bundled Nerd Font supplies terminal - icons. **PWA is not offline access**: Roamgate must remain reachable. - See [installation steps](README.md#install-as-a-pwa). -- **Configuration > Instance > Title suffix** gives different Roamgate servers - distinct page and PWA names, such as `Roamgate · Home` and `Roamgate · Work`. - Names are saved on each server and shared across devices. Existing Android - installations may need a browser-approved update or reinstall. - [Instance naming](docs/DEPLOYMENT.md#pwa-instance-names). +- **Configuration > Appearance > Layout** selects Automatic, Mobile, or Desktop + (`?layout=mobile|desktop|auto` overrides it). +- Touch scrolls without opening the keyboard. Long-press selects text for + **Copy**, **Add comment**, or link actions. +- Floating terminal shortcuts (a `2×8` grid plus side buttons) are customizable + under **Configuration > Behavior > Mobile terminal shortcuts** and can be + dragged to either edge. +- The **Type** button opens an input dock with two modes: **Composer** (edit a + draft with IME, dictation, and images; **Send** adds Enter) and **Direct** + (keys go straight to the terminal). +- The `>_` button in Composer offers slash commands for Claude Code, Codex, Pi, + Kimi Code, Grok Build, and Antigravity CLI. +- Install as a PWA for an app-style window; see + [installation steps](README.md#install-as-a-pwa). **A PWA is not offline + access**: Roamgate must stay reachable. +- **Configuration > Instance > Title suffix** names each server (for example + `Roamgate · Home`); see [instance naming](docs/DEPLOYMENT.md#pwa-instance-names). ## Remote, Multi-Client, and Operations -- Shared local/SSH profiles have independent browser selection. Disconnecting - does not stop Herdr. Linux/macOS bridges connect to Unix or Windows SSH hosts; - Windows hosts require Herdr 0.9.1+ and Windows PowerShell. Windows bridges support - native local profiles. [Connection setup](docs/DEPLOYMENT.md#multiple-and-remote-herdr-connections). -- Browsers receive pushed events; inspect client counts or pause/resume clients. - **Configuration > Behavior > Task notifications** independently enables - input-required/completed alerts, following Herdr's own notification decisions - by default. **Background push** works without an active page; **Active page - only** does not. Delivery is best-effort. - [Web Push setup and revocation](docs/DEPLOYMENT.md#web-push-notifications). -- Choose light/dark/system appearance, accents, and interface scale (80%–150%). - **Configuration > Appearance > Terminal** groups the terminal font, font size - (70%–200%, scaling terminal text without resizing the rest of the interface), - and built-in/custom terminal themes. The font can be any family installed on - the viewing device; Chromium browsers can list installed fonts, and missing - glyphs fall back to the default terminal fonts. Preferences stay in this browser. -- **Configuration > Connection > Terminal incremental transport** saves a shared - per-connection setting on the server. It reduces Herdr-to-Roamgate traffic, - briefly reconnecting displays without stopping tasks; older servers retain - their transport. -- Manage [user services](docs/DEPLOYMENT.md#run-as-a-user-service), use - checksum-verified standalone updates under a supported supervisor, and probe - `/health` or `/healthz`. -- [Command-line management](docs/DEPLOYMENT.md#command-line-management): - `roamgate doctor` diagnoses an installation, `url --qr` opens it on a phone, - and `config`, `auth`, `connections`, `push test`, `update`/`rollback`, - `service logs` and shell completions cover routine operations. - -**Normal runtime requires a token or password, including loopback.** The local -development exception, password policy, and login limits are described in -[Security](./SECURITY.md#trust-model). -UI access grants terminal/file authority, not a read-only role; read it before +- Save local and SSH connection profiles; each browser selects independently. + Linux/macOS bridges can reach Unix or Windows SSH hosts. See + [connection setup](docs/DEPLOYMENT.md#multiple-and-remote-herdr-connections). +- Task notifications for input-required and completed agents, delivered by + [Web Push](docs/DEPLOYMENT.md#web-push-notifications) even with the page + closed. Delivery is best-effort. +- Themes, accents, interface scale (80–150%), terminal font, font size, and + terminal color themes. Preferences sync across tabs in the same browser. +- Run as a [user service](docs/DEPLOYMENT.md#run-as-a-user-service) with + checksum-verified updates, and probe `/health` or `/healthz`. +- [Command-line tools](docs/DEPLOYMENT.md#command-line-management) such as + `roamgate doctor`, `url --qr`, `config`, `auth`, and `connections`. + +**Normal runtime requires login, including on loopback, and login grants full +terminal/file access.** Read [Security](./SECURITY.md#trust-model) before sharing access. ## Keyboard Shortcuts -On desktop, press `Cmd+Shift+K` (`Ctrl+Alt+Shift+K` on Windows/Linux), click -the command (`⌘`) button to the left of the top-bar controls, choose **Menu > -Shortcut cheat sheet**, or run **Show shortcut cheat sheet** from the command -menu for a read-only, filterable overview of the active preset. Press the same -shortcut or Esc to close it. - -Open **Menu > Configuration > Behavior > Keyboard shortcuts** for the searchable -reference/editor. Automatic detects your platform; explicit/custom presets -support up to three bindings per action, unassignment, reset, and JSON -export/import. Editing built-ins creates a custom copy. Changes save immediately -and sync to same-origin tabs; conflicts are rejected. - -Common defaults (Linux/Android exceptions follow): +On desktop, press `Cmd+Shift+K` (`Ctrl+Alt+Shift+K` on Windows/Linux) or choose +**Menu > Shortcut cheat sheet** for a filterable overview. Edit bindings under +**Menu > Configuration > Behavior > Keyboard shortcuts** (up to three per +action, with JSON import/export). | Action | macOS / iOS | Windows / Linux / Android | | --- | --- | --- | @@ -641,21 +248,7 @@ Common defaults (Linux/Android exceptions follow): | Terminal paste | `Cmd+V` | `Ctrl+V` (also `Ctrl+Shift+V` on Linux) | | Open terminal links / file paths | `Cmd+Click` | `Ctrl+Click` | -Linux/Android uses `Ctrl+Alt+Shift+T` for new tabs, `Ctrl+Alt+Shift+D` to split -right, and `Ctrl+Alt+Shift+S` to split down, avoiding desktop-reserved bindings. -Letter/number bindings use physical keys. Browsers/OSes can intercept shortcuts; -choose alternatives in the editor or use menus. - -Use **Focus terminal input** to return to the current pane after using other -controls. It reveals the terminal and focuses the draft when Composer is open, -or direct terminal input otherwise. Close open menus or dialogs first. A bright -accent border and glow indicate actual input focus, including single-pane and -zoomed terminals; split panes keep a muted border on the selected pane when -input focus moves elsewhere. - -Copy needs a selection; plain `Ctrl+C` remains terminal input. Page/half-page -navigation and modified Enter are configurable. Native editing/IME/app keys -remain available; remapped paste requires the Clipboard API. Touch shortcuts -have a separate editor. Esc dismisses transient UI; Tab/arrows navigate controls, -except in the pane switcher's search, which keeps typing in its field; -Enter/Shift+Enter advances/reverses diff search. +On Linux/Android, new tab is `Ctrl+Alt+Shift+T`, split right is +`Ctrl+Alt+Shift+D`, and split down is `Ctrl+Alt+Shift+S`, avoiding +desktop-reserved keys. Browsers and OSes may intercept some shortcuts; rebind +them or use the menus. Plain `Ctrl+C` always goes to the terminal. diff --git a/README.md b/README.md index c26f7c59..1ee27c8c 100644 --- a/README.md +++ b/README.md @@ -109,44 +109,41 @@ Click any screenshot to open the full-resolution image. [handoff-demo]: ./docs/images/roamgate-handoff.gif -> **Moving from Herdr Studio / herdr-gui?** Automatic upgrades are not supported. -> Follow the [migration guide](./docs/DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui) -> to install Roamgate manually. +> **Moving from Herdr Studio / herdr-gui?** Follow the +> [migration guide](./docs/DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui); +> automatic upgrades are not supported. ## Quick start -1. Install and start [Herdr](https://herdr.dev), or let Roamgate install and - start it later with `roamgate herdr setup`. -2. On Linux or macOS, install Roamgate: +1. Install and start [Herdr](https://herdr.dev), or let Roamgate do it later + with `roamgate herdr setup`. +2. Install Roamgate on Linux or macOS (to pin a version, end the command with + `| ROAMGATE_VERSION=X.Y.Z sh`): ```bash - # Empty selects latest; use X.Y.Z (no v prefix) to pin a Roamgate version. curl -fsSL \ https://github.com/powerfooI/roamgate/releases/latest/download/install-roamgate.sh \ - | ROAMGATE_VERSION= sh + | sh ``` - On Windows, download the matching x64 or ARM64 archive from the + On Windows, download the x64 or ARM64 archive from the [latest release](https://github.com/powerfooI/roamgate/releases/latest). -3. On Linux/macOS, add `~/.local/bin` to `PATH` and run `roamgate`. - On Windows, extract the archive and run `roamgate.exe`. Open the printed URL. - `roamgate url --qr` shows a QR code for a phone, and `roamgate doctor` - checks the setup when something does not work. +3. Run `roamgate` (`roamgate.exe` on Windows; on Linux/macOS make sure + `~/.local/bin` is on `PATH`) and open the printed URL. `roamgate url --qr` + shows a QR code for your phone, and `roamgate doctor` diagnoses problems. -See [deployment](./docs/DEPLOYMENT.md) for checksums, configuration, updates, -and services, or [historical installation](./docs/DEPLOYMENT.md#install-historical-herdr-studio) -for `herdr-gui` 0.6.2. +See [deployment](./docs/DEPLOYMENT.md) for configuration, services, and updates. ## Install as a PWA -**PWA installation is recommended for daily use:** a separate app window without -browser tabs or the address bar. Open and authenticate with Roamgate, then install: +**Recommended for daily use:** a separate app window without browser tabs or +the address bar. Log in to Roamgate, then install: - **iPhone/iPad Safari:** Share -> Add to Home Screen. - **macOS Safari 17+:** File -> Add to Dock. - **Chrome/Edge:** browser menu -> Install app. -The process must stay running and reachable. **PWA mode is not offline access.** +**A PWA is not offline access:** Roamgate must stay running and reachable. ## Documentation @@ -174,9 +171,10 @@ and pull requests. ## Security -Roamgate controls terminals and modifies real files. Normal runtime requires a -token or password, including loopback. Keep the default loopback binding; read -[SECURITY.md](./SECURITY.md) before allowing another device access. +Roamgate controls terminals and real files. Normal runtime requires login, even +on loopback; only the loopback development server (`bun run dev:server`) skips +it. Keep the default loopback binding and read [SECURITY.md](./SECURITY.md) +before giving another device access. ## License diff --git a/SECURITY.md b/SECURITY.md index 53473560..0275d6bf 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,136 +12,74 @@ GitHub-profile contact address. ## Trust Model -**UI access grants the Roamgate user's authority:** terminals, repository hooks, -session data, and workspace uploads/deletions. This is privileged administration, -not a sandbox or multi-user permission system. - -The default bind is `127.0.0.1`. **Normal runtime requires login**, including -loopback. The only exception is `NODE_ENV=development` with the bind address -exactly `127.0.0.1`, `localhost`, or `::1`: those development listeners bypass -Roamgate authentication, including access through a reverse proxy or tunnel. -`dev:server` enables this local development mode; `start:server` and installed -services do not enable it by default. Do not forward a development listener for -remote access. - -When authentication is enabled, set `ROAMGATE_PASSWORD` to a unique password -of 8-1024 Unicode characters; there are no required character combinations. -Configured passwords outside that range stop startup. Passwords shorter than 15 -characters start with a warning: login grants shell access, so use 15 or more -for any listener reachable beyond a trusted private network. Without a configured password, an authenticated listener uses a -persistent 256-bit random token and reports its protected file path. Read that -file to log in, or use `--open` to open a token URL automatically. Service -installation and URL tools can also provide token URLs; keep them private. - -Session cookies are signed by a separate, randomly generated 256-bit secret with -owner-only permissions in the Roamgate data directory. The default file is -`session-secret.json`. A nonempty `ROAMGATE_SETTINGS_PATH` (or its legacy alias) -selects `session-secret-.json` in that directory, using the full SHA-256 -digest of the normalized absolute settings path. Separate settings paths isolate -signing keys; changing that path requires logging in again. Keep custom paths -absolute and stable. - -The login password/token is never the cookie signing key. Ordinary restarts keep -sessions valid for their original 30-day lifetime. Changing the effective login -password/token or adding, changing, or removing the PIN rotates that instance's -signing secret at startup and invalidates earlier cookies, even if a previous -credential is later restored. Stop all listeners for the same instance before -changing credentials; already-running processes retain their in-memory -configuration. Keep signing state private and persistent, and never copy it into -a different installation. Corrupt or unsafe secret files stop startup. - -Upgrading from password/token-signed cookies requires one new login; legacy -cookies are deliberately not accepted. Removing the instance's signing state -while all its listeners are stopped resets its sessions without changing the -login credential. See [signing state and lock recovery](docs/DEPLOYMENT.md#run-as-a-user-service). - -Password login (`POST /api/login`) and token-URL login share limits by the -connection's source IP: at most 20 attempts per 60-second window, and five -consecutive failures trigger a five-minute cooldown. Successful login clears -the failure count, and failures start counting again after cooldown expires. -Rate-limited attempts return `429` with `Retry-After`. Login JSON bodies are -limited to 16 KiB of actual streamed bytes; oversized bodies return `413`. - -IP records expire after five minutes without an attempt and are removed on the -next login attempt. Without new attempts, expired records remain within the -same bounded table. Each source-IP table holds at -most 4096 active IP records; when full, it returns `429` for new IPs instead of -evicting active cooldowns. This state belongs to one process and resets when -the bridge restarts. Forwarded IP headers are not trusted: clients behind a -reverse proxy or tunnel share its connection-IP limits. Use the proxy's own -limits when per-client enforcement is needed. +**Anyone who can use the UI has the Roamgate user's full authority:** +terminals, repository hooks, session data, and file uploads/deletions. Roamgate +is single-user administration, not a sandbox or multi-user permission system. + +- The default bind is `127.0.0.1`. **Login is required, including on + loopback.** The only exception is `NODE_ENV=development` bound to + `127.0.0.1`, `localhost`, or `::1` (what `bun run dev:server` uses). Never + forward a development listener. +- Set `ROAMGATE_PASSWORD` to 8-1024 characters; use 15 or more for anything + reachable beyond a trusted private network (shorter passwords print a + warning). Without a password, Roamgate generates a persistent 256-bit token + and prints the file that holds it. Keep token URLs private. +- Session cookies last 30 days and are signed by a separate random secret in + the data directory (`session-secret.json`). Changing the password, token, or + PIN rotates that secret on the next start and signs out every browser. Stop + all listeners of an instance before changing credentials. Upgrading from + builds that signed cookies with the password/token requires one new login; + those legacy cookies are deliberately rejected. See + [signing state](docs/DEPLOYMENT.md#run-as-a-user-service). +- Login request bodies are limited to 16 KiB (`413` beyond that). +- Login is rate-limited per source IP: 20 attempts per minute, and five + consecutive failures trigger a five-minute cooldown (HTTP `429` with + `Retry-After`). Limits are per process and reset on restart. Forwarded IP + headers are not trusted, so clients behind one proxy share its limit. ### Optional PIN login -`ROAMGATE_PIN` enables a separate convenience credential only when explicitly -set to 6-12 ASCII digits (leading zeros are preserved). Unset or empty disables -it; other values stop startup. PIN login is intended only for a listener behind -a trusted VPN/private network and firewall. A short PIN is substantially weaker -than a unique password or random token; enabling it reduces login security. -Startup emits a warning. Transport still needs HTTPS or a trusted encrypted VPN. - -The PIN-first form offers **Use password or token instead** for recovery. The -existing password (or generated token when no password is configured) remains -valid. Token URLs keep working in generated-token mode; a configured password -continues to disable generated-token URL login. A PIN is never a URL credential -or a cookie signing key. All login methods grant the same full authority. - -PIN login uses `POST /api/login/pin` with a `pin` field; it cannot bypass its -limits through the password endpoint. It has a separate bounded source-IP table -with the same 20-attempt/minute and five-failure/five-minute limits. In addition, -ten consecutive failed PINs across **all source IPs** disable PIN login for one -hour. Requests blocked by that cooldown do not extend it; it expires automatically. -Successful PIN login resets its global consecutive-failure count. Password and -token login retain their own source-IP budget and remain available during PIN -cooldown, but do not reset it. Invalid or oversized PIN bodies count as failures. - -These counters are process-local and reset on restart; multiple independent -listeners multiply the guessing budget. Do not use PIN login across replicas -without shared outer rate limiting. Forwarded IP headers remain untrusted. -Distributed attempts can temporarily deny the PIN convenience path, so retain -access to the strong credential. If attacked, remove `ROAMGATE_PIN` and restart -all listeners for that instance; changing/removing the PIN also revokes its -existing session cookies. +`ROAMGATE_PIN` (6-12 digits) adds a convenience login for listeners reachable +**only** through a trusted VPN or private network. It is much weaker than a +password or token, yet grants the same full access, and startup prints a +warning. The password/token remains available via **Use password or token +instead**. + +PIN attempts have their own per-IP limits, and ten consecutive failures from +any source disable PIN login for one hour. Limits are per process, so multiple +listeners multiply the guessing budget. If attacked, remove `ROAMGATE_PIN` and +restart **every** listener of the instance; this signs out all browsers, +including password sessions. ### Access and session safety **Do not expose Roamgate directly to the public internet.** For remote access: -- Prefer `ROAMGATE_PASSWORD` to `--password`, which exposes - secrets in process arguments. -- Use [native HTTPS](docs/DEPLOYMENT.md#native-https), an HTTPS proxy, or a trusted - VPN; restrict access with a firewall/reverse proxy. +- Prefer `ROAMGATE_PASSWORD` over `--password`, which is visible in process + arguments. +- Use [native HTTPS](docs/DEPLOYMENT.md#native-https), an HTTPS proxy, or a + trusted VPN, and restrict who can reach the listener. - Treat worktree hooks as executable code. -The bridge checks neither browser Origin nor request Host. Any request reaching -it and passing required authentication has full authority. Secure the outer -access path: native TLS encrypts transport but supplies no multi-user -authorization or sandboxing. Without TLS configuration, the listener -uses unencrypted HTTP. - -**Menu > Log out** removes this browser's authentication cookie, disconnects its -active tabs, and returns to login. It does not stop terminals, change the server -password/token, or log out other browsers. Cookies are stateless signed -credentials: logout removes the browser's copy, but does not revoke a copied -cookie before its expiry. Rotate the server credential if it or a session cookie -has been compromised. - -Updates trust the configured HTTPS release origin (or explicit loopback test -mirror) and its manifest/checksums. Checksums detect corruption and bind the -archive, **not independently verify publisher identity**. Custom mirrors are -trusted executable-code infrastructure. - -`HERDR_GUI_*` aliases `ROAMGATE_*`; explicit new values win, even empty ones. -Auth-token migration preserves the old secret. Protect both copies and backups; -see [migration and rotation](./docs/DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui). -Update requests require normal listener authentication plus `x-roamgate-update: 1`. -Legacy `x-herdr-gui-update: 1` is accepted; the new header wins if both appear. -Neither header replaces login. - -Web Push subscription mutations require listener authentication, JSON, and -`x-roamgate-push: 1`; cross-site browser requests are rejected. Push endpoints -are restricted to supported browser-provider HTTPS hosts and are never followed -through redirects. Treat the private push registry as credentials. Revoking a -login password or logging out does not revoke device subscriptions: disable Web Push or remove -subscriptions separately. Notification payloads can expose agent names and -routing IDs on lock screens. See [Web Push configuration](docs/DEPLOYMENT.md#web-push-notifications). +The bridge does not check `Origin` or `Host`; any authenticated request has full +authority, so secure the access path itself. + +**Menu > Log out** removes this browser's cookie only. It does not revoke copied +cookies or log out other browsers. If a credential or cookie leaks, change the +password (or run `roamgate auth rotate` for a generated token), or run +`roamgate auth revoke-sessions` to sign out every browser. These commands only +restart the managed service: also restart any other listener of the instance +(for example a foreground `roamgate`), and handle each separate +`ROAMGATE_SETTINGS_PATH` instance on its own. + +Updates trust the configured HTTPS release origin and its checksums. Checksums +detect corruption, not publisher identity; treat custom mirrors as trusted code. + +Legacy `HERDR_GUI_*` variables alias `ROAMGATE_*` (new names win, even when +empty). Protect migrated token copies and backups; see +[migration](./docs/DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui). + +Web Push subscriptions are tied to devices, not logins: changing the password or +logging out does not revoke them. Disable notifications or remove subscriptions +separately. Notifications may show agent names on lock screens. See +[Web Push](docs/DEPLOYMENT.md#web-push-notifications). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cca45e01..1045bac0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -13,780 +13,270 @@ Browser (React + Vite) Bridge (Bun + TypeScript) -- node:net --> Herdr sockets ``` -Browsers cannot open Unix sockets or Windows named pipes. The bridge connects -`herdr.sock` (NDJSON control) and `herdr-client.sock` (binary terminal traffic), -serves the frontend, and owns authentication, local/SSH runtimes, host operations, -notifications, health, and updates. React owns presentation and browser-local -preferences; xterm displays Herdr-rendered output, not a bridge-owned PTY. +Browsers cannot open Unix sockets or named pipes, so the bridge connects to +`herdr.sock` (NDJSON control) and `herdr-client.sock` (binary terminal traffic). +It also serves the frontend and owns authentication, local/SSH runtimes, host +operations, notifications, health, and updates. React owns presentation and +browser-local preferences; xterm displays output that Herdr renders. Browser RPC sends `{ id, method, params }` and receives `{ id, result }` or -`{ id, error }`. Subscribed events use `{ event: ... }`; downstream traffic also -carries connection identity. Subscription acknowledgements trigger snapshots; -events during refresh queue another refresh. This reconciles missed changes, -not an atomic or replayable event log. +`{ id, error }`. Events arrive as `{ event: ... }`. Subscribing triggers a fresh +snapshot, and events during a refresh queue another refresh: this reconciles +missed changes but is not a replayable event log. ## Connection isolation -A bridge-global `ConnectionManager` owns shared profiles and independent -`ConnectionRuntime` instances. Each runtime owns its transport, viewers, -subscriptions, clipboard relay, services, caches, and reconnect lifecycle. -Render streams open only while viewed. Disconnect/removal stops the runtime and -SSH tunnel, not Herdr or its workspaces. - -- Downstream RPC/HTTP requires an immutable connection ID, runtime generation, - and request-local ready-runtime lease. Replies, events, frames, and clipboard - pushes carry that identity; HTTP streams recheck it per chunk. -- Replacement retires leases before publishing status. Already-dispatched effects - may finish on the original host, but retired results cannot publish. Explicit - malformed, unknown, stale, or unready identities fail without fallback. - Omitted identities and legacy HTTP aliases are a bounded, logged compatibility - path for older single-connection clients only. -- Authentication, health, updates, client accounting, and profile management are - bridge-global and independent of downstream readiness. Global RPC rejects - misleading connection fields. Starts/stops serialize; shutdown is bounded; - one failed runtime does not block healthy connections or management. -- Browser caches, storage, mount keys, notifications, and async actions are - connection-scoped. Switching connections retires the browser lease; same-ID - runtime replacement clears active and inactive sessions before IDs can recur. +`ConnectionManager` owns the shared profiles and one `ConnectionRuntime` per +profile. Each runtime owns its transport, viewers, subscriptions, caches, and +reconnect lifecycle. Disconnecting stops the runtime and SSH tunnel, never Herdr. + +- Every downstream request carries a connection ID and runtime generation and + holds a lease on a ready runtime. Replies, events, and frames carry the same + identity; mismatched or stale identities fail without fallback. +- When a runtime is replaced, old leases are retired first. Effects already + sent may finish, but their results are discarded. +- Authentication, health, updates, and profile management are bridge-global and + work regardless of runtime state. One failed runtime never blocks others. +- Browser caches, storage, and pending actions are scoped per connection. ## Instance naming -The authenticated, bridge-global `/api/instance-settings` endpoint reads and -updates `title_suffix` through the GUI settings store's atomic mutation queue. -Writes require JSON and the `x-roamgate-settings` header, reject cross-site -requests, and do not require a live Herdr connection. Existing settings without -this field retain the default `Roamgate` name. - -The server derives HTML title/application metadata and both manifest `name` and -`short_name` from the same normalized suffix. HTML values are escaped with -`HTMLRewriter`; manifest values are JSON-encoded. The credentialed manifest stays -at `/manifest.json`, with unchanged `id`, `start_url`, `scope`, and icons. Entry -HTML and manifest responses are private and require revalidation; the push -service worker does not cache them. Frontend startup/focus refreshes read the -same setting, and confirmed saves update document metadata without changing -connection selection or install identity. +`/api/instance-settings` reads and writes `title_suffix` in the GUI settings +file. Writes need JSON plus the `x-roamgate-settings` header and reject +cross-site requests. The server derives the HTML title and the manifest `name` +and `short_name` from it; the manifest `id`, `start_url`, and `scope` never +change. HTML and manifest responses are not cached. ## Terminal endpoints ### Negotiation and transport -Backend selection uses a verified protocol allowlist, not browser inference. -[Compatibility](./DEPLOYMENT.md#herdr-compatibility) defines supported versions, -legacy fallback, and clipboard limitations. - -Herdr 0.9.0 and 0.9.4 endpoints require generation 1 and exact codecs -`shell.snapshot.v1`, `shell.surface.v1`, `shell.input.semantic.v1`, and -`shell.blob.v1`. Unknown generations/codecs fail closed. Attachment waits for the -initial snapshot; each viewer crops its pane from the shared tab surface. +The backend is chosen from a verified protocol allowlist (see +[compatibility](./DEPLOYMENT.md#herdr-compatibility)). Herdr 0.9.x endpoints +require generation 1 and the exact codecs `shell.snapshot.v1`, +`shell.surface.v1`, `shell.input.semantic.v1`, and `shell.blob.v1`; anything +unknown fails closed. Each viewer crops its pane from the shared tab surface. -Capabilities belong to **each terminal socket**, are renegotiated on reattach, -and are checked again at dispatch: +Capabilities are negotiated per terminal socket and rechecked at dispatch: | Capability | Contract | | --- | --- | -| `pane.focus` | Required for attachment; absence fails without legacy takeover. | -| `pane.scroll` | Gates explicit history scrolling, not semantic page keys. | -| `tab.create`, `workspace.create` | Gate creation on the existing source endpoint. | -| `health_check` | Enables ping/pong; input and resize are core codec operations. | -| `surface_interest`, `presentation_effects_fence` | Do not authorize surface-setting or fencing controls. | - -Endpoint hellos opt into `surface_delta` and `surface_reuse` by default, unless -disabled; each requires a matching welcome capability. Full surfaces, legacy patches, -Base64-bincode deltas, and JSON reuse controls share a connection-local baseline -before cropping. Legacy patches update named panes and replace the cursor -(including `null`); delta/reuse replaces pane, cursor, hyperlink, and scroll -metadata. Geometry changes require a full surface. - -Decoders bound collections and validate boot/projection/surface revisions, spans, -and hyperlink indices. Invalid updates close the stream and clear its baseline; -viewers reattach for a fresh full frame rather than keep stale output. A -connection-wide endpoint observer follows the focused Space to report popup -identity even without pane viewers; the popup terminal uses direct attach. -Kitty graphics are not presented. - -`settings.terminal_transport.get/update` persists `surface_codecs` per connection -in `settings.json`. Changes close that runtime's endpoint displays and broadcast -`settings.terminal_transport.updated`; viewers reattach with fresh baselines. -Queued attachments reread settings. Configuration closes do not consume takeover -retries; tasks, legacy sessions, and other runtimes are unaffected. This is a -shared preference, not an authorization boundary. - -These codecs reduce **Herdr-to-bridge** traffic; browsers still receive cropped -ANSI repaints. Independently, terminal messages of at least 1 KiB use negotiated -WebSocket compression with per-connection context for WebKit compatibility. -Inbound decompression is shared with client context takeover disabled. Smaller -messages, clipboard payloads, and RPC replies stay uncompressed; backpressure -and generation checks still apply. +| `pane.focus` | Required to attach; no legacy fallback without it. | +| `pane.scroll` | Gates explicit history scrolling. | +| `tab.create`, `workspace.create` | Gate creation from the existing endpoint. | +| `health_check` | Enables ping/pong. | + +`surface_delta` and `surface_reuse` are requested by default and stored per +connection (`settings.terminal_transport`). Changing them makes viewers reattach +with a fresh full frame. Invalid updates close the stream rather than show stale +output. These codecs reduce Herdr-to-bridge traffic; terminal messages of 1 KiB +or more are also WebSocket-compressed to the browser. Kitty graphics are not +shown. ### Geometry, input, and selection -`terminal.attach` supplies pane `cols`/`rows` and optionally tab -`surface_cols`/`surface_rows` (both integers in 1..65535). Tab dimensions include -pane borders, not app chrome; surface feedback corrects stale hints. Legacy -attachments use pane dimensions. Repaints clip wide characters and cursors to -the viewer's viewport; browsers reject oversized frames after shrinking. - -Root CSS zoom scales the UI. Terminals cancel it and scale xterm fonts directly, -keeping cell measurements, IME, selection, and mouse input in viewport CSS pixels. -Popover positioning likewise cancels zoom and reapplies it to content. - -Mobile floating controls clamp their rendered position to the visual viewport -and visible input dock, including resize/scroll and dynamic Composer height. -Keyboard lift is transient; only an explicit drag changes the stored placement, -and saves its delta without the temporary lift. When the normal stack cannot -fit, a scrollable compact row reserves space above a height-bounded input dock. -Its toggle remains focus-neutral, while actual navigation retains keyboard -dismissal. These layout adjustments do not send terminal input or change drafts. - -Input waits for readiness and revalidates attachment/session/runtime leases; -it is never replayed into a replacement terminal. Disconnect rejects pending -requests and invalidates clipboard ownership. - -- Endpoint keyboard input uses bounded basic CSI-u for modified Enter, - Backspace, Escape, and ambiguous printable ASCII combinations. The bridge - reconstructs key identity and modifiers before Herdr encodes for the target - application. Hardware and custom mobile shortcuts preserve Ctrl+/ separately - from Ctrl+_, and Alt+[ / Alt+O cannot consume following text as escape prefixes. - Configured terminal actions take precedence; composition, AltGr, and Apple - Option text remain native. Legacy/shared attachments retain raw-byte encoding - because they bypass the semantic classifier. Browser/OS-reserved shortcuts - are available only if the browser delivers their key events. -- Full PageUp/PageDown sends semantic input for Herdr to route by PTY mode; - explicit half-page history uses `pane.scroll`, even in mouse-aware apps. - Legacy attachments retain PageKey/Wheel routing. -- Mouse cells are zero-based and pane-local. Only an in-pane press owns a drag; - subsequent positions clamp to edges. Reporting changes/closure cancel ownership. -- History scrolling coalesces wheel intent while awaiting RPC and viewport - feedback, without blocking other commands. No-op replies need no repaint. - Completed movement is not rebased by history growth; external viewport changes - become authoritative when no newer intent is queued. Input, missing metrics or - panes, and closure cancel queued movement. -- Browser selection holds the latest repaint until cleared. Session changes - retire pending presentation; replay never sends input. Edge-drag history reads - one overlapping viewport at a time, accepting matching content revisions and - retaining immutable cells for the complete copied range. Release, blur, lost - mouse-up, resize, or reset stops scrolling. Content/geometry changes preserve - captured text but stop further history requests. +`terminal.attach` sends the pane's `cols`/`rows` and optionally the tab's +`surface_cols`/`surface_rows`. Terminals cancel the UI's CSS zoom and scale the +xterm font instead, so mouse and IME coordinates stay correct. + +- Input waits for readiness, revalidates its lease, and is never replayed into a + replacement terminal. +- Endpoint keyboard input uses basic CSI-u for ambiguous modified keys + (modified Enter, Backspace, Escape, Ctrl+/ vs Ctrl+_); Herdr re-encodes for + the application. Legacy attachments send raw bytes. +- PageUp/PageDown are sent as semantic input; half-page history uses + `pane.scroll`. +- Mouse cells are zero-based and pane-local; only a press inside the pane owns + a drag. +- Selection freezes the displayed frame until cleared. Edge-drag reads history + one viewport at a time and stops on release, blur, resize, or content change. ### Links -Local detection scans soft-wrapped text with cell coordinates. File detection -also considers bounded, indented continuations because endpoint cell repaints -lack soft-wrap metadata; inferred paths must resolve within the pane's workspace. -File detection accepts POSIX paths plus Windows drive (`C:\`) and backslash -paths; relative paths must resolve, and absolute paths link with `/` separators. -Links detected from text check only that their row still shows the same text -when clicked, so repaints elsewhere, such as TUI timers, do not disable them. -Blank lines separate contexts. Local URL detection never guesses missing tails. - -Endpoint repaints carry an opaque `link_frame` identity, stable across identical, -cursor-only, and focus-only surfaces. Content, hyperlink, viewport/scroll, input, -or resize changes invalidate it. Identical repaints do not rewrite xterm, keeping -native link IDs and in-progress clicks intact. - -`terminal.link.resolve` verifies attachment ownership and frame identity. OSC 8 -uses the cropped frame's hyperlink table; optional plain-text resolution runs on -that exact socket with its content revision and scroll offset. Coordinates are -zero-based **cropped-pane display cells**, never surface origins or CSS pixels. -Hover probes are bounded and independently timed out; hover never activates. - -Herdr 0.9.1's `pane.link.resolve` returns inclusive visible regions, not URLs. -Roamgate reconstructs complete HTTP(S) targets, including wrapped/wide cells, -but rejects ambiguous viewport-clipped URLs. It never calls `pane.link.activate`, -which can invoke host plugin handlers. OSC 8 retains explicit destinations even -for partial labels. Local `file://` targets require decoded absolute host paths -and empty/`localhost` authorities; network hosts, UNC paths, credentials, -queries/fragments, malformed encoding, and control characters are rejected. -Other URI schemes are not opened. - -Async lookups recheck frame, buffer, geometry, navigation, and connection state, -even while selection freezes presentation. Once clicked, a file menu survives -ordinary output but closes on connection/workspace/navigation changes. Web links -open in the browser; files use the pane's workspace, not later global focus. - -Touch long-press resolves the original touched cell, with at most one upstream -probe, then requires an explicit **Open link** or **File actions** gesture. -Selection edits, cancellation, multitouch, scrolling, frame changes, resize, and -reconnect retire lookups. Unsafe explicit OSC 8 targets and failed endpoint touch -reads never fall back to URL-looking labels. Legacy touch supports plain-text -URLs/paths only, without explicit OSC 8 cell metadata. +The browser detects URLs and file paths in soft-wrapped text, including +Windows paths. Inferred paths must resolve inside the pane's workspace. + +Endpoint repaints carry a `link_frame` identity that changes with content, +scroll, or geometry. `terminal.link.resolve` checks ownership and frame +identity, then uses the frame's OSC 8 table or, on Herdr 0.9.1+, +`pane.link.resolve` to rebuild wrapped URLs. Coordinates are zero-based cropped +pane cells. Ambiguous viewport-clipped URLs are rejected, `pane.link.activate` +is never called, and only HTTP(S) and safe local `file://` targets open. Hover +never activates a link; touch requires an explicit **Open link** or **File +actions**. ## Browser navigation and creation -`browserNavigation.ts` projects endpoint browser-local selections into shared UI -fields. Snapshots supply topology, not subsequent navigation; stale layouts and -results cannot overwrite newer selections. Legacy navigation, topology changes, -terminal dimensions, and native same-tab pane focus remain shared. - -Active selection/clicks send `terminal.focus` through the attached `pane.focus` -endpoint. Focus restores after split attachment, not on routine frames/snapshots. -Requests serialize per browser across endpoint lanes; superseded selections are -discarded and ownership/leases rechecked. Same-tab cursor ownership remains shared. - -Creation uses explicit context and `focus: false`; returned IDs are adopted only -while the initiating selection and lease remain current. The bridge validates -and strips Roamgate-only `browser_source`, then uses the existing endpoint's -serialized focus/scroll lane without another endpoint or focus call. Omitting -synthetic cwd preserves Herdr's `terminal.new_cwd`; explicit cwd wins. Shared -same-tab focus still controls the `follow` source. - -`workspace.path` expands `~`/`~/` on the selected connection's host, validates -directory access, and returns up to 50 directory suggestions for a directory -or partial basename. Explicit workspace creation paths must be absolute or -home-relative; the bridge resolves and checks them again before dispatch, so -invalid paths fail instead of silently falling back to Herdr's home directory. -Empty cwd still preserves Herdr's configured policy. These checks use native -filesystem access locally and quoted Bash/PowerShell commands over SSH. - -Missing source attachments fail. Only first-workspace bootstrap uses control -creation, serializing an empty-topology check per runtime. Competing requests -retry when topology becomes nonempty. A 20-second admission deadline covers -readiness, validation, and queuing: expired undispatched mutations never execute; -dispatched timeouts report uncertain completion, requiring inspection before retry. +`browserNavigation.ts` keeps endpoint workspace/tab selection local to each +browser; snapshots provide topology but never override newer selections. Pane +focus within a tab stays shared and is sent through `pane.focus`. + +Creation passes `focus: false` and adopts the returned IDs only if the +initiating selection is still current. Omitting cwd preserves Herdr's +`terminal.new_cwd`. `workspace.path` expands `~` on the connection's host, +validates the directory, and returns up to 50 suggestions; creation rechecks the +path before dispatch. + +Only the first workspace in an empty session uses control-socket creation. A +20-second admission deadline covers readiness and validation: expired requests +never run, and dispatched timeouts are reported as uncertain. ## Workspace resource ownership -A **checkout** owns Files/Changes/Commits; a workspace supplies routing, a tab a return -location, and a pane optional path/session context. Repository groups are not -merged working trees. Changes and Last step snapshots do not prove agent ownership. - -Git resource keys pair endpoint-qualified `worktree.gui_settings_key` with the -normalized checkout path, scoped by connection. Blank keys fall back to the -trimmed repository key, which cannot distinguish endpoints; enriched identities -do not inherit fallback state. Persistent keys exclude runtime generations. -Non-Git resources use workspace identity. - -Shared-checkout workspaces may share caches, but requests retain workspace/runtime -leases and resource revisions. Refresh/removal retires old prefetches. Tab/pane -IDs never own caches. Legacy repository-wide Inspector state is retained but not -automatically migrated because it lacks checkout identity. - -Actions capture the originating workspace. A vanished workspace can rebind only -to the same checkout; missing paths never fall back to siblings. Agent cwd is -used only inside the checkout. Removal clears only that checkout's state; closing -one workspace retains resources another workspace still uses. Terminals stay -mounted across Inspector changes; layout preferences and content caches are separate. - -Browser UI preferences share namespaced local storage across tabs at the same -origin. Subscribers normalize current keys, legacy keys and deletion markers, -then reread effective values so cleared preferences cannot revive legacy values. -Appearance, layout, shortcuts, pins and display options update without reloading. -Inspector geometry is shared; active views, files, workspace/tab navigation, -drafts and focus stay local to each tab. A saved input-mode choice is read on -explicit dock opening and never remotely enables Direct input in a live session. -Session-storage reload guards and live connection controls remain tab-local. +A **checkout** owns Files, Changes, and Commits. A workspace supplies routing, a +tab a return location, and a pane optional context. Changes and Last step +snapshots do not prove which agent made a change. + +Git resource keys combine `worktree.gui_settings_key` with the normalized +checkout path, per connection. Workspaces on the same checkout may share caches. +Actions remember their originating checkout and never fall back to a sibling. +Terminals stay mounted while the Inspector changes. + +Browser preferences live in namespaced `localStorage` and sync across tabs on the +same origin. Navigation, open files, drafts, and focus stay per tab. ### Last step capture limits and ownership -Last step captures checkout contents at workspace active/idle boundaries, not -when Changes is viewed. Captures include small untracked files and changes -committed during the turn; they never stage files in the user's index. A private -copy of the current index preserves newly staged gitlinks and sparse-checkout -metadata. Absent skip-worktree entries retain their indexed Git objects rather -than appearing deleted. Materialized files use raw on-disk bytes, without Git -clean filters, CRLF conversion, or `working-tree-encoding` conversion, and override -their indexed contents. Initialized nested repositories retain their current -commit even when untracked; uninitialized submodules retain their indexed commit. -Regular files retain their executable/non-executable distinction. Resolved Git -metadata directories inside the checkout are excluded from capture, including -nonstandard separate Git directories and linked-worktree shared metadata. - -Automatic capture refuses the whole snapshot when a file exceeds 8 MiB, regular -file content exceeds 32 MiB in total, or there are more than 10,000 regular files. -The byte limits apply to the bytes actually copied, including tracked changes, -not just preflight sizes. Applicable `filter` attributes (other than unset or -unspecified), symlinks (including symlink ancestors), special/unreadable files, -and newline/tab filenames are unsupported and cause refusal. Unused filter -configuration does not prevent capture. Shared repositories are unsupported: -capture refuses enabled or unrecognized effective `core.sharedRepository` values -(including inherited configuration) before creating quarantine storage or -publishing objects. Only an unset value, `false`, `0`, or `umask` is supported; -manual object publication does not implement Git's shared permission handling. -A capture also fails if required shell file-size limits, hard links, or the GNU/BSD `dd` byte-count report are unavailable; -files on another filesystem from the Git directory cannot be pinned by hard link. -The command has a ten-second caller timeout and refuses to begin publication -once nine seconds have elapsed on the executing host. A refusal is logged and -never publishes a partial range; the previous successfully completed Last step -remains available. Snapshots are not atomic against concurrent file edits. - -Capture uses an owner-only, exclusively created -`roamgate-last-step-capture` directory inside the checkout's Git directory (the -worktree-specific Git directory for linked worktrees). Bounded copies, temporary -objects, and the scratch index stay there until Git has finished successfully. -Only complete loose objects are published, by non-overwriting hard links into -Git's ordinary object database. No automatic `git add` or streaming pack write -runs against the user's objects. Successful objects deduplicate normally and -remain subject to Git's existing unreachable-object garbage collection; Roamgate -does not run GC or remove user object files. Metadata and temporary object copies -require space in addition to the 32 MiB content budget. - -Normal completion and confirmed command failures remove the owned directory. -Signals or crashes retain it: later captures refuse the existing directory rather -than deleting a possibly live lock or allocating replacement scratch storage. -Disposal does not delete uncertain storage. The `owner-pid` file is diagnostic, -not proof that descendants have exited. For recovery, stop captures for that -checkout and confirm that its capture shell and all children have exited (reboot -the executing host if uncertain). Only then remove the exact -`$(git rev-parse --absolute-git-dir)/roamgate-last-step-capture` directory. Do not -remove anything from `objects`, `objects/pack`, or the user's index. - -SSH uses the same script on the selected host. Killing or losing the local SSH -transport does not guarantee a remote signal: an unsignaled remote capture can -finish, publish bounded complete objects, and clean its directory after the -caller has failed. Those objects may be unreachable; no successful snapshot token -is returned to the failed caller. Concurrent captures still contend on the same -fixed remote ownership directory. No remote process-tree termination is assumed. +Last step snapshots the checkout when the workspace goes from active to idle. +Captures include small untracked files and commits made during the turn, use raw +on-disk bytes (no Git filters), and never touch the user's index. + +A capture is refused entirely when a file exceeds 8 MiB, content exceeds 32 MiB +in total, or there are more than 10,000 files. It is also refused for symlinks, +special or unreadable files, newline/tab filenames, applicable `filter` +attributes, shared repositories (`core.sharedRepository` other than unset, +`false`, `0`, or `umask`), files on a different filesystem from the Git +directory, or after a 10-second timeout. A refusal is logged and the previous +Last step stays available. Snapshots are not atomic against concurrent edits. + +Work happens in an owner-only `roamgate-last-step-capture` directory inside the +checkout's Git directory. Only complete loose objects are hard-linked into Git's +object database; Roamgate never runs `git add`, GC, or deletes user objects. + +**Recovery:** a crash can leave that directory behind, and later captures then +refuse to run. Stop captures for the checkout and make sure the capture shell +and its children have exited (reboot if unsure). Then remove only +`$(git rev-parse --absolute-git-dir)/roamgate-last-step-capture`. Never remove +anything from `objects`, `objects/pack`, or the index. Over SSH, a remote capture +may still finish after the local caller fails. ## Filesystem browsing -`file.list` is checkout-relative with realpath/symlink escape checks. Each -filesystem listing explicitly sends `scope: "filesystem"` and an absolute host -directory; replies confirm scope and return absolute entries. The mode is -view-local, not a persisted permission; retired responses cannot replace a new -view. Search filters loaded entries only. - -Absolute previews use `scope=filesystem` download URLs; relative Markdown links -and images resolve beside their source. Upload/delete remain checkout-scoped. -`file.mkdir` takes `workspace_id`, a relative `directory` (empty for the root), -and a single-component `name`. `file.move` takes `workspace_id`, a relative -source `path`, and a relative destination `directory`. Both reject filesystem -scope and absolute paths, and confine resolved parent directories to the -checkout. Moves retain the source filename, accept files and symlinks, and -never overwrite existing entries. Moves use the host's atomic exclusive rename -primitive (`renamex_np`, `renameat2`, or `MoveFileExW` without replacement or -copy flags), so concurrent saves are never removed by a later source deletion. -Cross-filesystem moves and unsupported primitives fail without a copying -fallback. SSH moves require Python 3 to call the primitive through its standard -library. The client submits one move request at a time and reports partial -failures, then refreshes source and destination listings sequentially and -retires moved source previews. - -`file.reveal` opens the host file manager with a fixed per-platform argv -(`explorer.exe`, `open`, `xdg-open`), never a shell. It is disabled unless the -host opts in with `ROAMGATE_ALLOW_FILE_REVEAL=1`, and still refuses SSH profiles -and non-loopback peers. The hello `file_reveal` capability reports host opt-in -and peer eligibility; the UI also checks the selected profile. Loopback peers -can be forwarded/tunneled remote browsers: neither TCP loopback nor the browser -URL proves same-machine access. See [deployment](DEPLOYMENT.md#host-file-reveal). - -Reveal validates scope explicitly: workspace paths must be relative (including -on Windows), with realpath/symlink confinement; only `scope: "filesystem"` -permits absolute paths outside the checkout. Changes sends `source: "changes"` -with a Git-root-relative path. The server resolves the workspace's Git root and -uses actual existence, not Git status codes, falling back to the nearest existing -ancestor inside that root. Ordinary explorer requests still fail on missing -paths. Download behavior is unchanged. Windows foreground focus through `user32` -(`bun:ffi`) is best effort, not guaranteed; the host needs a usable desktop. -Explorer caches are separate from lazy UI code. Mermaid previews share a lazy -renderer, strip wrappers/metadata only for detection, and retain original source. -Images are inert elements; SVG is never inserted into the app DOM, and direct -SVG responses carry a sandbox CSP blocking scripts and external resources. - -HTML previews use a separate empty-sandbox iframe and an enforcing response CSP, -including on direct navigation. The server embeds workspace CSS, images and fonts -as data URLs; Bun's CSS bundler resolves imports and asset URLs. Relative paths -resolve beside their document or stylesheet; root-relative paths resolve within -its workspace. Realpath checks reject resources outside that workspace. HTTPS -stylesheets and their CSS imports are loaded directly by the browser, not fetched -by the bridge; protocol-relative stylesheet URLs use HTTPS. Stylesheet links use -`no-referrer`, and document-supplied referrer metadata is removed. CDN requests -expose the viewer's IP address and are not covered by workspace read limits. -Scripts, forms, embedded documents, external images/fonts and HTTP stylesheets -are blocked. Missing or unsupported workspace resources are omitted. Ordinary -downloads retain original bytes. - -HTML and each workspace stylesheet are limited to 512 KiB, each workspace -image/font to 5 MiB, and a preview to 64 resource paths and 10 MiB of resource -bytes. Resource-bearing style processing is limited to 256 blocks/attributes, -with a 20 MiB embedding/output budget. Local and SSH readers check size and read -at most the limit plus one byte to detect growth; oversized previews return -HTTP 413. HTML requests have a 120-second HTTP idle timeout for preparation over -SSH. The source view and downloads remain available. -These limits do not bound decoded image memory or browser rendering cost. User -links and deliberate copy/drag into other controls remain untrusted user actions. +`file.list` is checkout-relative with realpath/symlink escape checks. +`scope: "filesystem"` with an absolute path enables read-only host browsing; it +is a view mode, not a stored permission. Upload, delete, `file.mkdir`, and +`file.move` are checkout-only and reject absolute paths. Moves use the host's +atomic no-replace rename (`renamex_np`, `renameat2`, or `MoveFileExW`), never +copy, and need Python 3 over SSH. + +`file.reveal` runs a fixed per-platform command (`explorer.exe`, `open`, +`xdg-open`), never a shell. It needs `ROAMGATE_ALLOW_FILE_REVEAL=1`, a local +profile, and a loopback peer; loopback does not prove the browser is on the same +machine. See [host file reveal](DEPLOYMENT.md#host-file-reveal). + +Previews are inert. SVG is never inserted into the app DOM and carries a +sandbox CSP. HTML previews render in an empty-sandbox iframe with an enforcing +CSP: workspace CSS, images, and fonts are inlined as data URLs; HTTPS +stylesheets load directly from the browser (exposing the viewer's IP); scripts, +forms, frames, and external images/fonts are blocked. Limits: 512 KiB per HTML +or stylesheet, 5 MiB per image/font, 64 resources and 10 MiB per preview +(HTTP 413 beyond that). ## Agent activity -`agent.list` adds optional `last_activity_at` from session-file mtime without -parsing transcripts. Checks have bounded concurrency and a 1.5-second budget; -missing metadata never removes agents. Remote IDs needing local directory search -stay unresolved. Idle ordering uses mtime, then Herdr state-change sequence, -without browser activity history. [History synchronization](./HISTORY.md) owns -projection, caching, and incremental transcript contracts. +`agent.list` adds `last_activity_at` from the session file's mtime, with a +1.5-second budget; missing data never hides an agent. +[History synchronization](./HISTORY.md) covers transcript projection and caching. ## Agent integrations -`integration.list` keeps Herdr's targets and installation states authoritative. -Missing versions are supplemented by read-only `herdr integration status` on the -selected host: locally for local connections, or through that connection's SSH -account. The CLI binary version must match the running server's `ping` version; -per-agent state and any existing version fields must agree before merging. -The bridge/SSH account must use the same agent configuration environment as Herdr. -RPC metadata is never overwritten, and a `current` state alone does not imply an -available version. CLI commands have five-second timeouts; missing binaries, -failed SSH commands, malformed output, and version mismatches leave missing -metadata unknown without failing the list. There is no remote-to-local fallback. +`integration.list` uses Herdr's RPC as the source of truth. Missing versions are +filled from read-only `herdr integration status` on the same host (over SSH for +remote connections), only when the CLI version matches the running server. CLI +failures leave versions unknown without failing the list. ## Ranger -The bridge owns one Pi Durable assistant service, independent of the selected Herdr -connection. Authenticated browsers share its configuration, saved conversations, -active conversation, and single active turn. `bridge.assistant.*` RPC and dedicated -assistant snapshot pushes are -bridge-global; snapshots carry a service instance ID and monotonically increasing -revision. The browser reconciles pushes and replies, retires old instances, and -fetches state after reconnect without replaying sends. Request IDs deduplicate -explicit retries. Hiding the window only changes presentation; stopping work is -a separate RPC. - -`bridge.assistant.mentions` lists reference candidates within an authorized, -captured workspace scope. Workspace references include connection and workspace -IDs plus the runtime generation; Agent references additionally pin the pane, -terminal, and an opaque hash of the detected session identity. Raw session paths -and process metadata stay private. Candidate discovery scans at most 64 -workspaces and 200 Agents, reports partial results explicitly, and can be scoped -to a particular workspace. Only Agents with verifiable sessions are offered. - -Messages may include up to 32 structured references with UTF-16 offsets matching -their exact visible `@label` text. The server validates and canonicalizes their -targets before admission, while preserving submitted labels in the transcript. -References are metadata, not permission grants or prompt-delivery operations; -the model reads bounded evidence through the existing tools. Agent identity is -rechecked before and after referenced history/terminal reads, during proposal -preparation, and before execution. Follow-up reads retain the last explicit -session binding for a pane. Replaced sessions fail without selecting another -Agent by name. Durable recovery restores references only after verifying the -original workspace identities. Scheduled proposals inherit referenced targets -within their fixed scope, and task plans/runs preserve those targets. Notification -deduplication also includes the referenced session identity. - -Unsent text and references remain browser-tab-local and separate per conversation. -The native composer keeps bindings consistent with text edits and stores them in -its local undo history. Old messages without references remain valid. Reference -navigation refreshes the candidate and checks its identity before opening it. - -Model credentials default to the separate Roamgate Ranger store. Reusing the -bridge OS account's Pi credentials requires an explicit source choice. Login -captures that choice and updates only the selected store; configuration changes -are blocked until login completes or is cancelled. Provider prompts and authorization URLs stay out -of conversation history and persisted snapshots; provider credentials remain on -the bridge host. [Deployment](DEPLOYMENT.md#ranger-model-connection) owns paths -and setup instructions. - -The protocol-independent tool catalog in `server/src/assistant/tools.ts` owns -tool names, descriptions, and JSON schemas. `callWorkspaceTool` validates the -named tool and arguments before invoking a reader bound to the service's -approved turn scope. Its result keeps text and sources separate; the Pi adapter -formats that result for the model. Adapters share this execution entry point and -the existing context reads, including cancellation and safe error handling. -The catalog does not grant workspace access: adapters must authorize and capture -the scope on the server before binding a reader. Runtime leases remain private -to the context layer. - -The model receives `workspace_status`, `workspace_history`, `workspace_diff`, -and `workspace_terminal`, plus proposal tools for workspace creation, -worktree creation, tab creation, pane splitting, agent startup, agent prompts, -and closing agents, panes, and workspaces. -On Windows-host profiles, `workspace_status` reports `remote_platform: "windows"`. -Status and terminal reads and the other proposals use only native Herdr RPCs -there. History reads, diff reads, and worktree creation fail with an error that -names the operation and states that it requires a Unix SSH host; tool wrappers -return it to the model verbatim instead of the generic failure. The connection -runtime also makes its Unix-only file, Git, agent-history, and worktree-hook -helpers throw that error on Windows hosts, so a caller that skips its own check -cannot run Unix shell commands there. -By default, proposal tools prepare previews for user confirmation. In explicitly -enabled high-permission mode, the service executes the same validated operations -and returns their actual receipts to the model. Built-in tools and discovered extensions, skills, -prompts, and project instruction files are disabled. Evidence is bounded and -treated as untrusted content. The service validates the selected turn scope -against explicitly allowed connection/workspace pairs; none are allowed by -default. Explicitly confirmed high-permission mode also authorizes current and -newly discovered workspaces without changing the saved normal-mode selection. -The `workspace_scope: "all"` consent marker is required alongside automatic -approval; legacy automatic approval alone retains its selected scope. Only the -dedicated permission confirmation can grant the broader scope. Allowed and -per-turn scopes each contain at most 512 workspace pairs, sharing the inventory -bound. A truncated inventory cannot be used as a complete high-permission turn. -Inventory reconciliation prunes saved selections only for removed configured -connections or complete successful current workspace listings. Incomplete, -failed, and disconnected listings do not prove deletion. Running turns and tasks -retain concrete admitted identities; live permission checks still apply after -the global mode changes. -Each turn captures runtime identity/generation and rechecks its leases -before and after reads. A replaced runtime or vanished workspace fails without -falling back to another host or workspace. - -`workspace_diff` lists working-tree changes when `path` is omitted. To read a -listed file, pass its `path` and `kind`: `staged`, `unstaged`, `untracked`, or -`conflicted`. Omitting `kind` reads unstaged changes. - -`workspace_terminal` reads the selected pane's recent output, including its active -agent output, through Herdr `pane.read`. The model can choose `lines` from 1 to -1,000; omitting it reads 120 lines. The response identifies its requested line -window and retains the newest 32,000 characters when the text budget is exceeded, -with an explicit truncation warning. Reads allow 25 seconds for Herdr to collect -supported idle agents' application history and restore their viewport. Available -history depends on Herdr support and agent state; increasing the requested line -window does not guarantee recovery of every application-owned response. - -Each proposal captures its original runtime lease and exact target. Agent -operations also recheck the pane occupant before execution. Tab creation uses -Herdr's default terminal tab in the workspace's verified directory. Pane splitting -pins the source tab, pane, terminal, working directory, and right/down direction; -confirmation rechecks these identities before changing the layout. Result reads -verify the created tab or pane and include its identifiers in the receipt. -Close previews pin the affected panes and their occupants. Closing an Agent -uses the same pane-closing semantics as the UI; it does not delete Agent history. -Workspace closure does not remove checkout files or Git worktrees. Last-pane -closure uses `workspace.close` without group consent. Individual pane/Agent -closes are rejected for multi-pane worktree-root workspaces: the native -`pane.close` protocol cannot atomically prevent an implicit group close when -another pane disappears. Other close operations revalidate layout and occupant -identities before dispatch; the native API does not provide compare-and-close -identity preconditions. -Models can read `workspace_status` after confirmation before proposing an agent -start in a newly created pane. Authenticated bridge -browsers can confirm or cancel the shared preview through `action.confirm` and -`action.cancel`. Confirmation accepts only the proposal ID, never new parameters. -The service saves an executing receipt before starting an operation and admits -only one operation at a time. Duplicate confirmations return the same receipt. -Result reads verify workspace creation and agent startup; prompt delivery is -reported separately from whether the agent completed the requested task. Failed -or uncertain operations are not automatically retried. - -`configure_approval` persists the global `approval_mode` independently of model -and workspace configuration; missing values mean `manual`. Ordinary `configure` -preserves the live mode, so stale browser settings cannot re-enable it. Automatic -execution requires both the admitted turn's mode and the current global mode to -be `auto`, checked after preparation and immediately before execution. Switching -back to `manual` is allowed during a turn and prevents subsequent automatic -operations. Already dispatched effects retain their ordinary receipt semantics. -Automatic action and task-creation tools execute sequentially and remain unsafe -to replay. Revocation before action reservation leaves a pending preview; a -reserved action rejected before dispatch returns a failed receipt stating that -nothing was sent. Automatically -created schedules return confirmed task receipts through the same tool path. - -Worktree creation shares the ordinary UI's base synchronization, parent tracking, -and setup-hook execution. A changed setup command requires fresh confirmation; -an already-created worktree can therefore have a partial setup result. New -questions or configuration changes retire pending previews. On bridge restart, -pending previews become cancelled and executing receipts become uncertain; -captured execution closures are never restored. Manually confirmed results clear -retained model context and are supplied as receipts in subsequent questions; -automatic results are returned within the active durable turn. Creating a -workspace does not add it to the allowed scope. - -Sources carry their workspace, runtime generation, kind, and read time. They -describe evidence at that time rather than live workspace state; browser source -navigation rechecks runtime generation. Configuration, transcript, deduplication -IDs, and model context are persisted in private bridge data. Changing allowed -workspaces, turn scope, or captured runtime generation clears retained model -context before the next turn. **New chat** archives the current nonempty -conversation and starts a new transcript and model context. `select_session` -restores a saved conversation's transcript, model context, and deduplication IDs -while keeping the current global configuration. New chats and switches require an -idle service and retire pending action previews; archived actions never replay. -Snapshots include the active session ID and summaries for the history list. A -send may include that session ID to reject a stale browser's submission after -another device changes the active conversation. The service records an admitted -run before submitting it to a persistent Pi Durable conversation. Reopening the -bridge resumes the same request without a browser resending it. Graceful shutdown -pauses work; **Stop** explicitly aborts it. Recovery waits for the original -connections, verifies their configured endpoint fingerprints and Herdr boot IDs, -and checks workspace directory/worktree identities before acquiring fresh leases. -Changed or unsupported identities, removed permissions, and unavailable original -models prevent automatic recovery. Unsent drafts stay in browser-tab memory, -keyed by conversation. - -Completed tool results are retained. Interrupted reads may replay after scope -validation; proposal tools are not replay-safe. Confirmed operations retain their -existing receipt and uncertainty rules and are never automatically repeated. -Interrupted model streams are regenerated; aborted partial answers are excluded -from the completed answer displayed in the chat. - -Each retained transcript is limited to the newest 80 messages and 1 MB of serialized -UTF-8 data. Answers are capped at 32,000 characters, 64 tool activities, and 64 -sources each. Durable conversations compact model context while retaining the original -entries on disk. The active state -and history summaries live in `state.json`; inactive conversations are stored in -private `sessions/.json` files. Existing single-conversation state is -migrated without clearing its messages. Private -`durable//execution.sqlite` databases use Pi Durable's SQLite backend to -hold execution checkpoints, transcripts, and tool results; `state.json` retains -their active context reference and pending run. SQLite uses WAL mode and -`synchronous = FULL` with a process ownership lock. Missing or invalid execution -stores cannot create replacement contexts. -Tool activity cards expose bounded argument JSON and text results, including -safe tool failure messages. Historical details are projected from existing -execution logs through a read-only SQLite connection without resuming a run or -rewriting saved conversations. Missing or ambiguous records leave details -unavailable. Images and internal execution metadata are not included. -There is no automatic deletion or aggregate disk quota -for saved conversations or durable stores, including contexts retired by scope -changes. One bridge process must own an assistant data directory. - -Scheduled tasks are independent of chat sessions. A confirmed form or chat -proposal persists the exact prompt, scope, model configuration, original -endpoint/Herdr/workspace identities, and next deadline. Every admission is saved -before its isolated Ranger service starts. `tasks.sqlite` stores tasks, runs, -proposals and deduplication records in separate queryable tables. Each state -change commits in one transaction before being published; a failed save rolls -back in-memory changes and pauses scheduling. Schema creation is transactional, -and invalid databases disable scheduling instead of clearing saved tasks. -Each run uses the ordinary send, -durable resume, and Stop paths with its own transcript and execution directory; -credential lookup remains in the main assistant directory. Runs verify the -original identities and current global workspace permissions before reading or -proposing operations. Automatic management execution additionally requires the -task's saved `approval_mode` and the live global mode to both be `auto`. Existing -manual tasks are not escalated when the global mode changes; editing a task -through the Tasks form captures the currently selected mode for future runs. -Chat task-edit tools preserve the saved model, scope, and Agent references; -they edit title, prompt, schedule, and notification mode. A manually confirmed -edit downgrades future runs to manual approval. An automatically executed edit -retains the existing task mode and does not elevate a previously manual task. -Task management proposals use the same manual/high-permission policy as creation -and reject stale task revisions before applying a change. Update and resume -revalidate original workspace/Agent identities and the saved model. Local-only -pause, cancel, and delete still recheck the captured and current workspace -grants, but do not require a retired model or monitored Agent to remain live. -Management previews expire after ten minutes or bridge restart. Deletion -requires prior cancellation and removes the saved task and its private run -history. Disabling the global mode also revokes automatic execution in existing -task children. - -One scheduled model run is admitted at a time; interactive chat has a separate -service. Interrupted runs waiting for their original connections return to the -queue and release the model slot. Their original start time and checkpoint -directory are retained so the next admission resumes the same run, including -while its schedule is paused. A run waiting for operation confirmation retains -its service and blocks new runs of the same task, while other tasks can proceed. -Restart cancels those -previews under the ordinary receipt rules. A task has at most one outstanding -run; overdue occurrences are combined rather than replayed in a backlog. -Interval schedules preserve their cadence, daily schedules use explicit IANA -timezones (skip gaps, first overlap occurrence), and one-time schedules admit -their scheduled occurrence only once. Manual runs do not resume a paused task -or change its schedule. Pause leaves current work running; Stop ends only the -current run; Cancel stops it and disables future admissions. Pause also cancels -a non-manual occurrence still in asynchronous admission or child preparation -before model dispatch. Editing preserves paused status and any queued/admitted -run's original instruction snapshot; later occurrences use the edited template. -Cancelled tasks can be deleted. Up to 50 tasks and the latest 20 runs per task are -retained. List snapshots contain task and run summaries; full run transcripts are -fetched separately through `bridge.assistant.task.get`. - -After a task state change commits, transitions to `succeeded`, `failed`, or -`waiting` emit a dedicated global `assistant_notification` push and Web Push -with fixed text and the task/run UUIDs. Success uses the `completed` notification -preference; failures and confirmation requests use `blocked`. Repeated states, -initial history, Stop, and Cancel do not emit alerts. Delivery is best effort; -notification failures do not change task results, and alerts are not replayed -after downtime. Click targets open the specific Ranger run independently of the -selected connection. Notification text includes the task title and fixed status -guidance; prompts and provider errors stay in the run transcript. - -Confirmed scheduled runs also expose `send_user_notification`, with a bounded -title, body, `completed`/`attention` kind and stable event key. Interactive chats -propose a monitoring schedule; they do not expose the sending tool. The service -binds delivery to its own current task/run UUIDs and revalidates the original -workspace identities, current permissions and cancellation before accepting a -notice. Models cannot choose recipients, URLs or another task's target. -Notification kind describes the observed Agent outcome or need for attention, -independently of whether the Ranger check itself succeeds. - -Task `notification_mode` defaults to `status`. In `agent` mode, routine -successful checks are silent. A custom notice also replaces the fixed success -alert for that run; fixed failures and confirmation requests remain available. -Each task retains its latest 100 custom notification receipts. The original -target identity hash scopes event-key deduplication and the history supplied to -subsequent runs, so editing a task's scope cannot expose earlier workspace -content to its new model scope. A run accepts at most one custom notice. -Receipts commit before dispatch; repeated keys within the retained history do -not dispatch again. Pi marks the tool sequential and unsafe to replay, so an -interrupted delivery can be lost but is not automatically repeated. Tool results -report acceptance with best-effort delivery, never confirmation that a device -displayed the notice. Notification receipts remain after their run transcripts -are pruned and are removed with the task. +The bridge runs one Pi Durable assistant service, independent of the selected +connection and shared by all authenticated browsers through bridge-global +`bridge.assistant.*` RPC and revisioned snapshot pushes. Reconnecting fetches +state; sends are never replayed. + +- **Tools** are defined in `server/src/assistant/tools.ts` and run through + `callWorkspaceTool` against the turn's approved scope. Pi's built-in tools, + extensions, skills, and project instructions are disabled, and workspace + evidence is treated as untrusted. +- **Scope:** no workspaces are allowed by default. Each turn captures concrete + connection/workspace pairs (at most 512) and rechecks runtime leases around + every read. Only the dedicated confirmation can enable high-permission mode + (`approval_mode: "auto"` with `workspace_scope: "all"`). +- **Actions:** a proposal pins its runtime lease and target, and + `action.confirm` accepts only the proposal ID. A receipt is saved before + dispatch, one action runs at a time, and failed or uncertain actions are never + retried. Pane/agent closes are refused in multi-pane worktree-root workspaces + because Herdr cannot prevent an implicit group close. +- **Persistence:** conversations and execution checkpoints live in the assistant + directory; one bridge process must own it. After a restart, a turn resumes + only after its endpoints, Herdr boot IDs, and workspaces are re-verified. +- **Tasks** live in `tasks.sqlite`; each state change commits before it is + published. One scheduled run executes at a time, overdue occurrences are + combined, and runs re-verify identities and permissions. Run results emit an + `assistant_notification` push; custom notices are deduplicated by event key. ## Task notifications With the default `herdr` source, each runtime keeps one passive endpoint shell -(`surface_active: false`) on the render socket, gated on endpoint generation 1. -Herdr never promotes it to foreground or tab geometry controller, but delivers -`SemanticNotification` (frozen tag 14) to it after applying Herdr's own policy. -Only this shell decodes tag 14; per-pane terminal shells ignore it, so open views -do not multiply alerts. Finished/needs-attention map to completed/blocked; -pane-less custom alerts use their sound (`request` = blocked). The runtime relays -each one to Web Push and to browsers as `roamgate.task_notification`; the bridge -hello advertises `herdr_task_notifications` so pages disable their own status -tracker. Herdr 0.9.4's Antigravity/Grok foreground-idle policy stays authoritative; -Roamgate does not synthesize extra alerts for their background tasks. The shell -reconnects with capped backoff. Servers without endpoint support (older Herdr, -or endpoints disabled) fall back to the status tracker -below, relayed the same way; a temporarily disconnected endpoint server does not. - -The `status` source tracks agent transitions through per-pane subscriptions and -periodic reconciliation, even without browsers. Initial state is silent; -snapshots cannot overwrite newer events. Transitions emit once; disposal stops -observation and queued sends recheck the owning lease. - -Web Push persists private VAPID keys and device subscriptions. Authenticated -same-origin HTTP manages enrollment/revocation; encrypted sends use a provider -allowlist, ten-second deadline, four concurrent requests, and a bounded -256-delivery memory queue. Failures/overflow are logged, not durably replayed. -The Service Worker checks connection generation when routing clicks; enrolled -pages suppress duplicate local notifications. See [delivery limits](./DEPLOYMENT.md#web-push-notifications). +(`surface_active: false`) that receives Herdr's `SemanticNotification` (tag 14) +after Herdr applies its own policy. Finished maps to completed and +needs-attention to blocked. The bridge relays these to Web Push and browsers +(`roamgate.task_notification`) and advertises `herdr_task_notifications` so pages +skip their own tracking. Servers without endpoint support fall back to the +`status` tracker, which watches agent transitions (initial state is silent). + +Web Push stores private VAPID keys and subscriptions, sends to an allowlist of +push providers with a 10-second deadline, 4 concurrent requests, and a +256-message memory queue. Failures are logged, not replayed. See +[delivery limits](./DEPLOYMENT.md#web-push-notifications). ## SSH transport -Unix SSH runtimes supervise one OpenSSH process forwarding both sockets into a private -temporary directory. Readiness requires control `ping` and render handshake. -Transient failures retry six times with cancellable backoff capped at 30 seconds, -reset after 30 seconds stable-ready. Authentication, host-key, and permanent -protocol failures do not retry. Post-ready exit retires the generation first. -CLI SSH uses the same probes without persistence or automatic retry. - -Windows-host profiles (`remote_platform: "windows"`) instead expose two local -Unix-socket listeners in the same private runtime directory. Each accepted -connection owns an OpenSSH stdio process running Herdr's `remote-api-bridge` -or `remote-client-bridge`. Windows PowerShell launches the native executable with -inherited handles; its text pipeline must not process binary terminal frames. -`remote_herdr_path` optionally selects a concrete executable and `remote_session` -selects the named session (default `default`). A capability and server-status -probe rejects missing/old binaries and stopped sessions before readiness. -The control ping and render handshake remain the readiness contract. Closing a -local connection terminates its SSH process; runtime disposal closes all -listeners/connections and confirms child exit before removing owned paths. -Unexpected bridge failures use the existing connection retry and generation -isolation rules. Profile edits and Ranger recovery fingerprints include the -Windows executable and session. No transport installs or updates remote software. - -Only OpenSSH aliases or `user@host` are accepted, after `--` with fixed options. -Host-key checks remain enabled; service authentication is noninteractive. OpenSSH -configuration owns credentials/options. Stderr is bounded/sanitized. Cleanup -removes owned paths only after confirmed child exit; otherwise it preserves them -and reports failure. Host operations share this boundary; see [connection setup](./DEPLOYMENT.md#multiple-and-remote-herdr-connections). +Unix SSH runtimes run one OpenSSH process forwarding both sockets into a private +temporary directory. Readiness needs a control `ping` and render handshake. +Transient failures retry six times with backoff up to 30 seconds; +authentication, host-key, and protocol failures do not. + +Windows-host profiles instead listen on two local Unix sockets; each connection +spawns an OpenSSH stdio process running Herdr's `remote-api-bridge` or +`remote-client-bridge` through PowerShell. A capability probe rejects missing or +old binaries and stopped sessions. + +Destinations must be an OpenSSH alias or `user@host`, passed after `--`. +Host-key checks stay on and authentication is non-interactive. Owned paths are +removed only after the child process exits. Roamgate never installs remote +software. See [connection setup](./DEPLOYMENT.md#multiple-and-remote-herdr-connections). ## Distribution model -Production embeds frontend assets and Bun into one executable; targets need no -Bun/Node.js. Builds use the `roamgate` release identity across binaries, archives, -checksums, and manifests. Missing/legacy manifests fail closed without archive -discovery. Publication requires all six platform asset sets and no legacy update -aliases. Old clients require [manual migration](./DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui). +Production builds embed the frontend and Bun into one executable. Release +binaries, archives, checksums, and update manifests all use the `roamgate` name; +missing or legacy manifests fail closed. Publishing needs all six platform +asset sets. Old clients need [manual migration](./DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui). ## Trust boundary -Roamgate is trusted single-user administration, not a sandbox or multi-user -permission system. Normal runtime requires authentication, which grants full -authority; see [Security](../SECURITY.md#trust-model) for the local development -exception, login limits, TLS, and outer access controls. +Roamgate is trusted single-user administration; authentication grants full +authority (see [Security](../SECURITY.md#trust-model)). The browser accepts one +bridge hello before other messages; message kinds are validated and downstream +events cannot inject bridge fields. NDJSON lines are size-bounded, and malformed +terminal frames are dropped. + +State-changing HTTP endpoints need authentication plus a marker header; the +push and settings endpoints also reject cross-site requests: + +| Endpoint | Required header | +| --- | --- | +| Updates | `x-roamgate-update: 1` (legacy `x-herdr-gui-update: 1` is accepted; the new header wins) | +| Web Push subscriptions | `x-roamgate-push: 1`, JSON body | +| Instance settings | `x-roamgate-settings`, JSON body | -The browser accepts one unscoped bridge hello before other messages. Message kinds -are exclusive and validated; downstream events cannot inject reserved bridge -fields. NDJSON lines/acknowledgements are bounded; malformed terminal frames are -dropped. Observable HTTP traversal is rejected, but Bun may normalize dot segments -before routing; legacy aliases prevent distinguishing every such normalization. +Login bodies are limited to 16 KiB (`413`). Headers never replace login. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 4265d621..5c241658 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,70 +1,49 @@ # Installation and Deployment -Configuration reference for Roamgate. For a guided workflow and private remote -access, use the [tutorial](./TUTORIAL.md#networking). +Configuration reference for Roamgate. For a guided walkthrough and private +remote access, see the [tutorial](./TUTORIAL.md#networking). ## Requirements -- A running Herdr server, or [managed local setup](#managed-herdr-setup). -- Default Unix sockets: `~/.config/herdr/herdr.sock` and - `~/.config/herdr/herdr-client.sock`; Windows uses corresponding named pipes - under `%APPDATA%\herdr\`. -- [Bun](https://bun.sh) 1.4.3+ for source builds only; standalone needs no Bun/Node.js. - -Windows local Git features require [Git for Windows](https://gitforwindows.org/) -with Git available on PATH. Changes, Commits, and branch auto-sync locate its -bundled Bash beside Git or in standard system/per-user installation directories; -`sh.exe` does not need to be on PATH. For a portable or custom installation, -add its `cmd` or `bin` directory to PATH and restart Roamgate. These commands use -Git Bash even when Roamgate starts from PowerShell; SSH workspaces continue to -use the remote host's shell and tools. +- A running Herdr server, or let Roamgate [set one up](#managed-herdr-setup). + Roamgate uses Herdr's default sockets (`~/.config/herdr/herdr.sock` and + `herdr-client.sock`; named pipes under `%APPDATA%\herdr\` on Windows). +- Release binaries need nothing else. Source builds need + [Bun](https://bun.sh) 1.4.3+. +- On Windows, local Git features need [Git for Windows](https://gitforwindows.org/) + on PATH. ### Herdr compatibility -This source build supports verified legacy protocols 14–20 (Herdr 0.7.0–0.8.2) -and **tagged Herdr 0.9.0 and 0.9.4 / protocol 22**. Protocol 21 and unknown versions -are rejected at control/binary probes. Use a compatible Roamgate build or separate -server; **do not downgrade a live server**. Published binaries follow their -[release notes](https://github.com/powerfooI/roamgate/releases). -The [plugin](#herdr-plugin) separately requires Herdr 0.7.2+. -Tab reordering requires Herdr 0.7.2+; Herdr 0.7.0 and 0.7.1 retain their fixed -tab order. The bridge confirms support before enabling dragging. - -Herdr 0.9.0 and 0.9.4 use stable endpoint generation 1, distinct from protocol 22. -`ROAMGATE_DISABLE_ENDPOINT=1` explicitly selects legacy direct-terminal fallback. - -| Area | Behavior / limitation | +| Herdr | Support | | --- | --- | -| Attachment | Endpoints crop the server-rendered tab per pane. Unknown codecs/generations or missing required `pane.focus` fail without silent fallback. Legacy fallback uses takeover and may disconnect another owner. | -| Optional methods | Creation/history controls require advertised methods; unavailable controls explain why. Input, mouse, paste, resize, and rendering can work without optional history. | -| Navigation | Endpoint workspace/tab selection is browser-local across reconnects, not reload/runtime replacement. Same-tab pane focus, topology, and terminal sizes remain shared; size follows Herdr's last-interacting client. Legacy uses shared navigation. | -| Creation | Requires a connected source terminal, except first-workspace bootstrap. Preserves `terminal.new_cwd` (`follow`, `home`, `current`, fixed path); explicit cwd wins. Shared pane focus means `follow` is not browser-isolated. | -| Input | Pixel mouse and enhanced Kitty keyboard / modifyOtherKeys parity are unsupported; legacy keyboard-mode messages are decoded but not applied in-browser. | - -**OSC 52 clipboard writes:** only the browser with input in the last 30 seconds -matching the receiving endpoint session receives them, never passive viewers. -Herdr supplies no producing-pane/input identity: a delayed write from A after B -becomes foreground can reach B's recent input owner. This is not source-PTY or -original-browser isolation. Detach/replacement/disposal invalidates ownership. -Reads are disabled; permission failures show copy-retry UI. Ordinary copy/paste -is unchanged. OSC 52 is unavailable on the **protocol 22 legacy fallback**; older -servers retain their existing relay. - -Closing one workspace never implicitly closes its linked group. If Herdr requires -that, inspect all linked workspaces before explicitly running: - -```bash -herdr --session workspace close --group -``` - -See [endpoint contracts](./ARCHITECTURE.md#terminal-endpoints) and -[creation/timeouts](./ARCHITECTURE.md#browser-navigation-and-creation). +| 0.7.0–0.8.2 (protocols 14–20) | Legacy terminal mode | +| 0.9.0 and 0.9.4 (protocol 22) | Endpoint terminal mode (recommended) | +| Protocol 21 and unknown versions | Rejected | + +Do not downgrade a live Herdr server. The [plugin](#herdr-plugin) and tab +reordering need Herdr 0.7.2+. `ROAMGATE_DISABLE_ENDPOINT=1` forces legacy mode. + +Known limitations: + +- Endpoint mode shares pane focus, layout, and terminal size between clients; + only workspace/tab selection is per browser. Legacy mode shares everything and + may take over the terminal from another client. +- Creating tabs or workspaces needs a connected terminal (except the first + workspace in an empty session) and keeps Herdr's `terminal.new_cwd` policy. +- Pixel mouse reporting and enhanced Kitty keyboard modes are not supported. +- **OSC 52 clipboard writes** go only to the browser that typed into that + session in the last 30 seconds. Herdr does not say which pane produced a + write, so a late write can reach the wrong recent typist. Clipboard reads are + disabled. OSC 52 is unavailable in protocol 22 legacy mode. +- Closing a workspace never closes its linked group. To do that, inspect the + group and run `herdr --session workspace close --group`. + +See [endpoint contracts](./ARCHITECTURE.md#terminal-endpoints). ## Install a release -Roamgate 0.7.0+ releases provide Linux/macOS/Windows x86-64 and ARM64 assets. -Older releases may lack native Windows ARM64; prefer it when available. -Unpublished versions require a [source build](#build-a-standalone-executable). +Releases include Linux, macOS, and Windows binaries for x86-64 and ARM64. On Linux/macOS, the checksum-verifying installer writes `~/.local/bin/roamgate`: @@ -74,25 +53,21 @@ curl -fsSL \ | sh ``` -Add `~/.local/bin` to PATH, run `roamgate --version`, then `roamgate` and open the -printed URL. Rerun the installer or run `roamgate update` to update; -`roamgate rollback` restores the previous executable. +Add `~/.local/bin` to PATH, then run `roamgate` and open the printed URL. +`roamgate update` updates (or rerun the installer); `roamgate rollback` restores +the previous binary (`roamgate.previous`). -On Windows, download matching `roamgate-windows-.tar.xz` and `.sha256` -files from the [latest release](https://github.com/powerfooI/roamgate/releases/latest), -verify with `Get-FileHash`, extract with Windows 11's `tar.exe`, and run `roamgate.exe`. +On Windows, download `roamgate-windows-.tar.xz` and its `.sha256` from the +[latest release](https://github.com/powerfooI/roamgate/releases/latest), verify +with `Get-FileHash`, extract with `tar.exe`, and run `roamgate.exe`. -Installer overrides apply to the `sh` command above: +Installer settings go on the `sh` side of the pipe (`curl ... | sh`): -| Setting | Example / behavior | +| Installer setting | Example / behavior | | --- | --- | | Pin a version | `ROAMGATE_VERSION=X.Y.Z sh` (no `v`; empty means latest) | -| System directory | `sudo env ROAMGATE_INSTALL_DIR=/usr/local/bin sh` (empty rejected) | -| Release mirror | `ROAMGATE_RELEASE_BASE_URL` selects a compatible flat asset directory | - -Mirrors require HTTPS except loopback testing; URLs cannot contain credentials, -queries, or fragments. Installer and in-app updater preserve the old executable -as `roamgate.previous` for manual recovery. +| Install directory | `sudo env ROAMGATE_INSTALL_DIR=/usr/local/bin sh` | +| Release mirror | `ROAMGATE_RELEASE_BASE_URL= sh` | ## Managed Herdr setup @@ -101,107 +76,70 @@ roamgate herdr setup roamgate herdr status ``` -If absent, setup downloads **Herdr 0.9.4**, verifies build-pinned SHA-256 hashes, -and installs to `~/.local/bin/herdr` on Unix or -`%APPDATA%\roamgate\herdr\0.9.4` on Windows. It never replaces a binary it did not -install. If already installed, it uses the first binary on PATH or in those -locations, leaving it in place. Unix's standard location supports `herdr update`; -Windows uses Roamgate's private versioned directory, not the official junctioned -store, and needs a newer verified Roamgate install for replacement. - -The verified Windows asset is x64 only; managed setup has no native Windows -ARM64 Herdr asset. Windows ARM64 SSH hosts can run Herdr's x64 build under -emulation on a best-effort basis. - -Setup installs/starts `herdr server` as a user service: `roamgate-herdr.service` -(Linux), `dev.roamgate.herdr` (macOS), or a per-user Windows scheduled task. -Definitions carry a Roamgate marker; unrelated existing definitions are untouched. -The web UI offers the same confirmed **Set up Herdr / Start Herdr** action when -the default local server is unreachable, with visible failures/retry. +If Herdr is missing, setup downloads Herdr 0.9.4, verifies its pinned SHA-256, +and installs it (`~/.local/bin/herdr`, or `%APPDATA%\roamgate\herdr\0.9.4` on +Windows x64). An existing Herdr on PATH is used as is. Setup then runs +`herdr server` as a user service: `roamgate-herdr.service` (Linux), +`dev.roamgate.herdr` (macOS), or a per-user scheduled task (Windows). The web UI +offers the same **Set up Herdr / Start Herdr** action when the local server is +unreachable. -Only default local configuration is supported. SSH profiles/`--ssh-host`, named -sessions, and explicit control/render socket flags or environment variables are -refused; start Herdr yourself for those configurations. +Only the default local setup is supported; for SSH, named sessions, or custom +sockets, start Herdr yourself. -To stop and disable, use native tools: +To remove the service: ```bash systemctl --user disable --now roamgate-herdr.service # Linux launchctl bootout gui/$(id -u)/dev.roamgate.herdr # macOS -# Windows: find dev.roamgate.herdr- in herdr-task.ps1 +# Windows: find the task name (dev.roamgate.herdr-) in herdr-task.ps1 schtasks /End /TN "" schtasks /Delete /TN "" /F ``` -Then remove only its generated definition: -`~/.config/systemd/user/roamgate-herdr.service`, -`~/Library/LaunchAgents/dev.roamgate.herdr.plist`, or -`%APPDATA%\roamgate\herdr-task.ps1`. Linux also needs -`systemctl --user daemon-reload`. Remove an installed binary only if unwanted -and owned by this setup; never delete the shared `~/.local/bin` directory. +Then delete its definition (`~/.config/systemd/user/roamgate-herdr.service` plus +`systemctl --user daemon-reload`, `~/Library/LaunchAgents/dev.roamgate.herdr.plist`, +or `%APPDATA%\roamgate\herdr-task.ps1`). ## Transition from Herdr Studio / herdr-gui -**Herdr Studio / herdr-gui 0.6.2 and earlier require manual installation.** -Roamgate 0.7.0+ publishes only `roamgate-*` assets/manifests and -`install-roamgate.sh`. Old Latest/update channels no longer deliver fixes and may -error; historical tagged downloads and running processes remain intact. -Legacy assets are not republished because old clients can discover archives -without manifests. Custom mirrors/pinned historical downloads are outside this cutoff. - -**Service, data, and plugin identity migration requires Roamgate 0.7.1+.** -0.7.0 can update its binary through the new channel, but still uses legacy service -names, paths, and `herdr.studio` plugin ID. Binary updates never rename services -or migrate plugin registrations automatically. - -1. Back up executable, service definition, and config with permissions intact. - Include `~/.config/herdr-gui/` and Windows `%APPDATA%\herdr-gui\` plus the old - `~/.config/herdr-gui/` settings/profile location. -2. Before replacing the executable, stop/uninstall with `herdr-gui service uninstall` - or the previous Roamgate binary's `service uninstall`. For custom wrappers, - stop/disable/archive through the native manager and preserve args/environment. - Stop unmanaged processes too; never leave both auto-start entries enabled. -3. Install 0.7.1+ or build source, then explicitly run `roamgate service install` - (`./server/roamgate service install` for source). Install rejects existing/loaded - legacy services and detection errors, even with `--force`, before changing files. -4. Verify status, login, and connections. To roll back, uninstall with the new - binary, restore the old binary/definition, and enable only that service. - New-directory changes are not synchronized back. - -After an in-place 0.7.0 update, the new CLI can route status/restart/reload/uninstall -to the **sole unmodified generated legacy definition pointing at this executable**. -It preserves name/environment/data and installs nothing automatically. Ambiguous -old/new services, custom/symlinked definitions, loaded services missing definitions, -or detection errors require the previous binary/native manager; the CLI never guesses. - -New data lives in `~/.config/roamgate` or Windows `%APPDATA%\roamgate`: - -- Missing tokens/settings/connections copy on first use; service install also - copies `herdr-gui.env` to `roamgate.env`. Existing new files win, even empty/invalid. -- Copies retain originals and restrict permissions to the owner's existing - read/write bits. Copy failures stop without replacement credentials; legacy - symlinks are rejected. Explicit registry-path overrides are not migrated. -- Windows reads old settings/profiles from historical `~/.config/herdr-gui`. - Stop old writers first: migration is one-way, not synchronization. -- Browser `roamgate:` keys copy missing legacy values on the **same origin** only. - New values win; remembered deletions prevent old drafts reappearing. The website - checklist follows the same rule. Host/port changes cannot transfer storage. - -[Plugin migration](#herdr-plugin) is explicit. Historical releases retain their contracts. +**Herdr Studio / herdr-gui 0.6.2 and earlier cannot auto-update to Roamgate.** +Their update channel no longer receives fixes. Service, data, and plugin +migration needs Roamgate **0.7.1+**. + +1. Back up the executable, service definition, and config, keeping permissions: + `~/.config/herdr-gui/`, plus `%APPDATA%\herdr-gui\` on Windows (Windows + also keeps settings and connections in `~/.config/herdr-gui/`). +2. Stop and uninstall the old service with `herdr-gui service uninstall` (or the + old binary's `service uninstall`). Stop any unmanaged processes too. Never + leave both services enabled. +3. Install Roamgate and run `roamgate service install`. It refuses to run while + a legacy service exists, even with `--force`. +4. Check status, login, and connections. To roll back, uninstall with the new + binary and restore the old binary and service. + +On first use, Roamgate copies missing tokens, settings, and connections into +`~/.config/roamgate` (`%APPDATA%\roamgate` on Windows), and service install +copies `herdr-gui.env` to `roamgate.env`. Existing new files always win. The +copy is one-way; stop old processes first. Files at custom +`HERDR_GUI_SETTINGS_PATH`/`HERDR_GUI_CONNECTIONS_PATH` locations are not copied. +Browser preferences carry over only when the URL (host and port) is unchanged. +Migrate the [plugin](#herdr-plugin) separately. + +After an in-place update from 0.7.0, the new CLI manages an old service only if +it is the single unmodified generated definition for this executable. Otherwise +(custom, ambiguous, or missing definitions), use the previous binary or the +native service manager. ### Repository and website addresses -Use [powerfooI/roamgate](https://github.com/powerfooI/roamgate) and - ([tutorial](https://roamgate.dev/tutorial/)). GitHub redirects -old `powerfooI/herdr-studio` Git/release URLs; **do not reuse the old repo name**, -which would remove redirects. The `/roamgate/` Pages URL redirects to the new site; -do not rely on `/herdr-studio/`. +The project lives at [powerfooI/roamgate](https://github.com/powerfooI/roamgate) +and . Old `powerfooI/herdr-studio` URLs redirect. ### Install historical Herdr Studio -The retained [legacy installer](../scripts/install-herdr-gui.sh) installs only -`herdr-gui`, has no working Latest channel, and is absent from new releases. -Pin a historical tag, with no future fixes through that channel: +The [legacy installer](../scripts/install-herdr-gui.sh) installs `herdr-gui` +only and must be pinned to a historical tag: ```bash curl -fsSL \ @@ -211,26 +149,20 @@ curl -fsSL \ ## Herdr plugin -Requires Herdr 0.7.2+. The native plugin uses the standalone Roamgate executable -for its actions and panel; users do not need Bun or Node.js. Unix installation -uses the same system tools as the standalone installer; Windows uses Windows -PowerShell and `tar.exe`. Bun remains a source-build dependency. -Herdr's GitHub plugin installation also requires Git. -Plugin ID is `roamgate` from 0.7.1; 0.7.0 uses `herdr.studio`. +The plugin (ID `roamgate`; `herdr.studio` in 0.7.0) needs Herdr 0.7.2+ and Git. +It uses the standalone executable, so Bun is not required. -**Migrating `herdr.studio`:** stop/uninstall its old service first, close old plugin -panes, finish pending actions, then unregister: +Install a published release (0.7.1 or newer): ```bash -herdr plugin disable herdr.studio -herdr plugin unlink herdr.studio +herdr plugin install powerfooI/roamgate --ref vX.Y.Z ``` -Unlink retains checkout/data, including managed checkouts. Keep them for rollback; -do not run old and new start actions together. Roamgate never edits Herdr's registry. +The installer verifies the package checksum, version, and platform before +replacing the executable. To upgrade, run the same command with the new tag; +`herdr plugin update` stays on the original tag. -For an **unreleased checkout**, build before linking (existing binaries are not -validated). Keep the checkout and rebuild after updates: +For an unreleased checkout, build before linking, and rebuild after updates: ```bash git clone https://github.com/powerfooI/roamgate.git @@ -239,29 +171,6 @@ bun scripts/studio-plugin.ts build-source herdr plugin link . ``` -`build-source` installs the root workspace with its frozen lockfile and builds -`server/roamgate` (`roamgate.exe` on Windows). Linking after success skips release -download; neither step starts the service. - -For a **published release**, replace `X.Y.Z` with 0.7.1 or newer: - -```bash -herdr plugin install powerfooI/roamgate --ref vX.Y.Z -``` - -The system installer downloads that version's package into a staging directory, -checks its SHA-256, version and platform, and verifies native plugin commands -before publishing the executable. Failure preserves an existing executable and -never falls back to compilation or legacy assets. Tags predating native plugin -commands still use their own Bun launcher; installing an unreleased checkout -against one of those older release packages fails with source-build guidance. - -To upgrade a version-pinned installation, run the install command with the new -published tag. Herdr 0.9.4's `herdr plugin update roamgate` follows the original -ref, so a fixed tag remains pinned. Locally linked checkouts are updated and -rebuilt by their owner. Neither installation nor plugin update starts Roamgate -automatically. - Plugin actions manage the same [user service](#run-as-a-user-service): ```bash @@ -269,122 +178,87 @@ herdr plugin action invoke roamgate.start # install/start herdr plugin action invoke roamgate.url # login URL herdr plugin action invoke roamgate.status herdr plugin action invoke roamgate.restart -herdr plugin action invoke roamgate.uninstall # remove service, retain data +herdr plugin action invoke roamgate.uninstall # remove service, keep data ``` -Actions are asynchronous. Inspect `herdr plugin log list --plugin roamgate` or -open `herdr plugin pane open --plugin roamgate --entrypoint panel` for status, -URL, version, and start/restart/uninstall controls. Default is a session-modal -popup; `--placement split`, `tab`, `zoomed`, or `overlay` creates a regular pane -visible to other Herdr clients. +Actions run asynchronously; check `herdr plugin log list --plugin roamgate`, or +open the panel with `herdr plugin pane open --plugin roamgate --entrypoint panel`. +After updating the plugin, reopen the panel before Start or Restart. -After updating the plugin, close and reopen its panel before choosing Start or -Restart. Herdr 0.9.4+ keeps already-open plugin panes on their previous files. -Both controls register the current plugin's binary with the user service and -restart it, preserving its environment file and credentials. Start retains its -forced-install behavior; Restart refuses to overwrite a service definition that -Roamgate did not generate. Use the standalone `roamgate service restart` command -to restart an existing custom definition without changing it. - -The panel and actions invoke `roamgate plugin` directly. The source-only -`scripts/studio-plugin.ts` entry point remains available for build helpers and -delegates runtime verbs to the built executable. +**Migrating from `herdr.studio`:** uninstall its service, close its panes, then +run `herdr plugin disable herdr.studio` and `herdr plugin unlink herdr.studio`. ## Basic runtime configuration -Flags override environment variables, then defaults; `roamgate --help` lists all -options. Standalone ignores cwd `.env`/`bunfig.toml`: export variables, pass flags, -or edit the service environment file. Source `bun run` retains normal Bun loading. - -`ROAMGATE_*` accepts legacy `HERDR_GUI_*` aliases, including installer settings. -The new name wins **even when empty**. Herdr's `HERDR_*` settings are unchanged; -explicit connection registry paths remain authoritative, including empty values. +Flags override environment variables, which override defaults. Release binaries +ignore `.env` files: export variables, pass flags, or edit the service +environment file. `roamgate --help` lists everything. | Flag | Environment variable | Default | | --- | --- | --- | | `--host ` | `HOST` | `127.0.0.1` | | `--port ` | `PORT` | `8787` | -| `--password ` | `ROAMGATE_PASSWORD` | Generated token when authentication is enabled | -| None | `ROAMGATE_PIN` | Disabled; optional 6-12 digit login for private networks only | -| `--tls-cert ` | `ROAMGATE_TLS_CERT` | Disabled; PEM chain, requires key | -| `--tls-key ` | `ROAMGATE_TLS_KEY` | Disabled; PEM key, requires certificate | -| `--socket-path ` | `HERDR_SOCKET_PATH` | Default control socket/pipe | -| `--client-socket-path ` | `HERDR_CLIENT_SOCKET_PATH` | Default render socket/pipe | +| `--password ` | `ROAMGATE_PASSWORD` | Generated token | +| None | `ROAMGATE_PIN` | Disabled; see [PIN login](#optional-pin-login) | +| `--tls-cert ` | `ROAMGATE_TLS_CERT` | Disabled; PEM chain | +| `--tls-key ` | `ROAMGATE_TLS_KEY` | Disabled; PEM key | +| `--socket-path ` | `HERDR_SOCKET_PATH` | Default control socket | +| `--client-socket-path ` | `HERDR_CLIENT_SOCKET_PATH` | Default render socket | | `--ssh-host ` | `HERDR_SSH_HOST` | Disabled; Linux/macOS only | | `--session ` | `HERDR_SESSION` | Default session | | `--public-dir ` | `PUBLIC_DIR` | Embedded assets | | `--log-level ` | `ROAMGATE_LOG_LEVEL` | `info` | -| `--notification-source ` | `ROAMGATE_NOTIFICATION_SOURCE` | `herdr`; see [Web Push](#web-push-notifications) | -| `--profile` | `ROAMGATE_PROFILE=1` | Disabled; bounded server CPU capture | -| `--profile-duration ` | `ROAMGATE_PROFILE_DURATION` | `30`; integer 1..300 | -| `--profile-dir ` | `ROAMGATE_PROFILE_DIR` | Roamgate data directory's `profiles/` | +| `--notification-source ` | `ROAMGATE_NOTIFICATION_SOURCE` | `herdr` | +| `--profile` | `ROAMGATE_PROFILE=1` | Disabled; see [profiling](#performance-profiling) | +| `--profile-duration ` | `ROAMGATE_PROFILE_DURATION` | `30` (1-300) | +| `--profile-dir ` | `ROAMGATE_PROFILE_DIR` | `/profiles` | | `--open` | `OPEN_BROWSER=1` | Disabled | -| Additional environment variable | Purpose | +| Other environment variable | Purpose | | --- | --- | -| `ROAMGATE_UPDATE_BASE_URL` | Latest-release mirror directory | +| `ROAMGATE_UPDATE_BASE_URL` | HTTPS update mirror directory | | `ROAMGATE_DISABLE_UPDATE_CHECK=1` | Disable update checks | -| `ROAMGATE_RESTART_SUPERVISOR=0\|1` | Override external supervisor detection | -| `ROAMGATE_DISABLE_ENDPOINT=1` | Legacy terminal fallback; see compatibility | -| `ROAMGATE_ALLOW_FILE_REVEAL=1` | Opt into [host file reveal](#host-file-reveal); disabled by default | -| `ROAMGATE_UPLOAD_MAX_BYTES` | Maximum size of a terminal file upload; default 100 MiB | -| `ROAMGATE_UPLOAD_RETENTION_HOURS` | Staged terminal file retention; default 24 hours | - -Update mirrors need platform archives, `.sha256` files, and -`roamgate-.update.json` with `name: "roamgate"`. Missing/legacy manifests -fail closed without archive discovery. HTTPS is required except loopback tests; -credentials, queries, and fragments in URLs are rejected. +| `ROAMGATE_RESTART_SUPERVISOR=0\|1` | Override supervisor detection | +| `ROAMGATE_DISABLE_ENDPOINT=1` | Force legacy terminal mode | +| `ROAMGATE_ALLOW_FILE_REVEAL=1` | Enable [host file reveal](#host-file-reveal) | +| `ROAMGATE_UPLOAD_MAX_BYTES` | Max terminal upload size (default 100 MiB) | +| `ROAMGATE_UPLOAD_RETENTION_HOURS` | Upload retention (default 24) | +| `ROAMGATE_SETTINGS_PATH` | GUI settings file (one per bridge) | +| `ROAMGATE_CONNECTIONS_PATH` | Connection profiles file | +| `ROAMGATE_ASSISTANT_DIR` | Ranger data directory | +| `ROAMGATE_WEB_PUSH_PATH` / `ROAMGATE_WEB_PUSH_SUBJECT` | [Web Push](#web-push-notifications) | + +Legacy `HERDR_GUI_*` names still work; the `ROAMGATE_*` name wins, even when +empty. ```bash -roamgate # loopback, generated token -roamgate --host 0.0.0.0 --port 8787 # generated token +roamgate # loopback, generated token +roamgate --host 0.0.0.0 --port 8787 # all interfaces, generated token ``` -For a fixed password, prefer `ROAMGATE_PASSWORD` over process-visible -`--password`. It must contain 8-1024 Unicode characters; values outside that -range stop startup. Startup warns below 15 characters, the recommended length -for any listener reachable beyond a trusted private network. Normal runtime -requires login; see [Security](../SECURITY.md#trust-model) for the local development -exception, login limits, and remote access guidance. +Prefer `ROAMGATE_PASSWORD` over `--password`. Passwords must be 8-1024 +characters; use 15+ for anything beyond a trusted private network. See +[Security](../SECURITY.md#trust-model). ### Optional PIN login -For a server reachable only through a trusted private network or VPN, set -`ROAMGATE_PIN` to 6-12 ASCII digits in the process environment or protected -service environment file, then restart. There is deliberately no CLI flag. -Unset or empty disables it; invalid values stop startup. Keep leading zeros. -A PIN adds a weaker alternative credential and grants full access, so do not -use it on a public listener. The server prints a warning when it is enabled. - -The form starts in numeric PIN mode and can switch to password/token login. -`ROAMGATE_PASSWORD` still selects the strong login credential; when it is absent, -the existing generated token and token URLs remain available. The PIN never -replaces a signing key. Ten consecutive failures across all sources pause PIN -login for one hour; per-source limits also apply. Strong-credential login remains -available during that cooldown. See [PIN security and limits](../SECURITY.md#optional-pin-login). - -Adding, changing, or removing the PIN rotates the independent session signing -secret at the next listener startup, just like changing the strong credential. -Stop all listeners for the same instance before changing credentials. +For a server reachable only through a trusted VPN or private network, set +`ROAMGATE_PIN` to 6-12 digits (environment or service file only; there is no +flag) and restart. The login form then starts in PIN mode, with the +password/token still available. Changing the PIN signs out all browsers. See +[PIN security](../SECURITY.md#optional-pin-login). ### Host file reveal -Desktop file reveal is **off by default**. To enable it, set -`ROAMGATE_ALLOW_FILE_REVEAL=1` in the host process environment (or the service -environment file and restart the service). Only local Herdr profiles and -loopback TCP peers are eligible; SSH profiles and non-loopback peers are refused. -The menus open the **Roamgate host's** file manager, which requires a usable -desktop session. Foreground focus, especially on Windows, is best effort. - -**Opt-in also allows loopback-forwarded/tunnel clients to open windows on the -host.** A loopback peer or browser URL cannot prove the browser runs on that -machine. Enable only when that is acceptable for every client reaching the -loopback listener, including reverse proxies and SSH forwards. The browser's -platform is not used to name the host desktop. +**Off by default.** With `ROAMGATE_ALLOW_FILE_REVEAL=1`, local profiles accessed +over loopback get **Reveal on host** and **Open folder on host**, which open the +file manager on the Roamgate host. SSH profiles and non-loopback clients are +refused. **Tunnelled or reverse-proxied loopback clients count as local too**, +so enable it only if every such client may open windows on the host. ### Native HTTPS -Supply both a PEM chain (leaf first) and matching unencrypted private key: +Pass a PEM certificate chain (leaf first) and its unencrypted key: ```bash roamgate --host 0.0.0.0 --port 8443 \ @@ -392,13 +266,11 @@ roamgate --host 0.0.0.0 --port 8443 \ --tls-key /path/to/private-key.pem ``` -Missing/unreadable/malformed/mismatched files stop startup, never fall back to -HTTP. Without TLS settings, HTTP is used. HTTPS adds `Secure` cookies and HTTPS -startup links; it does not change the [authentication rules](../SECURITY.md#trust-model). -Do not expose directly to the public internet. +Invalid files stop startup instead of falling back to HTTP. HTTPS only adds +encryption; it does not change authentication. -For private LAN testing, use your issuer or [mkcert](https://github.com/FiloSottile/mkcert). -Replace this reserved example IP with the host's actual LAN address: +For LAN testing with [mkcert](https://github.com/FiloSottile/mkcert) (use your +host's real LAN IP): ```bash mkcert -install @@ -407,437 +279,159 @@ roamgate --host 0.0.0.0 --port 8443 \ --tls-cert "$PWD/cert.pem" --tls-key "$PWD/key.pem" ``` -- SANs must match each client hostname/IP; `localhost` does not cover LAN addresses. -- Every device must trust the issuer. Transfer only mkcert's `rootCA.pem` from - `mkcert -CAROOT`, **never `rootCA-key.pem`**. On iOS, install the CA profile and - enable full trust in Settings > General > About > Certificate Trust Settings. -- Bypassing warnings does not enable Service Workers/secure APIs. Verify trusted - HTTPS before Home Screen installation; remove temporary CA profiles after tests. -- Protect keys and keep them out of Git. Issuance/renewal is external; restart - after replacement. Services need absolute paths in their environment file. +Every device must trust the CA: copy only `rootCA.pem` (from `mkcert -CAROOT`), +**never `rootCA-key.pem`**. On iOS, install the profile and enable it under +Settings > General > About > Certificate Trust Settings. Clicking through +certificate warnings does not enable PWA features. Restart after renewing +certificates; services need absolute paths. ## Ranger model connection -Open the global **Ranger** window and its **Settings** button. Choose -**Ranger connection** or **Shared Pi credentials**, select a provider in the -**Provider** picker, and use the available **Sign in** or **Enter API key** -action. Complete the provider's web/device-code or manual authorization prompt -in the window, choose a default model, select allowed workspaces under -**Workspace access**, and **Save settings**. Each question automatically uses saved -authorized workspaces that are currently available; no workspaces are authorized -by default. - -**High-permission mode** is in the permissions menu below the chat input, off by -default. The button shows **Ask for approval**, **Full access**, or **High permission** -for older configurations limited to selected workspaces. Enabling it requires an -explicit confirmation and authorizes all -currently discoverable workspaces, including new workspaces that appear while -the mode is enabled, -without individual selection. Workspace content may be sent to the selected -model provider. Ranger can then execute its supported workspace, -worktree, tab, pane, agent and prompt operations, and create schedules, without -individual approval. Each admitted question and scheduled task retains its -concrete workspace targets; new workspaces do not widen an already-running turn -or an existing task. Disabling the mode restores the saved normal-mode workspace -selection and takes effect before subsequent -automatic operations, including in running tasks; an operation already dispatched -may finish. -Existing tasks retain their saved mode and need an edit to adopt high-permission -mode. Ranger's built-in write/command tools remain disabled. - -Existing saved high-permission configurations from older versions keep their -selected-workspace permissions. Confirm the updated all-workspaces option once -to authorize the broader scope; upgrading alone does not grant it. - -Deleted workspaces are removed from the list and saved normal-mode selection -after a successful complete inventory confirms their absence. Temporary -disconnects, failed listings, and truncated inventories do not erase saved -permissions. A deleted workspace that later reappears must be selected again in -normal mode; high-permission mode includes it automatically. Inventories and -concrete scopes are bounded to 512 workspaces. If an inventory is truncated, -Ranger will not silently treat the partial result as all workspaces. Large scopes -can take longer to validate and send more workspace context to the model. - -For a compatible endpoint, select **Add** under **Custom models**. The editor -shows **Models** (model IDs and supported efforts) and **Endpoint** (provider -ID, API format, API base URL, and API key) together. When a custom provider is -selected, **Add** keeps its endpoint and starts with no model IDs, so new IDs -join that provider; enter a different provider ID for a new endpoint. Enter one -model ID per line or separate IDs with commas, up to 100 IDs of 500 characters -each. -Duplicate IDs are saved once. Supported formats are OpenAI Chat Completions, -OpenAI Responses, and Anthropic Messages. Use a dummy key for an unauthenticated -local server. **Save custom models** saves the whole batch and selects the first -ID; then select allowed workspaces and -**Save settings**. Select a custom provider and model and choose **Edit** to -change that model. -**Save custom models** returns to Settings after saving; **Back** or **Cancel** -abandons unsaved editor changes. A blank key keeps its existing key. -Stop running tasks before changing a model connection. Existing models outside -the batch keep their settings. -Use a separate provider ID for a different endpoint so existing providers keep -their connection settings. - -Under **Supported thinking efforts**, select the levels your endpoint supports: -Off, Minimal, Low, Medium, High, Extra high, and Maximum. New models start with -Off selected. Existing models show their saved supported levels; leaving the -checklist unchanged preserves each model's capabilities and Pi thinking-level -maps. Changing the checklist applies the selected levels to every entered ID, -so save separate batches for models with different capabilities. At least one -level is required. Existing Pi compatibility settings are preserved. - -You determine which levels a third-party endpoint supports; saving a selection -does not verify those capabilities. Pi translates the selected effort into the -chosen API format. Select the desired **Thinking effort** and **Save settings**, -or use the chat selector. Existing reasoning models without an explicit level -list or Pi thinking-level map expose Off, Minimal, Low, Medium, and High. - -The **Provider** picker lists providers with saved OAuth logins and API keys -from the selected credential source first, each with its credential status, -followed by providers without saved credentials. Type to search by name or ID. -Select a provider to choose it or log in from the window. The refresh button -next to **Provider** reads changes made outside the window. These labels report -stored credentials, not a successful live connection test; credentials are -checked or refreshed when used. - -Ranger runs as the Roamgate bridge's OS account, including when the browser -is on a phone or a workspace uses SSH. Provider credentials therefore belong on -the bridge host. Separate Ranger credentials live in -`~/.config/roamgate/assistant/auth.json` or -`%APPDATA%\roamgate\assistant\auth.json`; custom endpoints live in `models.json` -beside them. Configuration, the active conversation, -model context, and history summaries live beside them in `state.json`. Inactive -conversations live in private `sessions/.json` files. Durable execution -state and tool results live in `durable//execution.sqlite`. Stop the bridge -before backing up the whole assistant directory to retain saved chats, database -files and credentials. These files may contain secrets or workspace content. -Keep them private and use one bridge -process per data directory. -`ROAMGATE_ASSISTANT_DIR` overrides this directory; the legacy -`HERDR_GUI_ASSISTANT_DIR` alias is also accepted. - -An unfinished question resumes when the bridge restarts and its original -connections become ready, even with the browser closed. Recovery requires the -same Herdr server boot and workspace identity and a supported endpoint handshake. -It waits for manually connected profiles until the user reconnects them; it does -not connect additional profiles itself. If Herdr also restarted or the original -target, permissions, or provider is unavailable, send a new question. **Stop** -cancels work; stopping the bridge pauses it. Confirmed management operations are -never automatically retried. Durable stores have no automatic retention quota. - -The Ranger **Tasks** view manages one-time, daily, and interval schedules. -Task creation requires an endpoint with a stable Herdr boot identity, the chosen -model, and an explicitly allowed workspace scope. If creation fails, the -returned error names the unmet requirement, such as an older Herdr server -that does not expose a boot identity. Tasks keep their original -model and target identities; after a Herdr restart or target change, edit the -task to authorize the current targets. The bridge must be running for schedules -to fire. After downtime, missed occurrences are combined into one run. -If an original connection is temporarily unavailable, its run stays queued while -other ready tasks can run. Manual connection profiles still require the user to -reconnect them; scheduling does not override their `auto_connect` setting. -Daily times use the task's IANA timezone, independently of the bridge's local -timezone; daylight-saving gaps are skipped and overlaps fire only once. -Plans, proposals, deduplication records and run receipts live in `tasks.sqlite`, -with private per-run state under `tasks//runs//`. The task -database and Pi Durable databases use WAL mode with `synchronous = FULL`. -The task database upgrades schemas 1 and 2 to schema 3 automatically and -transactionally to store notification modes, custom-notification receipts, and -bound workspace/Agent references. Older builds -cannot open the upgraded task database. If you need rollback, back up the Ranger -data directory while the bridge is stopped before upgrading. -An invalid database disables task scheduling instead of discarding saved plans. -Task runs share the selected bridge credential store; credentials are not copied -into run directories. Include -these files when backing up the assistant directory. The latest 20 runs per -task are retained; cancelling keeps history, while deleting a cancelled task -removes its saved runs. There is no aggregate byte quota for durable stores. - -**Shared Pi credentials** explicitly opts into Pi's existing credential store -for that bridge account. Signing in or entering an API key in this mode updates -that store, so Pi can also use the saved credentials. The separate **Ranger connection** -keeps its own store. Neither mode requires terminal interaction to log in. -The catalog uses credentials saved in Pi's `auth.json` and compatible endpoints -in Pi's `models.json`. Custom model changes in shared mode update that file. -**Refresh** rereads both files. Environment-only credentials for built-in -providers are not imported. Model-file keys may use Pi's environment or command -syntax; entering a key in the custom model editor accepts literal keys only. -Pi's extensions, skills, project instructions, and -built-in write/command tools are not loaded into Ranger. - -Pi SDK 1.1 names its Azure provider `azure`. Ranger also keeps -`azure-openai-responses` as **Azure OpenAI (legacy ID)**, so existing selections, -scheduled tasks, and interrupted runs continue using their original credentials -and model configuration. No file migration is required. When both provider IDs -are configured, their credentials and custom model overrides stay independent; -Ranger does not copy or rename either store entry. The Responses model API value -remains `azure-openai-responses` for both provider IDs. - -For remote bridge deployments, follow the provider's displayed device-code or -manual authorization flow. A browser's localhost callback points to the browser -machine; it does not automatically reach the bridge host. Cancelling a login -does not disconnect the bridge or its Herdr profiles. See -[Ranger behavior](../FEATURES.md#ranger) and -[tool and session contracts](ARCHITECTURE.md#ranger). - -Saved grants for temporarily unavailable workspaces remain visible under -**Unavailable authorized workspaces**. Remove one permission without clearing -other grants, then save settings. A complete authoritative listing still removes -grants for deleted workspaces. High mode must be turned off before restricting -its all-workspace access with individual manual grants. - -On Windows SSH workspaces, Ranger can read workspace status and terminal output. -Agent history reads, Git diff reads, and worktree creation fail with an error -stating that they require a Unix SSH host. See -[Ranger tool contracts](ARCHITECTURE.md#ranger). +Open **Ranger > Settings**, choose a **Provider**, and **Sign in** or **Enter API +key**. Then pick a model, allow workspaces under **Workspace access**, and +**Save settings**. + +- **Shared Pi credentials** reuses Pi's `auth.json` and `models.json` for the + bridge account instead of Ranger's own store. +- **Custom models** add an OpenAI- or Anthropic-compatible endpoint; use a + separate provider ID per endpoint. +- On a **remote bridge**, use the provider's device-code or manual login; a + `localhost` callback would reach the browser's machine, not the bridge. +- **High-permission mode** grants every workspace and skips confirmations. + Workspace content may be sent to the model provider. High-permission settings + saved by older versions keep their selected workspaces until you confirm the + all-workspaces option once. +- On Windows SSH hosts, Ranger can read status and terminal output only. + +Ranger runs as the bridge's OS account and keeps its data (credentials, +conversations, and scheduled tasks) in `~/.config/roamgate/assistant/` +(`%APPDATA%\roamgate\assistant\`, or `ROAMGATE_ASSISTANT_DIR`). It may contain +secrets: keep it private, use one bridge per directory, and stop the bridge +before backing it up. **Upgrades can migrate `tasks.sqlite` irreversibly**, so +older builds cannot open it: back up the directory before upgrading if you may +roll back. + +Unfinished questions and task runs resume after a bridge restart if their +original connections and workspaces are still the same; confirmed actions are +never repeated. See [Ranger](../FEATURES.md#ranger) and +[contracts](ARCHITECTURE.md#ranger). ## Web Push notifications -1. Use trusted HTTPS; on iOS/iPadOS 16.4+, open the installed Home Screen app. -2. Enable **Task notifications**, grant permission, and choose **Task needs attention** - and/or **Task completed**. **Background push** confirms enrollment; existing - local-only users should toggle off/on once. - -Server push is enabled by default; every device still needs enrollment. -`roamgate push test` sends a test notification to every enrolled device. -`ROAMGATE_WEB_PUSH_SUBJECT` optionally sets a `mailto:`/HTTPS operator contact, -not a delivery destination. Default: `https://github.com/powerfooI/roamgate/issues`. -An explicitly empty value disables push; restart after changes. Removing the -variable restores the default and enables delivery. - -Private VAPID keys/subscriptions live in `~/.config/roamgate/web-push.json` or -`%APPDATA%\roamgate\web-push.json`; override with `ROAMGATE_WEB_PUSH_PATH`. -**Protect/back up this file, never share/commit it, and use one writer per file.** -Corrupt data is preserved and push disabled rather than silently rotating keys. -After intentional key replacement, reopen/toggle notifications to re-enroll. -Browser profiles/subscriptions are device-specific. - -After upgrading Roamgate, reopen or reload each browser/PWA once to activate the -current notification worker. Until then, an older worker can show a Ranger alert -but open only the app instead of its task run when clicked. - -Delivery needs outbound HTTPS to Apple (`*.push.apple.com`), Google -(`fcm.googleapis.com`), Mozilla (`*.push.services.mozilla.com`), or Windows -(`*.notify.windows.com`); other providers are rejected. No public inbound endpoint -is needed. Devices must reach their provider and Roamgate when opening an alert. - -The bridge and relevant Herdr runtimes must stay connected; alerts arrive without -open browsers. The default `herdr` notification source relays Herdr's own -notifications (Herdr 0.9+), so Roamgate alerts match Herdr's toasts, including -`[ui.notifications] external_agents` suppression and alerts sent with -`herdr notification show`. Roamgate keeps one passive client-shell connection per -runtime for this; Herdr counts it as a connected client for `notification show`, -but it never takes focus or tab geometry. `status` (or older Herdr servers) instead -observes `working -> blocked` and `working -> done/idle`; startup snapshots are -silent. Herdr 0.9.4 treats Antigravity and Grok as idle once they return to the -foreground prompt, even while background tasks or subagents continue; Roamgate -preserves that policy. Delivery is best-effort: OS/Focus settings, outages, -expired subscriptions, or stopped runtimes can prevent it. Messages expire after -five minutes, with no durable replay; HTTP 404/410 removes expired subscriptions. - -Turning notifications off revokes on the server before browser unsubscribe. -If revocation fails, reconnect/retry or revoke OS/browser permission immediately; -already-accepted messages may arrive. Server-wide disable retains the registry -for reuse when re-enabled. Password changes do not revoke device subscriptions. - -**Active page only** is the fallback when push is unavailable; do not rely on it -while suspended/closed. Encrypted payloads contain notification text and routing -IDs, including Ranger's custom titles and messages, and may appear on lock +1. Use trusted HTTPS (or loopback HTTP). On iOS/iPadOS 16.4+, open the installed + Home Screen app. +2. Enable **Task notifications**, grant permission, and choose **Task needs + attention** and/or **Task completed**. **Background push** confirms the + device is enrolled. + +`roamgate push test` sends a test notification to every enrolled device. Push is +on by default; setting `ROAMGATE_WEB_PUSH_SUBJECT` to an empty value disables it +(otherwise it is an optional `mailto:` or HTTPS contact). After upgrading, +reload each browser/PWA once so the new notification worker is active. + +VAPID keys and subscriptions live in `~/.config/roamgate/web-push.json` +(`%APPDATA%\roamgate\web-push.json`). **Keep it private and backed up**; +replacing it requires every device to re-enroll. + +Delivery needs outbound HTTPS to the browser's push service (Apple, Google, +Mozilla, or Microsoft); no inbound port is needed. The bridge and Herdr must stay +running. By default (`--notification-source herdr`, Herdr 0.9+), Roamgate relays +Herdr's own notifications, matching its toasts and suppression rules. With +`status` (or older Herdr), Roamgate watches agents go from working to +blocked/done. Delivery is best-effort; messages expire after five minutes. + +Turning notifications off revokes the subscription on the server first. Changing +the password does not revoke devices. Notification text can appear on lock screens. ### Local notification testing -HTTP loopback origins such as `http://localhost` and `http://127.0.0.1` are -[secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts) -and can test browser notifications and Web Push without local TLS. With -`bun run dev:web`, the Vite origin on port 5173 proxies the bridge on port 8788. -For a bridge on another port, run `bun run build:web` and open that bridge's URL -directly. Use an isolated data directory and browser origin for test permissions, -subscriptions and Ranger tasks. - -Enable **Task notifications** in the test browser, then ask Ranger to propose -a monitoring task using **Let Ranger decide** and confirm it. **Run now** exercises -the task and its notification; click the alert to verify the exact run opens. -Repeat the same observed event to check deduplication. To isolate active-page -system delivery, first turn notifications off to revoke an existing subscription, -set `ROAMGATE_WEB_PUSH_SUBJECT=''`, restart the bridge and enable notifications. -To test closed-page delivery, restore the default subject, -restart and re-enroll until **Background push** appears, then close the test -origin's pages while leaving the bridge and Herdr running. Browser and OS -notification permissions and outbound access to the push provider still apply. - -The reproducible local-model integration test covers proposal, confirmation, -scheduled execution, custom text and deduplication across a bridge restart: +`http://localhost` and `http://127.0.0.1` are secure contexts, so notifications +work without TLS. Use an isolated data directory and browser profile. To test +end to end, enable **Task notifications**, ask Ranger for a monitoring task with +**Let Ranger decide**, confirm it, and use **Run now**. The automated +integration test covers scheduling and deduplication, but not real browser +delivery: ```sh bun test server/src/assistant-recovery.integration.test.ts --test-name-pattern 'confirmed Ranger proposals schedule custom notifications' ``` -It uses the real Pi driver and bridge with a scripted model transport and fixture -Herdr sockets. It does not establish browser or system notification delivery. - ## Logging -Logs use one line per event: timestamp, severity, scope, bounded key/value context. -`info` covers lifecycle/failures, not routine RPC/events/frames/successful auto-sync. -`roamgate service logs -f` follows a managed service's output. -Use `roamgate --log-level debug` or `ROAMGATE_LOG_LEVEL=debug` temporarily; -restart services after editing their environment. Debug can expose paths/IDs; -return to `info` afterwards. Logs omit URL auth tokens, which remain in protected files. +Logs have one line per event. `info` covers lifecycle events and failures. Use +`--log-level debug` (or `ROAMGATE_LOG_LEVEL=debug`) temporarily; debug logs can +include paths and IDs, but never URL tokens. `roamgate service logs -f` follows a +managed service. ### Performance profiling -Start a short foreground capture on an unused port, then reproduce the slow -interaction in that instance's browser page: +Run a short CPU capture on a spare port, then reproduce the slow interaction in +that instance: ```bash roamgate --host 127.0.0.1 --port 8788 --profile --profile-duration 30 --open ``` -Sampling begins during startup and stops automatically after the requested -interval; the server keeps running. Ctrl+C/SIGTERM attempts to save the partial -capture before normal shutdown. Force-kill/crash or a forced shutdown deadline -may lose it. The deadline is event-loop scheduled, so a blocked loop can delay -completion. Profiling adds CPU/memory overhead and exporting the report can -briefly pause the server; leave it off for ordinary use. No debugger port or -profiling HTTP endpoint is opened. - -Each run creates a private, uniquely named subdirectory under `--profile-dir` -(default `~/.config/roamgate/profiles`, or `%APPDATA%\\roamgate\\profiles` on Windows). -The log reports the output directory. Files are owner-only on POSIX; Windows -access follows directory ACLs. Keep diagnostic files private and remove them -when no longer needed; captures accumulate without automatic cleanup. - -- `summary.txt`: elapsed time, process CPU time, ending RSS, event-loop-delay - statistics, hottest sampled functions and JIT-tier/bytecode breakdown. -- `cpu.json`: Bun/JSC's raw `functions`, `bytecodes` and `stackTraces` result. - This is **not Chrome's `.cpuprofile` format**. It can contain source paths, - function names and source-map references; review it before sharing. - -Process CPU time sums all process threads and can exceed elapsed time. The -20 ms event-loop-delay probe includes scheduling delays; it cannot by itself -separate CPU contention from synchronous JavaScript. CPU samples also do not -measure browser drawing, network wait or keyboard-to-screen latency. Minified -standalone builds may have shortened function names; for source-level diagnosis, -run the matching checkout with `bun server/src/index.ts --profile` after building -web assets. To investigate browser stalls, separately record its Performance -panel while reproducing the same interaction. - -For a managed service, set `ROAMGATE_PROFILE=1` and optionally -`ROAMGATE_PROFILE_DURATION`/`ROAMGATE_PROFILE_DIR` in its environment file, then -restart when appropriate. The capture is once per process start, not an on-demand -capture of an already running process. Remove the settings afterwards. Do not -combine `--profile` with Bun's other CPU sampling profilers in the same process. +The capture stops automatically and writes a private directory under +`--profile-dir` containing `summary.txt` (CPU, memory, event-loop delay, hottest +functions) and `cpu.json` (raw Bun/JSC samples, **not** Chrome's `.cpuprofile` +format). Files may contain source paths; review before sharing and delete when +done. For readable function names, run from source with +`bun server/src/index.ts --profile`. For services, set `ROAMGATE_PROFILE=1` in +the environment file, restart, and remove it afterwards. ## Multiple and remote Herdr connections -The title's selector adds/tests/connects/disconnects/edits/removes shared profiles; -each browser selects independently. Local profiles attach to sockets, not start -Herdr. The first saved profile retains the default as writable `Local`. -`roamgate connections` lists, tests and edits the same profiles from a terminal. +The connection selector in the title bar adds, tests, connects, edits, and +removes profiles shared by all browsers; each browser picks its own. +`roamgate connections` does the same from a terminal. Disconnecting never stops +Herdr. ![Connection selector with local and SSH profiles](./screenshots/multi-connection-selector.png) -Use Enter/Space/Up/Down to open; arrows/Home/End navigate. There is no global -next/previous-connection shortcut. - -SSH requires an already-running remote Herdr and accepts an OpenSSH alias or -`user@host`. Leave socket paths empty to resolve remote home defaults. Put ports, -jump hosts, keys, and other options in `~/.ssh/config`; Roamgate stores no SSH -passwords/keys/passphrases/options. Verify host keys and noninteractive service-user -authentication first. The Roamgate bridge must run on Linux/macOS; a Windows -bridge supports native local profiles only. - -Moving files on Linux/macOS SSH hosts requires `python3` on the remote user's -PATH and a filesystem supporting atomic exclusive renames. Unsupported hosts -or cross-filesystem destinations fail without copying or deleting the source. - -For a **Windows SSH host**, select **Windows** as the remote host OS. Install -Herdr 0.9.1 or a later compatible release on that host, including the `conpty/` -directory from its Windows archive, and start the desired Herdr session first. -The remote SSH shell must be able to launch `powershell.exe`. Roamgate finds -`herdr.exe` on the SSH user's PATH, then tries -`%LOCALAPPDATA%\Programs\Herdr\bin\herdr.exe`; set an absolute executable path -for another installation. Paths with spaces and Unicode are supported. An empty -session selects `default`; set the session name to attach to a named server. -Remote socket fields are unavailable for Windows. Roamgate checks bridge support -before connecting and never installs or updates remote software on reconnect. -The Roamgate-managed local Herdr version does not determine the remote version. - -Herdr 0.9.4 restricts an elevated Windows server to administrator clients by -default. Run Herdr and its clients (including the SSH bridge) at matching -privilege levels; prefer an ordinary server when elevation is unnecessary. -Roamgate reports this access denial separately from SSH login failure and does -not retry it automatically. After correcting privileges, reconnect the profile. -To deliberately share an elevated server with ordinary clients of the same -account, Herdr supports `allow_unelevated_clients = true` under `[server]` in -its TOML configuration or `herdr --session server --allow-unelevated-clients`. -Stop and restart the selected server from an administrator terminal to apply -this change; stopping it closes its panes. This permits ordinary clients to -control the elevated server. Roamgate never enables it automatically. - -Windows SSH supports Herdr API operations and interactive terminals, including -PowerShell, resize and reconnect. Remote file, Git, worktree and agent-history -helpers that require a POSIX shell remain Unix-only. Failed remote operations -never fall back to Local. macOS ARM64 to Windows 11 ARM64 with Herdr's x64 package -is exercised by the opt-in end-to-end test; Linux to Windows and native Windows -x64 are not yet covered by that live test. See [validation](../CONTRIBUTING.md#validation). - -Profiles live atomically in `~/.config/roamgate/connections.json` or Windows -`%APPDATA%\roamgate\connections.json` (`ROAMGATE_CONNECTIONS_PATH` overrides). -Unix directory/file modes are `0700`/`0600`; registry/direct-parent symlinks are -rejected. Version 1 migrates on first successful mutation. Invalid files remain -intact with mutations disabled; repair before retry. Failed durable rollback -retires routing and blocks edits rather than letting memory/disk disagree. - -`auto_connect` controls startup, not browser selection. Disconnect/removal stops -only the runtime/tunnel, never Herdr/workspaces. SSH retries transient failures, -not auth/host-key/Herdr permission/permanent protocol errors. There is no idle -cleanup or aggregate resource budget; disconnect unused profiles. - -GUI preferences and worktree source relationships live in -`~/.config/roamgate/settings.json` (Windows: `%APPDATA%\roamgate\settings.json`). -Use `ROAMGATE_SETTINGS_PATH` to give another bridge its own settings file; -`HERDR_GUI_SETTINGS_PATH` remains a supported alias. Separate settings paths also -isolate session signing keys. Use stable absolute paths; changing the normalized -absolute path requires logging in again. - -Explicit CLI/environment connection settings create a read-only `legacy-default` -profile. Change those settings to edit it. Old browser preferences migrate once -into the first real profile without overwriting values. +**SSH profiles** need an already-running remote Herdr and an OpenSSH alias or +`user@host`. Put ports, keys, and jump hosts in `~/.ssh/config`; Roamgate stores +no SSH secrets. Verify host keys and make sure non-interactive login works. Leave +socket paths empty to use the remote defaults. SSH profiles need a Linux/macOS +bridge; a Windows bridge supports local profiles only. File moves need `python3` +on the remote host. + +**Windows SSH hosts:** choose **Windows** as the remote OS, install Herdr 0.9.1+ +(including its `conpty/` directory), and start the session first. The SSH shell +must be able to run `powershell.exe`. Roamgate looks for `herdr.exe` on PATH and +in `%LOCALAPPDATA%\Programs\Herdr\bin\`, or you can set its path and session +name. Terminals and Herdr operations work; file, Git, worktree, and agent-history +features need a Unix host. Run Herdr and the SSH user at the same privilege +level: an elevated Herdr 0.9.4 server rejects ordinary clients unless started +with `--allow-unelevated-clients`. + +Profiles are stored in `~/.config/roamgate/connections.json` +(`%APPDATA%\roamgate\connections.json`). `auto_connect` controls whether a +profile connects at startup. Transient SSH failures retry; authentication and +host-key failures do not. GUI preferences live in `settings.json` beside it. +Give each bridge on the same account its own `ROAMGATE_SETTINGS_PATH` (an +absolute, stable path; changing it requires logging in again). + +Connection flags or variables such as `--ssh-host` create a read-only +`legacy-default` profile: ```bash roamgate --ssh-host user@host ``` -CLI SSH forwards both sockets; file, image-paste, Git, and hooks run remotely. -Explicit socket flags/environment variables override automatic tunnel paths. -See [isolation](./ARCHITECTURE.md#connection-isolation) and -[SSH lifecycle](./ARCHITECTURE.md#ssh-transport). +Explicit socket flags/variables override the SSH tunnel paths. See +[isolation](./ARCHITECTURE.md#connection-isolation) and +[SSH transport](./ARCHITECTURE.md#ssh-transport). ## PWA instance names -Use **Configuration > Instance > Title suffix** to distinguish Roamgate servers -installed from different addresses. A suffix such as `Home` produces -`Roamgate · Home` in the page title and PWA installation metadata. The name is -shared by every viewer of that Roamgate instance, including across devices; it -is independent of the selected Herdr connection. Use **Reset to default** or save -an empty suffix to restore `Roamgate`. - -Suffixes accept up to 32 Unicode characters after whitespace normalization. -Keep them short so Android launchers can display the distinguishing part. -The `title_suffix` setting is saved in the server's GUI settings file and -survives restarts. When running multiple bridges under one OS account, use a -separate `ROAMGATE_SETTINGS_PATH` for each, as described in -[connection setup](#multiple-and-remote-herdr-connections). - -Set the suffix before installing the PWA when possible. Saving updates the open -page immediately; other open pages refresh the name when brought to the -foreground. Existing installed apps may keep their old launcher name until the -browser processes the manifest update and asks for confirmation. If it stays -unchanged, reinstall the PWA from that instance's address. This does not rename -an installed Android app instantly. See -[Chrome's Android app update guidance](https://support.google.com/chrome/answer/9658361?co=GENIE.Platform%3DAndroid&hl=en). +**Configuration > Instance > Title suffix** turns `Roamgate` into, for example, +`Roamgate · Home` in the page title and PWA name, so several servers are easy to +tell apart. The suffix (up to 32 characters) is saved on the server and shared by +all viewers. Set it before installing the PWA; already installed apps (especially +on Android) may keep the old name until the browser updates the manifest or you +reinstall. ## Worktree hooks @@ -854,149 +448,87 @@ Configure hooks in `roamgate.json` at the repository root: } ``` -| Hook | Timing / working directory | +| Hook | When / working directory | | --- | --- | | `setup` | After create/open; new worktree | -| `opened` | After opening an existing worktree; opened worktree | +| `opened` | After opening an existing worktree; that worktree | | `teardown` | Before removal; target worktree | | `removed` | After removal; source checkout | -Roamgate selects the first existing file in this order: - -1. Target worktree's `roamgate.json` -2. Source checkout's `roamgate.json` -3. Target worktree's `paseo.json` (legacy compatibility) -4. Source checkout's `paseo.json` (legacy compatibility) - -One complete configuration is selected; files are never merged and hooks never -run twice. Missing or blank hook commands are skipped. Valid empty objects (`{}` -or `{"worktree": {}}`) suppress fallback. Empty files, invalid JSON, non-object -configuration or `worktree` values, non-string known hook fields, and read -failures report an error with the selected path, without trying another file. -Unknown fields are ignored. - -After removal, the target is gone and resolution uses the source checkout; -the `removed` configuration and its scripts must be available there. -Configuration reads and commands run on the connected local or SSH host. -Commands use the existing `sh -c` runner and the working directories above. - -To migrate, rename `paseo.json` to `roamgate.json`, preserving its `worktree` -object. Review both checkouts: even a source native file overrides a target -legacy file. No automatic migration is performed. **Worktree Hooks** and -**Worktree Lifecycle** show the effective path, errors, and legacy compatibility. -The settings RPC exposes `config_path` and `config_source` (`roamgate`, `paseo`, -or `null`); the compatibility field `paseo_path` is set only for a Paseo file. - -Prefer the `ROAMGATE_HOOK_*` variables in new scripts: +Roamgate uses the first file found, in order: target `roamgate.json`, source +`roamgate.json`, target `paseo.json`, source `paseo.json` (legacy). Files are +never merged. `{}` disables fallback; invalid files report an error. Hooks run +with `sh -c` on the connected (local or SSH) host. To migrate, rename +`paseo.json` to `roamgate.json`. | Variable | Value | | --- | --- | | `ROAMGATE_HOOK_EVENT` | `worktree.created`, `worktree.opened`, `worktree.before_remove`, or `worktree.removed` | -| `ROAMGATE_HOOK_CHECKOUT_PATH` | Target path, including former path after removal | -| `ROAMGATE_HOOK_SOURCE_CHECKOUT_PATH` | Source checkout when known | - -`HERDR_GUI_HOOK_*` remains an alias for each corresponding Roamgate variable. -`PASEO_HOOK` contains `setup`, `opened`, `teardown`, or `removed`; -`PASEO_CHECKOUT_PATH` and `PASEO_SOURCE_CHECKOUT_PATH` retain the same paths. -All aliases are available for both native and legacy configuration. -Notices show bounded diagnostics including the actual configuration path. -**Failed teardown stops removal; other failures do not roll back completed actions.** -Hooks default on; inspect/disable per repository under **Worktree hooks** or -**Worktree Lifecycle**. Disabling applies to both formats and skips execution -even when configuration is invalid. Hooks are trusted, unsandboxed repository -code executed with the execution user's permissions. A native filename does -not establish trust: review configuration and scripts before worktree operations. +| `ROAMGATE_HOOK_CHECKOUT_PATH` | Target worktree path | +| `ROAMGATE_HOOK_SOURCE_CHECKOUT_PATH` | Source checkout, when known | + +`HERDR_GUI_HOOK_*` and `PASEO_HOOK` / `PASEO_CHECKOUT_PATH` / +`PASEO_SOURCE_CHECKOUT_PATH` are also set. + +**Hooks are on by default and run unsandboxed repository code with your +permissions.** Review them, or disable them per repository under **Worktree +hooks** or **Worktree Lifecycle**. A failed teardown stops removal; other +failures do not undo completed steps. ## Run as a user service | Command | Behavior | | --- | --- | -| `roamgate service install` | Create/update definition and start | -| `roamgate service install --force` | Replace a non-Roamgate definition (not a legacy migration bypass) | -| `roamgate service status` | Native manager status | -| `roamgate service restart` | Restart after environment changes | -| `roamgate service reload` | Reload definition, then restart | -| `roamgate service uninstall` | Stop/remove service; retain configuration/tokens | -| `roamgate service logs [-f]` | Show recent service output | - -| Platform | Definition / behavior | +| `roamgate service install` | Create or update the service and start it | +| `roamgate service install --force` | Replace a non-Roamgate definition | +| `roamgate service status` | Show status | +| `roamgate service restart` | Restart after config changes | +| `roamgate service reload` | Reload the definition, then restart | +| `roamgate service uninstall` | Stop and remove; keeps config and tokens | +| `roamgate service logs [-f]` | Show recent output | + +| Platform | Definition | | --- | --- | -| Linux | `~/.config/systemd/user/roamgate.service`, `Restart=always` | -| macOS | `~/Library/LaunchAgents/dev.roamgate.plist`, label `dev.roamgate`, `KeepAlive`; logs `~/Library/Logs/roamgate.stdout.log` / `roamgate.stderr.log` | -| Windows | Task `dev.roamgate-` (config-path hash), `%APPDATA%\roamgate\roamgate-task.ps1`; login start, normal privileges, restart on failure | - -Stop/remove legacy services first via the [migration procedure](#transition-from-herdr-studio--herdr-gui). -**New services bind `0.0.0.0:8787`**, generate a persistent token, and print -localhost/LAN token URLs. Config lives in `~/.config/roamgate/roamgate.env` or -`%APPDATA%\roamgate\roamgate.env`, preserved on reinstall/uninstall. Edit HOST, -PORT, password, and Herdr settings there, then restart, or use -`roamgate config set NAME=value --restart`. For local-only installation, -set `HOST=127.0.0.1` first. On Windows, allow Private networks only if prompted; -on Linux, `sudo loginctl enable-linger "$USER"` keeps services after logout. -On macOS, installation checks the configured listener before registering the -launchd job. If another process owns the port, it reports the conflict and leaves -the preserved config available for choosing a different port. - -macOS service installation validates effective config values without executing -the file. Use one `NAME=value` assignment per line, optionally prefixed by -`export`, with shell single/double quotes, backslash escapes, and comments. -`$NAME` and `${NAME}` expand variables defined earlier in the file; `HOME` is -available as the service user's home directory. For example: - -```sh -SECRET='replace-with-a-strong-password' -ROAMGATE_PASSWORD="$SECRET" -``` - -The expanded password must contain 8 to 1024 Unicode characters. An explicitly -empty `ROAMGATE_PASSWORD` selects generated-token authentication, even when the -legacy password variable is set. Define other referenced variables in the config: -the installing terminal's environment is not the launchd environment. References -to externally defined variables, shell-special assignments, command substitutions, -standalone commands, and other unsupported shell expressions are rejected before -config migration or service changes. Keep the config self-contained; resolve -dynamic values yourself before installing or reinstalling. +| Linux | `~/.config/systemd/user/roamgate.service` (`Restart=always`) | +| macOS | `~/Library/LaunchAgents/dev.roamgate.plist` (`KeepAlive`); logs in `~/Library/Logs/roamgate.*.log` | +| Windows | Scheduled task `dev.roamgate-` running `%APPDATA%\roamgate\roamgate-task.ps1` at login | + +**New services listen on `0.0.0.0:8787`** with a generated token and print +localhost/LAN login URLs. Settings live in `~/.config/roamgate/roamgate.env` +(`%APPDATA%\roamgate\roamgate.env`); edit it and restart, or run +`roamgate config set NAME=value --restart`. For local-only access, set +`HOST=127.0.0.1`. On Linux, `sudo loginctl enable-linger "$USER"` keeps the +service running after logout. Remove legacy services first +([migration](#transition-from-herdr-studio--herdr-gui)). + +On macOS the env file is parsed, not executed: use `NAME=value` lines (optionally +prefixed with `export`) with quotes, comments, and `$NAME`/`${NAME}` references +to `HOME` or variables defined earlier in the file. Command substitution and +other outside variables are rejected. ```bash curl -fsS http://127.0.0.1:8787/healthz ``` -Tokens live in `~/.config/roamgate/auth-token` or `%APPDATA%\roamgate\auth-token`. -Private random cookie signing state lives in the same data directory. With no or -an empty settings-path override, its file is `session-secret.json`. A nonempty -`ROAMGATE_SETTINGS_PATH` (or `HERDR_GUI_SETTINGS_PATH`) selects -`session-secret-.json`, where `` is the full SHA-256 digest of the -normalized absolute settings path. The key stays in the data directory even when -the settings file is elsewhere. Different settings paths isolate signing keys; -keep custom paths absolute and stable. Changing the normalized path requires -logging in again. Preserve the signing state across restarts and protect it like -the login token. - -`ROAMGATE_PASSWORD` and the generated token are login credentials only. The first -upgrade from credential-signed cookies requires logging in again. Changing an -effective credential rotates that instance's signing secret at startup, so old -sessions stay revoked even if that credential is later restored. Stop all -listeners for the same instance before credential changes. To revoke its sessions -without changing credentials, stop those listeners, remove only its signing -state file, then restart. Malformed or unsafe signing state fails startup rather -than silently resetting authentication. - -Concurrent startups use a short-lived `.lock` directory, -such as `session-secret.json.lock` or `session-secret-.json.lock`. If -startup was killed while holding it, stop all listeners for that instance, -confirm no startup is running, and remove only its matching lock directory before -restarting. A busy lock fails closed after a bounded wait; it is never stolen -from a potentially live writer. - -A `?token=...` visit sets an HttpOnly cookie and removes the URL token. To rotate, -run `roamgate auth rotate`, which also restarts a running managed service and -prints the new URLs. Manually, stop the service, replace the file with a fresh -64-character lowercase hexadecimal secret (mode `0600`), then restart. -`roamgate auth revoke-sessions` removes the signing state and restarts the -service to sign out every browser without changing credentials. **Deleting only the new file can restore a -readable legacy token**, not rotate it. - -Manual templates live under `deploy/`. Keep systemd as restart owner for wrappers: +**Credentials and sessions.** The generated token is in +`~/.config/roamgate/auth-token` (`%APPDATA%\roamgate\auth-token`). A `?token=` +URL logs in and sets a cookie. Cookies are signed by a separate secret in the +same directory (`session-secret.json`, or `session-secret-.json` when +`ROAMGATE_SETTINGS_PATH` is set). Protect both like passwords. + +- `roamgate auth rotate` replaces the token, signs out every browser, and + restarts the service. **Do not just delete `auth-token`**: after a migration, + the legacy token is copied back on the next start. To rotate by hand, stop + the service and write a fresh 64-character lowercase hex secret (mode `0600`). +- `roamgate auth revoke-sessions` signs out every browser without changing + credentials. +- Changing the password or PIN also signs out every browser on the next start. + Stop all listeners of the instance before changing credentials. +- If startup was killed and a stale `session-secret*.json.lock` directory + remains, stop all listeners and delete only that lock directory. + +Manual templates live in `deploy/`. When wrapping the binary, keep systemd as +the restart owner: ```ini [Service] @@ -1004,29 +536,26 @@ ExecStart= ExecStart=/absolute/path/service-wrapper -- %h/.local/bin/roamgate --host 0.0.0.0 ``` -The updater saves `roamgate.previous`, atomically installs a verified binary, and -exits; it never starts its replacement. Reinstall preserves custom `ExecStart` -in a managed unit when it still invokes the same binary. +The updater installs a verified binary, keeps `roamgate.previous`, and exits so +the supervisor restarts it. ## Command-line management -Management commands act on the [user service](#run-as-a-user-service)'s -environment file and data directory, not on the shell's flags or variables. -`roamgate --help` lists them; `roamgate --help` describes each. -They exit 0 on success, 1 on failure and 2 on invalid arguments. Commands with -`--json` print machine-readable output for scripts. +These commands act on the [user service](#run-as-a-user-service)'s config and +data, not on the current shell's flags. They exit 0 on success, 1 on failure, +and 2 on bad arguments; many accept `--json`. | Command | Purpose | | --- | --- | -| `roamgate doctor [--offline]` | Check the config, credentials, TLS certificate, service, listener, network exposure, local Herdr, connections, Web Push and updates; print a fix for each problem. Exits 1 on any failure. The output contains no secrets. | -| `roamgate url [--qr] [--address ]` | Print tokenized login URLs; `--qr` draws a terminal QR code for a phone, and `--address` substitutes a VPN or Tailscale name. The code contains the token. | -| `roamgate config show\|get\|set\|unset\|edit\|path` | Read or change `roamgate.env`. Changes are validated as a whole and written atomically, preserving comments; `--restart` restarts a running service. `KEY=-` reads a secret from standard input. | -| `roamgate auth show\|rotate\|revoke-sessions` | Show the login method, replace the generated token (which also signs out every browser), or sign out every browser without changing credentials; both restart a running service unless `--no-restart`. | -| `roamgate connections list\|test\|add\|remove\|set-default` | Manage the profiles the [connection selector](#multiple-and-remote-herdr-connections) edits. A running Roamgate keeps its own copy, so edits are refused while it serves unless `--restart` restarts the managed service. Only the service config's address is checked; stop other processes sharing the registry, such as a foreground run on another port, first. | -| `roamgate push test` | Send a test notification to every enrolled [Web Push](#web-push-notifications) device and report each provider's answer. | -| `roamgate update [--check]` / `roamgate rollback` | Install the latest release with the same verification as the in-app updater, or swap back to `roamgate.previous`; a running service using this executable restarts. Linux and macOS only. | -| `roamgate service logs [-f] [-n ]` | Show service output: the user journal on Linux, `~/Library/Logs` on macOS. Windows tasks do not capture output. | -| `roamgate completion bash\|zsh\|fish` | Print a shell completion script; its help shows where to install it. | +| `roamgate doctor [--offline]` | Check config, credentials, TLS, service, network exposure, Herdr, connections, Web Push, and updates, with a fix for each problem. Prints no secrets. | +| `roamgate url [--qr] [--address ]` | Print login URLs; `--qr` shows a QR code for a phone (it contains the token); `--address` substitutes a VPN or Tailscale name. | +| `roamgate config show\|get\|set\|unset\|edit\|path` | Read or change `roamgate.env` atomically. `--restart` restarts the service; `KEY=-` reads a secret from stdin. | +| `roamgate auth show\|rotate\|revoke-sessions` | Show the login method, replace the token, or sign out every browser. | +| `roamgate connections list\|test\|add\|remove\|set-default` | Manage [connection profiles](#multiple-and-remote-herdr-connections). Edits are refused while the managed service runs unless `--restart` is given. Only the service config's address is checked, so stop other processes sharing the registry (such as a foreground run on another port) first. | +| `roamgate push test` | Send a test [Web Push](#web-push-notifications) notification. | +| `roamgate update [--check]` / `roamgate rollback` | Update to the latest release or restore `roamgate.previous` (Linux/macOS). | +| `roamgate service logs [-f] [-n ]` | Show service output (not available for Windows tasks). | +| `roamgate completion bash\|zsh\|fish` | Print a shell completion script. | ## Build a standalone executable @@ -1035,47 +564,32 @@ bun scripts/studio-plugin.ts build-source # Output: server/roamgate (server/roamgate.exe on Windows) ``` -This installs dependencies and embeds frontend/Bun. Afterward, `bun run build` -rebuilds; targets need no Bun. Cross-build/package with `bun run build:` -or `bun run package:`: +Afterwards `bun run build` rebuilds. Cross-compile with `bun run build:` +or package with `bun run package:`; `bun run build:all` builds every +target: | Targets | Architectures | | --- | --- | -| `linux-x64`, `linux-arm64` | Linux x86-64 / ARM64 | +| `linux-x64`, `linux-arm64` | Linux x86-64 / ARM64 (glibc; musl is unsupported) | | `darwin-x64`, `darwin-arm64` | macOS Intel / Apple Silicon | | `windows-x64`, `windows-arm64` | Windows x86-64 / ARM64 | -`bun run build:all` builds all targets. Bun downloads runtimes automatically. -Use glibc Linux x64 for Ubuntu/Debian/Fedora/CentOS; musl is unsupported there -because Bun's musl binary still dynamically links `libstdc++`/`libgcc_s`. -Run `./server/roamgate`; `bun run clean` removes generated builds. -Release packaging/publishing follows [AGENTS.md](../AGENTS.md#release-notes). - -Standalone builds omit source maps to keep release downloads smaller. Errors -report positions in the compiled code rather than the original TypeScript. -The Release workflow retains a separate `debug-roamgate--` CI -artifact for 30 days, containing a tar archive of the same version built with -embedded source maps and its `.map` file. Extract the tar before running the -debug executable; this preserves its executable permissions. These debug -artifacts are not published to GitHub Releases or used by automatic updates. -Use the matching debug executable to -reproduce errors with source-level stack traces. - -To build a debug executable locally, first run `bun run build:web`, then -`bun run --cwd server compile --sourcemap`, or use -`compile: --sourcemap` for another platform. This replaces the local -executable with a mapped build; run the normal compile command again before -packaging a production executable. Merely placing a `.map` file beside a -map-free executable does not restore source-level stack traces. +`bun run clean` removes builds. Releases follow +[AGENTS.md](../AGENTS.md#release-notes). + +Release binaries have no source maps. Each release run keeps a +`debug-roamgate--` CI artifact for 30 days with source maps; extract +its tar and run it to get readable stack traces. To build one locally, run +`bun run build:web` and `bun run --cwd server compile --sourcemap`. ## Troubleshooting | Symptom | Action | | --- | --- | -| Not sure what is wrong | Run `roamgate doctor`; it checks each part below and prints a fix. | -| Cannot connect to Herdr | Check server and both socket paths; default local setup can use `roamgate herdr setup`. Override with `--socket-path /path/to/herdr.sock` only deliberately. | -| Another device cannot open the page | Check bind, token URL, network/firewall, and [private access setup](./TUTORIAL.md#networking). | -| SSH connects locally | Remove explicit socket flags/`HERDR_SOCKET_PATH`/`HERDR_CLIENT_SOCKET_PATH`; they override tunnel paths. | -| Want automatic browser launch | Use `roamgate --open` or `OPEN_BROWSER=1`. | +| Not sure what is wrong | Run `roamgate doctor`. | +| Cannot connect to Herdr | Check that Herdr runs and both sockets exist; try `roamgate herdr setup`. | +| Another device cannot open the page | Check the bind address, token URL, firewall, and [private access setup](./TUTORIAL.md#networking). | +| SSH profile connects locally | Remove socket flags and `HERDR_SOCKET_PATH`/`HERDR_CLIENT_SOCKET_PATH`; they override tunnels. | +| Want the browser to open automatically | Use `roamgate --open` or `OPEN_BROWSER=1`. | For step-by-step diagnosis, see [the tutorial](./TUTORIAL.md#troubleshooting). diff --git a/docs/HISTORY.md b/docs/HISTORY.md index d5e42eeb..8e6d0824 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,72 +1,56 @@ # History synchronization -`createAgentSessionHandlers` shares a per-connection projection cache across +`createAgentSessionHandlers` shares one projection cache per connection across History, summary, transcript preview, and ATIF export. Concurrent requests share -in-flight reads/projection; raw preview prefix reads/downloads stay separate. +reads; raw preview and download reads stay separate. ## Cache and consistency -The cache retains at most 16 sessions, four recent History revisions per session, -and an estimated 32 MiB of values. Admission counts UTF-16 strings and structural -overhead without serialization, conservatively recounting shared values. -Oversized/deep projections are served but not retained; transient parsing memory -is not included in the estimate. +The cache keeps at most 16 sessions, four recent revisions per session, and about +32 MiB of data (an estimate, not a heap bound). Oversized projections are served +but not cached. -Refresh resolves/stats the session. Path, provider, session, file identity, -size/mtime, change metadata, and provider descriptors invalidate projections. -A post-read stat triggers up to three retries on change. Errors invalidate data; -incomplete JSONL records wait for later changes. This is not a filesystem -transaction: later writes appear on the next refresh, and coarse metadata may -miss same-size in-place rewrites. +Each refresh stats the session file. Changes to path, provider, session, file +identity, size, or mtime invalidate the projection; a change during the read +triggers up to three retries. Incomplete JSONL lines wait for the next change. +This is not a filesystem transaction: same-size in-place rewrites may be missed +until the next change. ## Version 2 protocol -Send `history_version: 2` to `agent_history.get`, optionally with the last accepted -`cursor: { epoch, revision }`. Legacy callers receive conversation-only `messages`. +Send `history_version: 2` to `agent_history.get`, optionally with the last +accepted `cursor: { epoch, revision }`. Legacy callers get conversation-only +`messages`. | Mode | Response | | --- | --- | -| `snapshot` | `entries` replaces the window. Missing files/sessions yield empty snapshots. Invalid/expired cursors, connection replacement, eviction, file replacement, or truncation reset this way. | -| `delta` | `base_revision`, `upserts`, `removed`, plus complete ID `order` only when membership/order changes. No `messages`, `entries`, or trajectory. No-change replies keep revision with empty changes. | +| `snapshot` | `entries` replaces the window. Returned for a missing session, an invalid or expired cursor, a replaced connection, eviction, or file replacement/truncation. | +| `delta` | `base_revision`, `upserts`, `removed`, and the full ID `order` only when membership or order changed. An unchanged reply keeps the revision with empty changes. | -The window holds the latest **200 conversation entries**, including errors; -associated tools do not count toward the limit. Eviction emits removals; raw/ATIF -exports remain complete. IDs identify projected occurrences/tool calls, **not -durable source records or ATIF step numbers**. Full-window comparisons handle -provider representation changes; synthetic timestamps remain stable within a -cached file generation. Clients accept replies only against the request cursor -and current pane/connection lease. +The window holds the latest **200 conversation entries** (including errors); +associated tool calls do not count. Raw and ATIF exports stay complete. Entry IDs +identify projected entries, **not durable source records or ATIF step numbers**. +Clients accept a reply only against their request cursor and current +pane/connection lease. -History refreshes every four seconds only while open and visible, without -overlapping requests. Cards cap default DOM content at 4,000 characters; full -tool details open as escaped text. Names/call IDs associate results without -moving them from their transcript positions. +History refreshes every four seconds while open and visible, never overlapping. +Cards show up to 4,000 characters; full tool details open on demand. ## Message filters -The Session header offers counts, details, transcript preview, raw export, and -refresh. Info toggles metadata/messages in wide and compact layouts, preserving -filters/selection; session changes return to messages. +User and Agent start enabled; Tool starts disabled. Agent includes assistant +errors; Tool includes calls, outputs, and errors. Counts reflect the unfiltered +window, and the header shows visible/total when filtered. Filters persist while +the panel is mounted, not across reloads. **Show all types** restores them. -User/Agent start enabled; Tool starts disabled. Agent includes assistant errors; -Tool includes calls, outputs, and errors. Button counts reflect the unfiltered -window; the header shows visible/total when filtered. Minimap/numbering follow -visible entries, but hidden entries still update and exports remain complete. -Type filters survive pane switches/reopening while mounted, not reloads; -**Show all types** restores all message types. - -Search combines type filters with case-insensitive literal matching across full -loaded text, not just card previews. Redacted tools become searchable only after -explicit loading; search never fetches older messages or tool payloads. **Reset -filters** clears search and enables all types. Search resets on pane/connection changes. - -Click a card's header/text/background for details; Copy stays separate. Timestamps -use local `MM-DD HH:mm`, prefixed with `YYYY-` outside the current year. +Search is a case-insensitive literal match over all loaded text and combines +with the type filters. It never fetches older messages or hidden tool payloads. +**Reset filters** clears search and type filters. Timestamps use local +`MM-DD HH:mm`, with the year outside the current year. ![History filtered to tool calls and outputs using synthetic data](screenshots/history-tool-filter.png) -## Consistency and resource limits +## Resource limits -Changed files still require **full JSONL reads and provider projection**: no -incremental parser, byte cursor, database, or new transport. Raw previews/full -ATIF exports can remain large; cache limits are not exact JavaScript heap bounds. +Changed files are re-read and re-projected in full: there is no incremental +parser or database. Raw previews and ATIF exports can be large. diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md index fa676027..60816c28 100644 --- a/docs/SCREENSHOTS.md +++ b/docs/SCREENSHOTS.md @@ -1,32 +1,39 @@ # Reproducible Product Screenshots -Capture the current Roamgate UI against a real Herdr backend using the public -[Northstar demo](https://github.com/powerfooI/northstar-demo), a fictional six-task -board maintained in its own repository. Its names, code, tasks, and local SVG are -public demo content. Use one coherent task across desktop and mobile: add a -status filter, explain empty results, test it, and review the change. +Capture the real Roamgate UI against a real Herdr backend using the public +[Northstar demo](https://github.com/powerfooI/northstar-demo), a fictional task +board. Every shot tells one story: add a status filter, test it, and review it. + +## Isolate the Capture Environment + +Use a dedicated OS account (for example `demo`) or a clean VM with only Bun, +Git, Herdr, Roamgate, and the public repositories. Keep personal projects, +browser profiles, shell/SSH config, and agent transcripts out of it, and use a +neutral hostname and a fresh browser profile. The OS account is the privacy +boundary: Roamgate reads agent history and settings from the user's home +directory, so a separate Herdr session alone is not enough. + +Only include the PR status card if `gh` is already authenticated in that +environment and PR #1 is open with passing checks; do not copy private +credentials just for the card. Set an absolute `XDG_STATE_HOME` outside every +demo checkout. ## Prepare the Case -Install the root dependencies as described in [Contributing](../CONTRIBUTING.md). -Create a fresh independent clone outside the Roamgate checkout: +Install root dependencies ([Contributing](../CONTRIBUTING.md)), then clone the +demo into a new, empty directory: ```sh mkdir -p /tmp/roamgate-demo git clone --branch main https://github.com/powerfooI/northstar-demo.git /tmp/roamgate-demo/northstar ``` -The public repository is the canonical source; Roamgate does not keep another -fixture or patch. The project needs Bun and Git, with no extra packages. The -`main` branch contains the baseline; `codex/status-filter` contains the example -implementation in [PR #1](https://github.com/powerfooI/northstar-demo/pull/1). - | Scene | Initial state | Use | | --- | --- | --- | -| Baseline | Clean `main` clone | Give a real agent the task in the project README. | -| Review | Baseline plus the five example implementation files | Repeatable Files, Working tree diff, preview, annotation, and linked PR shots. | +| Baseline | Clean `main` clone | Give a real agent the task from the README. | +| Review | `main` plus the example implementation as unstaged changes | Files, diff, preview, annotation, and PR shots. | -For the review scene, run the following only in the clone just created above: +For the review scene, run this **only in the fresh clone**: ```sh git -C /tmp/roamgate-demo/northstar switch --track -c codex/status-filter origin/codex/status-filter @@ -34,16 +41,12 @@ git -C /tmp/roamgate-demo/northstar reset --mixed origin/main git -C /tmp/roamgate-demo/northstar diff origin/main --stat ``` -This leaves the feature files as real unstaged changes against `main` while -preserving the branch's upstream link to PR #1. The diff changes `app.ts`, -`index.html`, `styles.css`, `tasks.test.ts`, and `tasks.ts`. The PR is a prepared -example implementation; do not attribute it to an idle agent or a screenshot -session. Agent history and completed-task status require a matching real run. -Record both remote commit SHAs with the capture originals so later branch -updates do not obscure which version was photographed. +The diff touches `app.ts`, `index.html`, `styles.css`, `tasks.test.ts`, and +`tasks.ts` from [PR #1](https://github.com/powerfooI/northstar-demo/pull/1). +That PR is a prepared example; never present it as an agent's work. Record both +remote commit SHAs with the originals. -For desktop sidebar shots, create three additional independent clones of the -same public repository: +For sidebar shots, add three more clones: ```sh git clone --branch main https://github.com/powerfooI/northstar-demo.git /tmp/roamgate-demo/documentation @@ -51,114 +54,44 @@ git clone --branch main https://github.com/powerfooI/northstar-demo.git /tmp/roa git clone --branch main https://github.com/powerfooI/northstar-demo.git /tmp/roamgate-demo/ui-preview ``` -Run the board and tests from the Northstar clone: - -```sh -cd /tmp/roamgate-demo/northstar -bun test -bun run dev -``` - -Open . `bun run dev 4318` selects another port. On the -review scene, choose **Done** and search for `navigation` to reach the empty -state; **Clear filters** restores all six tasks and focuses the search input. -Roamgate's static HTML preview shows all six cards with local styles/images; -its script restrictions mean interactive filtering belongs in the live board. - -Use an unused capture destination; Git refuses a non-empty directory. To repeat -a scene, stop its processes and close its Herdr workspace, preserve the old -clone, and choose a new destination. Do not run the reset step on an existing -user checkout. Update the workspace commands and paths below to match the new -capture directories. - -## Isolate the Capture Environment - -Use a dedicated OS account named `demo` or a clean VM for published screenshots. -Install Bun, Git, Herdr, and Roamgate there; copy or clone only the public checkout -and the independent Northstar demo repositories. Keep personal projects, home -directories, browser profiles, shell configuration, SSH configuration, and agent -transcripts outside it. Configure only the credentials needed for a real agent, -without printing them in commands or terminal output. Choose a neutral hostname -and a fresh browser profile. - -The linked PR card is optional. Include it only when GitHub CLI authentication is -configured on the checkout host; otherwise omit the PR-status scene. Do not copy -private GitHub credentials solely to add the card. Keep authentication details -outside the repository and screenshots. If showing the card, verify that PR #1 -is open and its Bun test check succeeded; **Provider not configured**, missing -CLI, or login-required states are not ready for a PR-status shot. -When running with a cleaned environment, set an absolute `XDG_STATE_HOME` -outside every demo checkout so CLI runtime state cannot become a review file. - -A separate project, named Herdr session, and clean shell environment protect -against accidental reuse. They do not prevent the same OS user from accessing -personal files. Roamgate resolves some agent histories through that user's home -directory and stores GUI settings there; changing `CODEX_HOME` alone does not -isolate those lookups. The OS account or VM is the privacy boundary. +`bun test` and `bun run dev` in the Northstar clone run the tests and the board +at . To repeat a scene, use a new destination directory +instead of resetting an existing checkout. ## Start the Real Workspace -The following macOS/Linux commands run from the Roamgate checkout in the capture -account. `demo:herdr` runs the installed Herdr CLI with an allowlisted environment, -a `/bin/sh` non-login shell, neutral prompt, and a dedicated `roamgate-demo` -session under `/tmp/roamgate-demo/config`. Set `ROAMGATE_DEMO_ROOT` to an absolute -path to change that root; all invocations must use the same value. +`bun run demo:herdr` runs Herdr with a clean environment and a dedicated +`roamgate-demo` session under `/tmp/roamgate-demo/config` (override with an +absolute `ROAMGATE_DEMO_ROOT`, the same in every terminal). -In the first terminal: +Start the server in one terminal: ```sh bun run demo:herdr server ``` -In a second terminal: +In a second terminal, create the workspaces. Note the IDs each command returns: ```sh bun run demo:herdr workspace create --cwd /tmp/roamgate-demo/documentation --label Documentation -``` - -Record Documentation's returned `.result.root_pane.pane_id` as -`demo_docs_pane_id`. Create Release QA: - -```sh +# demo_docs_pane_id = .result.root_pane.pane_id bun run demo:herdr workspace create --cwd /tmp/roamgate-demo/release-qa --label "Release QA" -``` - -Record its `.result.workspace.workspace_id` as `demo_qa_workspace_id`, then add -its Tests tab: - -```sh +# demo_qa_workspace_id = .result.workspace.workspace_id bun run demo:herdr tab create --workspace "$demo_qa_workspace_id" --cwd /tmp/roamgate-demo/release-qa --label Tests --no-focus -``` - -Record that tab's `.result.root_pane.pane_id` as `demo_qa_pane_id`. Create UI -Preview, then focus Northstar: - -```sh +# demo_qa_pane_id = .result.root_pane.pane_id bun run demo:herdr workspace create --cwd /tmp/roamgate-demo/ui-preview --label "UI Preview" bun run demo:herdr workspace create --cwd /tmp/roamgate-demo/northstar --label Northstar --focus -``` - -Record Northstar's returned `.result.tab.tab_id` and `.result.root_pane.pane_id` -as `demo_tab_id` and `demo_pane_id` in this terminal. Rename and split the real -panes: - -```sh +# demo_tab_id = .result.tab.tab_id, demo_pane_id = .result.root_pane.pane_id bun run demo:herdr tab rename "$demo_tab_id" Implementation bun run demo:herdr pane rename "$demo_pane_id" Implementation bun run demo:herdr pane split --pane "$demo_pane_id" --direction down --ratio 0.65 --cwd /tmp/roamgate-demo/northstar --no-focus -``` - -Record the split's `.result.pane.pane_id` as `demo_checks_id`, then: - -```sh +# demo_checks_id = .result.pane.pane_id bun run demo:herdr pane rename "$demo_checks_id" Checks bun run demo:herdr pane run "$demo_checks_id" "bun test" ``` -For the desktop overview, install Pi normally in the capture account and use its -official Herdr integration. Keep Pi's standard package entry point so Herdr can -identify the real process. Use a dedicated writable profile for the integration -and all three interactive sessions: +For the desktop overview, install Pi in the capture account with its Herdr +integration and run idle Pi sessions (no model calls) in three panes: ```sh XDG_CONFIG_HOME=/tmp/roamgate-demo/config \ @@ -169,31 +102,13 @@ bun run demo:herdr pane run "$demo_docs_pane_id" "PI_CODING_AGENT_DIR=/tmp/roamg bun run demo:herdr pane run "$demo_qa_pane_id" "PI_CODING_AGENT_DIR=/tmp/roamgate-demo/pi-agent PI_OFFLINE=1 PI_TELEMETRY=0 pi" ``` -The native install command preserves `PI_CODING_AGENT_DIR`, which the -`demo:herdr` wrapper's allowlist does not forward. Keep the Pi processes running, -with the genuine Pi TUI in Implementation and real test output in Checks. Wait -for Herdr to report `agent=pi` and session source `herdr:pi`; these idle sessions -are waiting for input without submitting a model prompt or calling a model. -They do not claim to have produced the review patch, completed a task, or created -task history. Done/history shots require a matching real completed turn as -described below. Show the sidebar in **Nested** mode and expand the demo workspaces. - -In the current Implementation pane, enter `/new`, then -`!git --no-pager diff origin/main --stat`, pressing Enter after each. Pi remains -active: `/new` starts a clean session, and its Bash mode runs the genuine Git -command without a model request. The review clone shows the five example implementation -files. Keep this Pi TUI and command output above Checks, whose real `bun test` -run reports four passing tests and no failures. This output describes the public -review implementation; it does not attribute the implementation to Pi. - -For an optional shell overview, show the Status filter demo section and `git --no-pager diff -origin/main --stat` in Implementation after exiting Pi. For a narrow mobile -terminal, run `git status --short && bun test --dots` in Checks; the dots reporter -keeps the genuine test summary compact. Run commands again after switching viewport sizes -so terminal output uses the current geometry. Keep only one capture client -attached while doing this. - -Build Roamgate's web assets and start its actual bridge in another terminal: +Wait until Herdr reports `agent=pi`. In the Implementation pane, enter `/new`, +then `!git --no-pager diff origin/main --stat` to show the real diff summary +above the passing tests in Checks. For mobile, run +`git status --short && bun test --dots` in Checks. Rerun commands after changing +viewport size, and keep only one capture client attached. + +Build and start the bridge: ```sh bun run build:web @@ -203,150 +118,72 @@ XDG_STATE_HOME=/tmp/roamgate-demo/runtime-state bun server/src/index.ts --host 1 --client-socket-path /tmp/roamgate-demo/config/herdr/sessions/roamgate-demo/herdr-client.sock ``` -Open and log in with your configured password or the -saved token at the file path reported in the startup log. Remove any -authentication token from the URL before capturing. -Both socket paths must match a custom `ROAMGATE_DEMO_ROOT`. -The wrapper's Herdr session directory is selected by its `XDG_CONFIG_HOME`; -passing a different socket environment variable to a named Herdr session is -insufficient to change that directory. - -For a fresh topology, stop the demo bridge and server, then delete only the -stopped demo session before starting again: +Open , log in, and remove any token from the URL before +capturing. To start over, stop the bridge and run: ```sh bun run demo:herdr server stop bun run demo:herdr session delete roamgate-demo --json ``` -Agent transcripts live in the capture account's agent directories. Preserve a -useful real session before clearing or rebuilding that account/VM. - ## Run the Agent Scene -Use a fresh `main` clone in the isolated environment, skipping the review scene's -switch/reset steps. Start one supported agent in the Implementation pane with its -genuine Herdr integration and give it this prompt: +Agent, history, working-status, and **Last step** shots need a real agent run. +In a fresh `main` clone, start a supported agent in Implementation and give it: > Read README.md. Add All, To do, In progress, and Done > status filters to Northstar. Combine status with title search. Show an > accessible empty result with a Clear filters button. Preserve the static HTML > preview, add focused tests, and run them. Leave the changes uncommitted. -Use the resulting real changes, test output, and transcript together for the -agent/history shots. Keep the accepted session for later captures using the -agent's supported resume workflow. Re-running a model can change its wording and -implementation; the public review implementation is for deterministic file/diff -shots. Do not combine an unrelated transcript with PR #1's implementation as if -it caused the displayed changes. - -For a working-agent shot, start an actual follow-up task such as reviewing keyboard -behavior. Capture only after the integration reports the real status. **Last -step** also needs a genuine observed task completion; the review clone provides -**Working tree**, not an agent-completion snapshot. +Use that run's own changes, test output, and transcript together; never pair an +unrelated transcript with PR #1's code. Keep the session for later captures. ## Shot List and Capture Rules -Use dark theme with the default accent, English UI, and 100% browser zoom. -Use a desktop viewport of 1600 x 1000 CSS pixels and mobile viewport of -390 x 844 CSS pixels, both at device pixel ratio (DPR) 2. The PNG originals must -therefore be 3200 x 2000 desktop pixels and 780 x 1688 mobile pixels. Mobile -viewport emulation is suitable for layout screenshots; real mobile interaction -claims require a check on a physical phone. Record viewport, scale, app version, -scene, and browser with the exported originals. -After switching from desktop to mobile, enable **Wrap** and reload the page if -the diff retains its desktop width. Reopen Changes and select `tasks.ts` before -adjusting the scroll position. - -Export through Chrome's native screenshot UI: - -1. Open DevTools and toggle the device toolbar. Select **Responsive**. -2. Use **More options > Add device type** to show the device type control. - Choose **Desktop** for 1600 x 1000 or **Mobile** for 390 x 844, with **DPR 2** - and browser zoom at 100%. Allow Roamgate and terminal geometry to settle. -3. Choose **More options > Capture screenshot** in the device toolbar. This - exports a native PNG directly. Save the viewport capture, not a full-page shot. -4. Before accepting it, verify the PNG signature (`89 50 4e 47 0d 0a 1a 0a`) and - the required 2x pixel dimensions. Check that the image contains exactly the - product viewport, without browser chrome or empty padding. - -Tool-returned JPEG captures and JPEGs converted to PNG are not valid originals. -Changing the file format or upscaling does not recover lost text detail. +Dark theme, default accent, English UI, 100% browser zoom. Desktop viewport +1600 x 1000 and mobile 390 x 844, both at DPR 2, so originals are 3200 x 2000 and +780 x 1688 PNGs. In Chrome DevTools' device toolbar, pick **Responsive**, set the +size and DPR 2, and use **More options > Capture screenshot** (viewport, not full +page). Verify it is a real PNG of the right size with no browser chrome. JPEGs, +converted, or upscaled images are not acceptable. For the annotation shot, add `Keep the status filter combined with title search. -Clearing both filters should restore all six tasks.` on new line 27 of `tasks.ts`. -Pin the review panel and keep the comment as a draft. +Clearing both filters should restore all six tasks.` on new line 27 of +`tasks.ts`, pin the review panel, and leave the comment as a draft. | Asset stem | Layout and selection | Ready condition | | --- | --- | --- | -| `roamgate-desktop-changes` | Sidebar visible in Nested mode, genuine Pi TUI in Implementation/Checks split, Inspector right at about 55%; Working tree, `tasks.ts` | Connected; at least three real workspaces and two real Agent rows visible; Pi process registered and running; five review files loaded; real tests finished. If showing the optional PR card, PR #1 is open with successful CI. | -| `roamgate-desktop-files` | Sidebar visible in Nested mode; expand Inspector; Files; open `index.html` Preview; scroll to Release checklist | At least three real workspaces and two real Agent rows visible; local styles loaded; checklist heading, filters, and all six cards visible. | -| `roamgate-desktop-annotations` | Sidebar visible in Nested mode; `tasks.ts` unified diff and the review draft | At least three real workspaces and two real Agent rows visible; correct line anchor and full comment visible. | -| `roamgate-desktop-history` | Genuine Northstar Agent History, User/Agent/Tool enabled | The matching real task and completed test tool call are visible. | -| `roamgate-desktop-ranger` | Sidebar visible; Ranger pinned beside the Northstar workspace | Ranger has answered a real question using only authorized demo workspaces; its response and source references are readable. | -| `roamgate-mobile-changes` | Mobile; Changes, Working tree, `tasks.ts` | Unified diff loaded; key changed function readable. | -| `roamgate-mobile-terminal` | Mobile; Checks pane; Composer and terminal shortcuts open | Actual Git status and test summary visible; unrelated menus dismissed. | -| `roamgate-mobile-files` | Mobile; Files, `index.html` static Preview | Checklist heading, filters, and first task card readable. | -| `roamgate-mobile-ranger` | Mobile; Ranger full chat surface | The same real demo conversation is readable, with input and model controls visible. | - -For Ranger shots, connect a model in the isolated capture environment and -authorize only the public demo workspaces. Ask it to summarize Northstar's -uncommitted changes and recent test output, then wait for the real answer and -source references to finish loading. Preserve that conversation for the mobile shot. -Do not fabricate messages, tool results, task runs, or completion status. If -showing scheduled tasks, capture a real run while the Roamgate bridge is running. -Inspect the conversation, expanded tool details, model settings, and visible -source paths for credentials or personal information before capturing. Keep -Ranger's experimental status clear in the accompanying publication copy. - -Wait for connection, fonts, terminal geometry, and requested content to settle; -use these conditions rather than fixed sleeps. Start each shot with the same file, -scroll position, panel widths, and dismissed menus. Capture the product viewport -directly. Keep the interface text and terminal output intact. -If the exported bounds or dimensions differ, correct the browser settings and -recapture. Do not trim product controls, upscale, or sharpen screenshots. - -Before publishing, inspect the full-resolution image and a README-sized preview. -Text, code, icons, and terminal output must be crisp and readable at their intended -display sizes. Blur or compression artifacts fail review: recapture a native PNG -at DPR 2 instead of repairing the image. -Check visible paths, account/host names, URLs, prompts, Git authors, browser chrome, -and session metadata against the demo allowlist. Exclude auth URLs, tokens, -personal workspaces, and unrelated history. Scan the source text/transcript as -well as inspecting the image; automated scans cannot prove that an image is safe. -Correct the source scene and recapture when private content appears. - -Keep native PNG originals outside the publication assets, such as in a local -archive. Use local lossless compression for publication copies by default, -without resizing. Use TinyPNG only when the user explicitly authorizes uploading -the selected images to that external service; its compression may quantize colors -and must not be described as lossless. Verify native dimensions and text/icon -clarity at 100% and intended display sizes before saving the approved PNGs in -`docs/images/`. The existing `scripts/build-pages.ts` -copies the shared README shots into the website. -Set desktop PNG `img` dimensions to `width="3200" height="2000"` and mobile -dimensions to `width="780" height="1688"`. The homepage hero uses the -computer-to-phone animation described below. -Capture outputs and review drafts are not required additions to Git; publish -only the selected product assets. Run `bun run build:site` after updating consumed -images and links. +| `roamgate-desktop-changes` | Nested sidebar; Pi in Implementation above Checks; Inspector right (~55%); Working tree, `tasks.ts` | 3+ workspaces and 2+ agent rows visible; Pi running; five files loaded; tests finished. | +| `roamgate-desktop-files` | Nested sidebar; expanded Inspector; Files, `index.html` Preview scrolled to Release checklist | Styles loaded; heading, filters, and all six cards visible. | +| `roamgate-desktop-annotations` | Nested sidebar; `tasks.ts` unified diff with the draft | Correct line and full comment visible. | +| `roamgate-desktop-history` | Agent History, all types enabled | The real task and its test tool call visible. | +| `roamgate-desktop-ranger` | Ranger pinned beside Northstar | A real answer with sources, using only demo workspaces. | +| `roamgate-mobile-changes` | Mobile Changes, Working tree, `tasks.ts` | Key changed function readable. | +| `roamgate-mobile-terminal` | Mobile Checks pane with Composer and shortcuts | Real Git status and test summary visible. | +| `roamgate-mobile-files` | Mobile Files, `index.html` Preview | Heading, filters, and first card readable. | +| `roamgate-mobile-ranger` | Mobile Ranger chat | The same real conversation with input and model controls. | + +For Ranger, connect a model in the capture environment, allow only the demo +workspaces, and ask it to summarize Northstar's uncommitted changes and test +output. Never fabricate messages, tool results, or task runs. + +Wait for content to settle rather than sleeping a fixed time. Do not crop, +upscale, or sharpen; recapture instead. + +Before publishing, check full size and README size for crisp text, and inspect +paths, hostnames, URLs, prompts, authors, and transcripts for anything private. +Keep originals outside the repo; compress losslessly without resizing (use +TinyPNG only with explicit permission, and do not call it lossless). Save +approved PNGs to `docs/images/` with `width="3200" height="2000"` (desktop) or +`width="780" height="1688"` (mobile) on `img` tags, then run `bun run build:site`. ## Computer-to-Phone Animation -The README uses `docs/images/roamgate-handoff.gif`; the homepage uses the matching -`roamgate-handoff.mp4` and a static `roamgate-handoff.webp` poster. The Pages build -copies all three into `assets/`. The homepage starts playback when the demo is -visible, leaves playback manual when reduced motion is requested or JavaScript -is disabled, and exposes native playback controls. - -Render the 13-second English sequence at 1920 x 1200 and 15 fps from the original -desktop Changes, mobile Changes, and mobile Terminal PNG captures. Composite -them directly at the output size inside the device frames. The animation -illustrates a handoff using real UI captures; it is not a live recording of a -cross-device interaction. Keep Herdr and remote-access requirements visible. - -Export the GIF with a shared palette and conservative GIF compression. Encode -the same original frames as H.264 MP4 and export a lossless WebP poster from the -computer-and-phone scene. Preserve the source captures outside publication -assets. Check the complete loop, code text, rounded phone corners, and controls -at native and README display sizes before replacing the three shared files. +The README uses `docs/images/roamgate-handoff.gif`; the homepage uses +`roamgate-handoff.mp4` with a `roamgate-handoff.webp` poster. Render a 13-second +English sequence at 1920 x 1200 and 15 fps from the original desktop Changes, +mobile Changes, and mobile Terminal captures, composited into device frames. It +illustrates a handoff; it is not a live recording. Export the GIF with a shared +palette, the MP4 as H.264, and the poster as lossless WebP, then check the full +loop at native and README sizes before replacing all three files. diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index c7051efb..932f2cb5 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -3,14 +3,10 @@ # From your first terminal to a workspace that travels -**Start Roamgate on the Herdr host and open its URL:** begin with [chapter 1](#start). -Practice one task: ask an agent to improve a README, review it, then check from -another device. No Git or networking knowledge is needed to start. - -Allow 5 minutes for chapter 1 with both tools installed, 25 minutes for chapters -1–3, and another 15–25 for remote access, excluding installation/troubleshooting. -Stop at any completion check. **This tutorial website does not run Herdr or keep -terminals alive.** +Practice one task: ask an agent to improve a README, review the change, then +check it from another device. Chapter 1 takes about 5 minutes, chapters 1–3 +about 25, and remote access another 15–25. You can stop after any chapter. +**This website does not run Herdr or keep terminals alive.** ## 1. Start here: see your own work locally @@ -20,51 +16,47 @@ terminals alive.** Browser / PWA -> Roamgate (HTTP + WebSocket) -> Herdr -> shell / agent ``` -Herdr owns terminals; Roamgate supplies the browser UI; agents remain CLIs. -Roamgate does not provide models or install/sign in to agents. Closing the -browser leaves terminals running; host sleep, shutdown, or process exit can stop work. - -1. Start Herdr with a trusted project open; see [herdr.dev](https://herdr.dev). - For default local setups, `roamgate herdr setup` can also install/start it - after [installing Roamgate](./DEPLOYMENT.md#install-a-release). -2. Run `roamgate` on that computer and leave it running (`roamgate.exe` on - Windows). Plugin users instead use the [startup action](./DEPLOYMENT.md#herdr-plugin) - and obtain the URL from its panel/log; the plugin does not add the CLI to PATH. -3. Open the printed URL, including any token, **on the same computer**. Log in - with your configured password or the saved token at the file path reported - by the bridge. Select a workspace and idle shell; run `pwd` - (`Get-Location` in PowerShell). - -**You are done when:** terminal input shows your project directory. - -Standalone defaults to `127.0.0.1:8787`. **Normal runtime requires a token or -password**, including loopback; see [Security](../SECURITY.md#trust-model) for -the local development exception. A new user/plugin service defaults to -`0.0.0.0:8787` with a token. -Existing Herdr Studio / Roamgate 0.7.0 installations should follow -[migration guidance](./DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui) -before changing services. +Herdr owns the terminals, Roamgate is the browser UI, and agents remain CLIs. +Roamgate does not provide models or install agents. Closing the browser leaves +terminals running; sleeping or shutting down the host stops them. + +1. Start Herdr with a trusted project open ([herdr.dev](https://herdr.dev)), or + run `roamgate herdr setup` after [installing Roamgate](./DEPLOYMENT.md#install-a-release). +2. Run `roamgate` (`roamgate.exe` on Windows) on the same computer and leave it + running. Plugin users use the [start action](./DEPLOYMENT.md#herdr-plugin) + and get the URL from the plugin panel. +3. Open the printed URL **on that computer** and log in with your password or the + token from the file the bridge reports. Pick a workspace and an idle shell, + then run `pwd` (`Get-Location` in PowerShell). + +**You are done when:** the terminal shows your project directory. + +Login is required even on `127.0.0.1:8787` (only the loopback development +server skips it); see [Security](../SECURITY.md#trust-model). Upgrading from Herdr Studio? Follow the +[migration guide](./DEPLOYMENT.md#transition-from-herdr-studio--herdr-gui) +first. ![Desktop workspace with project navigation, terminals, and changed files](./images/roamgate-desktop-changes.png) | Object | Meaning | | --- | --- | -| Workspace | Working environment, usually a project directory | +| Workspace | A working environment, usually a project directory | | Tab | A layout within a workspace | -| Pane | One terminal region within a tab | +| Pane | One terminal in a tab | | Worktree | A separate Git checkout directory and branch | -**Splitting panes does not isolate files.** Use separate worktrees for parallel edits. +**Splitting panes does not isolate files.** Use worktrees for parallel edits. ## 2. Everyday work: verify what the agent changed -### Find a pane and give the agent one task +### Give the agent one task -1. Select your project, create a tab, and split right from its pane menu. +1. Select your project, create a tab, and split it right from the pane menu. 2. Start an installed, signed-in agent on the left; keep a shell on the right. -3. Paste this prompt, review it, and submit manually. Model usage may incur charges. +3. Paste this prompt, review it, and submit it yourself (model usage may cost + money): ```text Read this project's README and add a short first-run example. @@ -73,48 +65,41 @@ Modify only the README. Do not install dependencies, commit, or push. When finished, explain what changed and what you have not actually verified. ``` -The command menu searches workspaces, paths, tabs, panes, and agents. The recent -pane switcher moves between live panes; release its modifier to switch, Esc to -cancel. See [platform shortcuts](../FEATURES.md#keyboard-shortcuts); browsers -can intercept bindings, so use menus or customize them rather than replacing -all Cmd keys with Ctrl. +Use the command menu to find workspaces, files, tabs, panes, and agents, and the +recent pane switcher to jump between panes. See +[shortcuts](../FEATURES.md#keyboard-shortcuts). -**You are done when:** you know which pane receives input and can find the agent's -explanation in **Agent History**. A `done` status is not proof that checks passed. -For missing records, follow [session inspection guidance](../FEATURES.md#agent-awareness-and-session-inspection). +**You are done when:** you can find the agent's explanation in **Agent +History**. A `done` status does not mean checks passed. Missing history? See +[session inspection](../FEATURES.md#agent-awareness-and-session-inspection). ### Review the file and diff -1. Open **File Explorer**, select `README.md`, and compare **Raw / Rendered**. -2. Open **Diff Viewer > Working tree** and read the added/removed lines. -3. Search with `Cmd/Ctrl+F` for the new command; verify it in the project's configuration. +1. Open **File Explorer**, select `README.md`, and switch between rendered and + source views. +2. Open **Diff Viewer > Working tree** and read the changed lines. +3. Search (`Cmd/Ctrl+F`) for the new command and check it against the project. -**You are done when:** you can identify the change and verify its command. +**You are done when:** you can point to the change and confirm its command works. ![File Explorer previewing a README](./images/roamgate-desktop-files.png) -Tree search covers loaded files, not repository-wide contents. Right-click or -long-press opens file actions; `Cmd/Ctrl+Click` on terminal paths opens previews. -Uploads, deletions, and SSH file operations affect real target-host files. +Uploads, deletions, and SSH file operations change real files on the target host. ### Send precise feedback -1. Click/drag diff line numbers and comment, for example: “Use the dev command - in package.json.” Source gutters and rendered Markdown selections also work. -2. Check the files, ranges, and quotes in **Annotations**, then select the agent - pane and pre-fill its input. -3. Review the message in that pane and press Enter yourself. +1. Click or drag diff line numbers and add a comment, such as "Use the dev + command in package.json." +2. Review it in **Annotations**, then pre-fill the agent pane's input. +3. Check the message in that pane and press Enter yourself. -**You are done when:** the agent receives contextual feedback. **Pre-fill does -not submit.** Drafts belong to this browser/checkout, not GitHub or other devices; -stale anchors keep their captured quotes. +**You are done when:** the agent receives your feedback with its context. +**Pre-fill never submits.** Drafts stay in this browser. ![Diff annotations on selected changed lines](./images/roamgate-desktop-annotations.png) -This exercise needs no commit. Before committing, run `git status` and follow -the project's checks. Staging is not committing; committing is not pushing. -**Discard Unstaged loses changes; Delete Untracked deletes files. Confirmation -is not a backup.** Cancel bulk actions if unsure. +No commit is needed for this exercise. **Discard Unstaged loses changes and +Delete Untracked deletes files; confirmation is not a backup.** ## 3. Parallel work and mobile access @@ -122,30 +107,27 @@ is not a backup.** Cancel bulk actions if unsure. ### Separate parallel edits with worktrees -1. Verify that `origin` is reachable and has a default branch. Review `roamgate.json` and legacy `paseo.json`; - disable **Worktree hooks** if you do not trust its commands. Hooks are enabled - by default and run host code. -2. Open **Worktree Lifecycle** from the workspace/command menu and create a worktree. - It starts from `origin`'s freshly fetched default branch, without copying source - dirty files. +1. Make sure `origin` is reachable. Review `roamgate.json` (or legacy + `paseo.json`): its hooks are on by default and run on the host. Disable + **Worktree hooks** if you do not trust them. +2. Open **Worktree Lifecycle** and create a worktree. It starts from `origin`'s + latest default branch and does not copy uncommitted files. 3. Run `pwd` and `git status` there before starting another agent. -**You are done when:** parallel tasks have different checkout directories and branches. -Before removal, save wanted results, end tasks, and review confirmation. -[Failed teardown blocks removal](./DEPLOYMENT.md#worktree-hooks). -Leave [automatic branch updates](../FEATURES.md#automatic-branch-updates) off for -this exercise; when enabled they fetch/merge, never push. +**You are done when:** parallel tasks use different directories and branches. +Save wanted results before removing a worktree; a +[failed teardown hook blocks removal](./DEPLOYMENT.md#worktree-hooks). ### Continue from your phone -Complete [Tailscale + Serve](#tailscale) first. Use its HTTPS URL; `127.0.0.1` -on a phone points to the phone, not your computer. +Set up [Tailscale + Serve](#tailscale) first: `127.0.0.1` on a phone means the +phone itself. -1. Open Roamgate on your phone, log in, and select your project/pane. -2. Use the floating terminal shortcuts for Ctrl/arrows, then open Changes to review - the unified diff. Long-press opens file actions. -3. Adjust **Configuration > Appearance > Interface scale** or **Terminal** font size - if needed and install the PWA: +1. Open Roamgate's HTTPS URL on your phone, log in, and select your pane. +2. Use the floating shortcuts for Ctrl and arrow keys, and open Changes to review + the diff. +3. Adjust **Configuration > Appearance** scale or terminal font size if needed, + then install the PWA: | Browser | Install action | | --- | --- | @@ -153,18 +135,15 @@ on a phone points to the phone, not your computer. | macOS Safari 17+ | File > Add to Dock | | Chrome/Edge | Browser menu > Install app | -**You are done when:** the home-screen icon opens your project. Install the stable -Roamgate service URL, not this tutorial website. +**You are done when:** the home-screen icon opens your project. Install your +Roamgate URL, not this tutorial site. ![Mobile terminal with touch shortcuts](./images/roamgate-mobile-terminal.png) -PWA mode is neither offline access nor background keep-alive. Host sleep, stopped -services, or VPN loss interrupts access. [Notifications](./DEPLOYMENT.md#web-push-notifications) -are best-effort, not reliable alerts. - -**Multiple browsers are not separate permission roles.** Profiles are shared, -selection is per browser, and preferences/drafts may stay local. Anyone admitted -has terminal/file authority; do not share this URL like a read-only document. +A PWA is not offline access: host sleep, a stopped service, or lost VPN +interrupts it. [Notifications](./DEPLOYMENT.md#web-push-notifications) are +best-effort. **Everyone you let in has full terminal and file access**, so do not +share the URL like a read-only link. ## 4. Remote access: choose the right connection @@ -173,219 +152,171 @@ has terminal/file authority; do not share this URL like a read-only document. | Goal | Route | | --- | --- | | Regular phone/computer access | [Tailscale + Serve](#tailscale) | -| Local Roamgate controlling remote Herdr | [SSH profile](#ssh), Linux/macOS bridge | -| Another computer accessing remote Roamgate | [SSH web-port forwarding](#ssh) | -| Temporary experiment between your computers | [Tailcat](#tailcat) | - -These are external tools, not built-in integrations. No router forwarding, -exit node, or subnet routing is needed. +| Local Roamgate controlling a remote Herdr | [SSH profile](#ssh) (Linux/macOS bridge) | +| Another computer using a remote Roamgate | [SSH port forwarding](#ssh) | +| Temporary access between your own computers | [Tailcat](#tailcat) | -```text -Hop A: Browser -> Roamgate (Serve / SSH web forwarding / Tailcat) -Hop B: Roamgate -> Herdr (local sockets / Roamgate SSH profile) -``` +None of these need router port forwarding. ### Safety checks -- Admit only fully trusted people/devices. UI access has the Roamgate user's authority. -- Use normal runtime for forwarded access: loopback requires login too. Keep the - password or generated token private; use the credential from the Roamgate host. -- Use trusted HTTPS/encrypted tunnels and restrict listener/access policy. - Authentication alone adds no TLS, permission roles, or sandbox. See - [Security](../SECURITY.md#trust-model) for login limits and shared proxy-IP limits. +- Admit only fully trusted people and devices. +- Login is still required behind any tunnel; keep the password or token private. +- Use HTTPS or an encrypted tunnel and restrict who can connect. See + [Security](../SECURITY.md). - Keep passwords, token URLs, and Tailcat addresses out of screenshots, issues, - chats, and committed configuration. Read [Security](../SECURITY.md). + chats, and committed files. ### Tailscale + Serve: regular private access -Tailscale connects authorized devices over WireGuard; Serve supplies a tailnet-only -HTTPS address forwarding to local Roamgate. Direct connections or DERP relays -remain encrypted. +Tailscale connects your devices privately; Serve gives Roamgate an HTTPS address +reachable only inside your tailnet. -1. Install [Tailscale](https://tailscale.com/download) on both devices and join the - same tailnet. Verify `tailscale status`. For a missing macOS CLI, follow the - [macOS instructions](https://tailscale.com/docs/install/mac), not a second install. -2. [Restrict access](https://tailscale.com/docs/features/access-control) to intended - users/devices on the host's HTTPS port 443. Review broad rules before enabling Serve. -3. On the Herdr host, start Roamgate on a free loopback port: +1. Install [Tailscale](https://tailscale.com/download) on both devices, join the + same tailnet, and check `tailscale status`. +2. [Restrict access](https://tailscale.com/docs/features/access-control) to the + intended users and devices on port 443. +3. On the Herdr host, run Roamgate on loopback: ```bash roamgate --host 127.0.0.1 --port 8787 ``` - If a user/plugin service already runs it, do not duplicate it. Set - `HOST=127.0.0.1` and `PORT=8787` in its protected - [environment file](./DEPLOYMENT.md#run-as-a-user-service), preserving other settings, - then run `roamgate service restart` or the plugin's `roamgate.restart` action. - Confirm asynchronous plugin restarts in its log. Keep the configured password - or saved token available; Roamgate still requires it behind Serve. + If a service already runs it, set `HOST=127.0.0.1` in its + [environment file](./DEPLOYMENT.md#run-as-a-user-service) and run + `roamgate service restart` instead of starting a second copy. -4. Inspect `tailscale serve status`. Only if HTTPS port 443 at `/` is free, run: +4. If `tailscale serve status` shows port 443 at `/` is free, run: ```bash tailscale serve --bg --https=443 http://127.0.0.1:8787 ``` - Preserve existing mounts; use official docs if another service occupies them. - Approve HTTPS certificate consent as an authorized admin. Hostnames appear in - public certificate transparency logs, so avoid sensitive names. -5. On the Tailscale-connected phone, open the **printed HTTPS hostname**, not an - IP or localhost. Log in with the Roamgate credential, then run `pwd` in an - idle pane. Verify excluded devices cannot connect, using policy tests if no - test device exists. With Tailscale off and no other access path, the URL - should be unreachable. - -**You are done when:** cellular access works and excluded devices are denied. -Incognito is not an admission test; device identity is unchanged. + Approve the HTTPS certificate prompt. Hostnames appear in public certificate + logs, so avoid sensitive names. +5. On your phone (with Tailscale on), open the **printed HTTPS hostname**, log + in, and run `pwd`. Confirm devices outside your access rules cannot connect. -**Serve is private; Funnel is public. Do not substitute `funnel`.** Use domain -root `/`; arbitrary subpaths are not assured. Serve headers do not add user roles. +**You are done when:** it works over cellular and excluded devices are denied. -To stop sharing, first check other mounts will not be affected, then run: +**Serve is private; Funnel is public. Never use `funnel` here.** To stop +sharing: ```bash tailscale serve --bg --https=443 off tailscale serve status ``` -Avoid `tailscale serve reset`: it clears all Serve configuration. `--bg` persists -Serve, not Roamgate, and does not prevent sleep. For daily use, configure a -[Roamgate user service](./DEPLOYMENT.md#run-as-a-user-service) with loopback retained. +Avoid `tailscale serve reset`, which clears all Serve settings. For daily use, +run Roamgate as a [user service](./DEPLOYMENT.md#run-as-a-user-service) on +loopback. ### SSH: remote Herdr or remote web interface -**Option A — local Roamgate, remote Herdr:** requires a Linux/macOS bridge. -The remote host may run Unix or Windows. A Windows bridge supports local profiles. +**Option A: local Roamgate, remote Herdr** (needs a Linux/macOS bridge; the +remote host can be Unix or Windows). -1. Verify system SSH, host fingerprint, authentication, and remote Herdr readiness. - Put keys, ports, and jump hosts in `~/.ssh/config`, for example alias `workbox`. -2. Add an SSH profile from Roamgate's connection selector: Destination `workbox`, - choose the remote host OS. For Unix, leave socket paths empty for automatic - resolution. For Windows, install/start Herdr 0.9.1+ first, optionally specify - its executable path and session, then test/connect and verify `Get-Location` - in PowerShell. See [Windows requirements and limitations](./DEPLOYMENT.md#multiple-and-remote-herdr-connections). +1. Make sure plain `ssh` works non-interactively, put keys, ports, and jump hosts + in `~/.ssh/config` (for example alias `workbox`), and start Herdr remotely. +2. In Roamgate's connection selector, add an SSH profile with destination + `workbox` and the remote OS. Leave Unix socket paths empty. For Windows, see + [requirements](./DEPLOYMENT.md#multiple-and-remote-herdr-connections). + Connect and run `pwd`. -Alternatively, on a free port: +Or start a dedicated bridge for that host: ```bash roamgate --ssh-host workbox --host 127.0.0.1 ``` -Files, Git, image uploads, and hooks run remotely. Explicit socket flags/environment -variables override tunnel paths and can select the wrong host. Profiles store no -SSH secrets; see [connection configuration](./DEPLOYMENT.md#multiple-and-remote-herdr-connections). +Files, Git, uploads, and hooks then run on the remote host. -**Option B — forward remote Roamgate's web port:** both services stay remote; -this works with Windows OpenSSH too. Remote Roamgate in normal runtime uses -loopback and still requires login; SSH restricts who can establish forwarding. - -On the visiting computer, replace `workbox` with your SSH alias: +**Option B: forward a remote Roamgate's web port.** Both services stay remote; +this also works with Windows OpenSSH. On the visiting computer: ```bash ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18787:127.0.0.1:8787 workbox ``` -Open `http://127.0.0.1:18787` **on that computer**, not your phone. HTTP remains -loopback at both ends; SSH encrypts the link. Log in using the credential from -the remote Roamgate host. Press Ctrl+C to stop forwarding. +Open `http://127.0.0.1:18787` **on that computer** and log in with the remote +host's credential. Ctrl+C stops forwarding. ### Tailcat: temporary access between trusted computers -Tailcat is a separate Tailscale-team tool using WireGuard/NAT traversal/DERP -without a tailnet/account. It has no durable device directory or access-policy -management. **Experimental: no CLI/wire stability or relay SLA; this example -has not been verified end-to-end on two machines by this project.** - -Install matching supported builds from the [official repository](https://github.com/tailscale/tailcat#install) -on both computers (`brew install tailcat` on macOS). Check `tailcat --help`, -`tailcat serve --help`, and `tailcat forward --help` for the flags below. The web -demo is not a general Roamgate proxy; use Tailscale for phones. +[Tailcat](https://github.com/tailscale/tailcat#install) is an **experimental** +Tailscale tool that connects two computers without a tailnet account. It has no +access policies and this project has not verified it end to end. Use Tailscale +for phones. -1. Run and locally verify Roamgate at `127.0.0.1:8787` on your work computer. - Keep its login credential available; only proceed between computers you control. -2. In another terminal there, start a single-port service: +1. Run Roamgate at `127.0.0.1:8787` on your work computer and check it locally. +2. In another terminal there, run: ```bash tailcat serve --key=new 8787 ``` - Confirm a new address: `--key=new` avoids reusing a previously shared saved key. -3. Privately send the complete printed `tc...` address to the visiting computer. - Replace the placeholder and run there: +3. Privately send the printed `tc...` address to the visiting computer and run: ```bash tailcat forward tcREPLACE_WITH_PRIVATE_ADDRESS 18787:8787 ``` - Verify the listener is `127.0.0.1`; never add `--bind=0.0.0.0`. -4. Open `http://127.0.0.1:18787` there, log in with the Roamgate credential, - and verify terminal input with `pwd`. -5. Press Ctrl+C in both Tailcat terminals and confirm the URL stops working. - The ephemeral address no longer points to a running service after exit. + Keep it on `127.0.0.1`; never add `--bind=0.0.0.0`. +4. Open `http://127.0.0.1:18787`, log in, and run `pwd`. +5. Press Ctrl+C in both terminals and confirm the URL stops working. -**You are done when:** private access works and ends when forwarding stops. -Treat the address as a credential: it includes a pre-shared key, not just a -hostname. Keep default protections; do not expand to `serve all`, exit-node mode, -authentication-free SSH, or writable-directory sharing. Read the current -[Tailcat security model](https://github.com/tailscale/tailcat/blob/main/SECURITY.md). +**You are done when:** access works and ends when forwarding stops. Treat the +address as a secret: it contains a key. See Tailcat's +[security model](https://github.com/tailscale/tailcat/blob/main/SECURITY.md). ## 5. Troubleshoot the hop that failed -Check in order (about 3 minutes): +Check in order: 1. On the Roamgate host, run `curl -fsS http://127.0.0.1:8787/healthz` - (`curl.exe` in PowerShell). If refused, check process/port/service status. - Health is not proof every Herdr feature works. -2. Open the page locally. If terminals fail, check Herdr, selected profile, - both socket paths, and connection logs before changing the VPN. -3. Once local access works, check the route: `tailscale status` / - `tailscale serve status`, the SSH forwarding process, or both Tailcat processes - and this run's address. -4. If typing fails, check the selected pane, browser pause state, and proxy support - for WebSocket upgrades/long-lived connections. Retry locally to isolate the proxy. + (`curl.exe` in PowerShell). If refused, check the process, port, or service. +2. Open the page locally. If terminals fail, check Herdr, the selected + connection, and the logs before touching the network. +3. Once local access works, check the route: `tailscale serve status`, the SSH + forward, or both Tailcat processes. +4. If typing fails, check the selected pane and that your proxy supports + WebSockets. | Symptom | First check | | --- | --- | -| Phone localhost cannot reach the computer | Use Serve's printed HTTPS hostname. | -| Password set but no login page | Use incognito to rule out an existing cookie or a token URL that already logged you in. | -| Login returns `429` | Wait for `Retry-After`; forwarded clients may share the same [source-IP limits](../SECURITY.md#trust-model). | -| Address already in use | Check for an existing plugin/user service; do not duplicate it. | -| SSH history is empty | Check transcript readability and [session lookup limits](../FEATURES.md#agent-awareness-and-session-inspection). | -| Clipboard/PWA restricted | Use trusted, warning-free HTTPS; check browser permissions/support. | - -See [Deployment](./DEPLOYMENT.md) for tokens, service restarts, and debug logs. -Do not disable authentication or open the entire firewall. Share only redacted -logs, versions, and the failing step when requesting help. +| Phone cannot reach `localhost` | Use Serve's HTTPS hostname. | +| Password set but no login page | Try a private window; an existing cookie or token URL may have logged you in. | +| Login returns `429` | Wait for `Retry-After`; clients behind one proxy share [login limits](../SECURITY.md#trust-model). | +| Address already in use | A service or plugin may already run Roamgate. | +| SSH history is empty | Check [session lookup limits](../FEATURES.md#agent-awareness-and-session-inspection). | +| Clipboard or PWA restricted | Use trusted HTTPS without certificate warnings. | + +`roamgate doctor` checks most of this automatically. See +[Deployment](./DEPLOYMENT.md) for tokens, services, and debug logs. Never +disable authentication or open the firewall wide; share only redacted logs when +asking for help. ### Completion check -- [ ] I know which workspace, tab, and pane will receive my input. -- [ ] I reviewed the actual diff and know that an agent's "done" is not verification. +- [ ] I know which workspace, tab, and pane receives my input. +- [ ] I reviewed the actual diff; an agent's "done" is not verification. - [ ] I know which host receives file and Git operations. -- [ ] For remote access, I checked the effective authentication boundary, listener address, allowed users, and how to stop sharing. +- [ ] For remote access, I know who can connect and how to stop sharing. ### References [Features and shortcuts](../FEATURES.md) · [Deployment](./DEPLOYMENT.md) · -[Security](../SECURITY.md) · [Architecture](./ARCHITECTURE.md) - -Third-party commands follow official sources; check installed help for Tailcat: - -- [Serve concepts](https://tailscale.com/docs/features/tailscale-serve) and - [CLI/shutdown](https://tailscale.com/docs/reference/tailscale-cli/serve). -- [Access controls](https://tailscale.com/docs/features/access-control) and - [HTTPS/public certificate names](https://tailscale.com/docs/how-to/set-up-https-certificates). -- [Funnel](https://tailscale.com/docs/features/tailscale-funnel): not the private route used here. -- [Tailcat README](https://github.com/tailscale/tailcat#readme) and - [security model](https://github.com/tailscale/tailcat/blob/main/SECURITY.md). +[Security](../SECURITY.md) · [Architecture](./ARCHITECTURE.md) · +[Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) · +[Serve CLI](https://tailscale.com/docs/reference/tailscale-cli/serve) · +[Tailcat](https://github.com/tailscale/tailcat#readme) -This Markdown is the canonical tutorial; the Pages build renders it into HTML. -Update affected steps when product behavior or upstream commands change. +This Markdown is the canonical tutorial; the Pages build renders it to HTML.