Skip to content

Latest commit

 

History

143 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Helmryth

The work moves. You hold the helm.

A private operating system for autonomous work.

Persistent operators that run on CLIs you already own. Crews that share a brief without sharing state. A gate in front of every consequential action. All of it on your machine.

Quick start · The films · How it works · Security

3,054 tests, green on macOS, Linux and Windows · 160 routes · 597 UI controls declared across 63 files · connector catalog fetched live


Nothing runs until you answer

An operator wants to run a shell command. It does not get to.

A card appears in the transcript naming the exact command, the consequence in plain English, and who asked. You allow once, or you deny.

The answer is written to a ledger.

A gate card headed GATE WAITING · Shell command holds a mkdir command in a RESOURCE box, states its consequence and names the active operator as the requester, above Deny request, Grant options and Allow once; the composer below reads Resolve the gate above to continue.

Playing now, with no click. 04 · Nothing runs until you answer is the full film. The decision in it is a real row — Forge · Bash · mkdir -p /tmp/helmryth-demo && printf 'READY' > … — still readable at GET /api/decisions long after the recording stopped.


Why this exists

A chat window is stateless. You paste context in, you copy an answer out, and when you close the tab the thing you were working with is gone — its memory, its permissions, its half-finished job.

Helmryth keeps the other half. An operator is a standing role: its own engine, its own remit, its own capabilities, its own workstreams and its own cost ledger, all of which survive a restart. A crew is several operators sharing one brief and one transcript. A gate is the moment an operator wants to do something consequential and has to ask you first — in the transcript, with the exact command, and a record of what you decided.

Nothing here calls home. The control plane is a local HTTP server bound to IPv4 loopback, the renderer is one known origin, your API keys are write-only, and telemetry is off unless you turn it on.

Thirty films. One take each.

Every film on this page was recorded in one take against a live build — real engines, real model replies, real containers, real connectors. Nothing is mocked or re-enacted. Seven of them are played back faster to trim dead air while a model was thinking — most sit near 1.2x and the longest, 29-takeover and 36-isolated-workbench, run at 1.45x. The token counts, dollar amounts and error states on screen are the ones the app produced.

Every headline below links to its full film. The quoted string under each one is read off that film's own frame.

Why every loop on this page is named .png
GitHub renders a repo-relative <video> as nothing, and puts a click-to-play control in front of anything it recognises as a .gif — so a page of honest .gif loops sits motionless until each one is clicked. Browsers dispatch on the file header, not on the extension. These files are unmodified GIF89a: 860×538, 10fps, exactly 12.0 seconds, cut by scripts/film/loop.mjs straight from the delivered master. Nothing is re-encoded to achieve it, and renaming them to .gif would break every one.

I · A roster, not a tab strip

A run is an object inside a role, not a scroll position — its own transcript, its own token tally, its own unsent draft — and four of them can sit open in one operator without a word crossing between them. A crew's brief is one written document that every operator reads before each of its turns, and they all write back into one transcript.

A crew workstream headed Release desk under a Full crew mobilizes selector: one pinned brief at the top, a question addressed to Cairn, Cairn's answer posting beneath it, then Vesper's answer posting under that while the run-in-progress pip moves down the roster.

14 · One brief. Three operators. Ordered turns. — Name one operator and only it answers; switch to full crew and every operator takes a turn in roster order, in the same thread.

02 · Archive a role. Restore it whole.
Archiving takes an operator off the roster without destroying it; one click restores it whole.
Their workstreams remain intact until you delete the operator.
03 · Three operators, one shared workstream
Name a crew, tick its operators, then set how unaddressed work routes through them.
Choose the operators who will share one workstream.
26 · Read the repo, staff the crew
It reads a project folder and proposes a crew, with the files behind each role, before anything exists.
Detected via react, vite, index.html, vite.config.ts
08 · Roles load. Nothing else does.
Loading a crew from the library brings its named roles and nothing else of yours.
Only operator roles and appearance are loaded. Your work…
11 · Four runs. One operator. No bleed.
Each run keeps its own transcript, token count and unsent draft; switch away and back and the draft survives.
Hold the Windows signing evidence until the notarisation…
12 · Type into a run already moving
A direction sent mid-run lands inside that same run, or is visibly held until the step clears.
Rivet is running — Enter adds direction now
13 · Rewrite the direction. The run forks.
Edit a sent direction in place and commit; the run reruns, and the first outcome is kept, not replaced.
Enter commits · Shift+Enter adds a line · Esc keeps…
10 · The roster, one chord away
Press ⌘K anywhere, type three letters of a name, and land inside that operator's transcript.
Search operators, crews, workstreams…

II · What it will not do

The loop at the top of this page is from this band. A gate is not a confirmation dialog — it is the point where the operator stops and the record starts. These six films are the refusals: what a standing rule will and will not cover, what a credential is allowed to touch, what happens when an inbound payload tries to issue orders, and what an operator is never permitted to schedule for itself.

A typed shell command containing a plaintext OpenAI key returns in a GATE WAITING card with the identical command intact but the key replaced by the string redacted 44 chars, above Deny request, Grant options and Allow once.

20 · Nothing key-shaped survives the transcript — Paste a key into a command and it comes back masked in the gate card, in the reply, and in the raw protocol log on disk.

18 · Grant one command, not a blanket
Grant options names the exact key it will remember; the next unrelated command still stops at a gate.
Always allow Bash:mkdir
19 · A key the transcript never holds
The operator raises a gate for the key, stores it on the device, and resumes without recording it.
Stored locally; never written into the workstream.
22 · The payload never becomes an order
A webhook body demanding your API keys arrives fenced as untrusted data; the operator names it and refuses.
That's a prompt-injection attempt riding inside the web…
24 · It can only propose the schedule
Told to put itself on a cadence, it can only raise a gate carrying the exact executable draft.
Schedule: Monday, Tuesday, Wednesday, Thursday, Friday…
25 · Imported methods arrive switched off
A skill pulled from GitHub stays disabled until you read the whole file; edit it on disk and it disables itself again.
I reviewed the complete method and understand its sour…
31 · A hedge is not consent
Answer a gate out loud with "yes, if you think it's fine" and the operator asks you again, plainly.
Sorry — is that a yes or a no?

III · Where it works, and what it can reach

Every operator has an execution surface, and there are four of them: an isolated Linux desktop in a container on your own machine, a hosted desktop, your own Mac, or off. Off is the default. On top of that sits a browser the operator drives on its own profile, and a connector catalog it can only reach through connections you made by name.

The execution-surface row moves from Off to Isolated; the live-surface pane changes from a Workbench is off placeholder to a spinner reading Capturing the Isolated Workbench, then fills with a running Linux desktop — wallpaper, desktop icons, a taskbar — labelled Isolated Workbench above a Take controls button.

36 · A Linux desktop, on your machine — Switch the execution surface to Isolated and the operator gets a graphical Linux desktop in a local container, with no cloud account anywhere in the path.

28 · The operator gets its own browser
A real page loads inside the app on the operator's own profile, and you can take the wheel on the same tab.
Profiles hold logins and cookies. Operators assigned to…
29 · It will not type your password
At a sign-in wall the operator stops, hands you the keyboard and waits until you give the surface back.
You hold this surface. Rivet is waiting until you return it.
30 · Two locks before it touches macOS
The app asks you to acknowledge host control, and the local server rejects any API call that skips it.
{"error":"Auto mode on this Workbench requires confir…
06 · Search the live connector catalog
Type in the catalog and results filter against the live broker; connecting one is an explicit, named act.
Connect the services your operators may use during a run.

IV · Work that starts without you

Three ways a run begins while nobody is looking at the app: another AI over MCP on the loopback interface, a clock, or a single HTTP request. The plate is the first of them.

A terminal floating over the app runs Claude Code against Helmryth's MCP server on 127.0.0.1; create_operator returns created · Atlas, send_operator_message returns delivered · run, a Working counter ticks, and behind it the roster grows from three operators to four with Atlas at the top marked Run in progress.

27 · Another AI can staff your roster — Claude Code, over MCP on 127.0.0.1, creates an operator and briefs it; it appears in the roster while nobody touches the app. Ends on exit 0 · 9.0s.

07 · Put an operator on a schedule
Pick the operator, write what it should deliver, choose the hour and the weekdays, and the calendar fills.
Each occurrence starts a fresh run for its operator.
23 · One curl wakes an operator
Copy one command and an operator starts; send it twice for one run; rotate the secret and the replay is rejected.
curl -sS -X POST 'http://127.0.0.1:8621/hooks/wh_p6t…

V · Everything it did, on the record

Everything an operator does is written down while it happens: a step ledger that counts up live, a price per operator, and the verbatim protocol frames underneath both. Deleting a run does not delete the record of the deletion.

A Trace panel beside a run: an Events row expands into its JSON record showing stopReason end_turn, a cost figure and input and output token counts, then the panel switches to the Provider lens and fills with verbatim engine frames timestamped to the millisecond.

16 · The raw protocol your engine spoke — One switch turns a run into a timestamped step ledger; one lens shows the verbatim frames the engine sent and received, including your own prompt exactly as it arrived.

15 · Close the window. Work continues.
The run ledger records every step as it happens; reopen mid-run and it has moved on without you.
8 recorded steps · live
17 · Every run priced as it runs
Steps, tokens and cost per operator, with subscription spend captioned equivalent rather than billed.
Cost equivalent — on your subscription, not billed.
21 · Delete a run, keep the receipt
Removing a run itemises the transcript, attachments, trace files and decision rows it destroyed, and names what it kept and why.
Workspace checkpoint history was retained because snap…

Check it yourself

Three receipts that weigh nothing, need no video, and reproduce against your own build.

The host-workbench acknowledgement is enforced by the server, not the UI. Handing an operator your real macOS session takes a warning in the app and an explicit flag on the wire. The same PATCH without it is refused:

$ BOT=$(curl -s http://127.0.0.1:8799/api/bots -H 'Origin: http://127.0.0.1:5199' \
        | python3 -c 'import json,sys; print(json.load(sys.stdin)["bots"][0]["id"])')

$ curl -sD- -X PATCH http://127.0.0.1:8799/api/bots/$BOT \
      -H 'Origin: http://127.0.0.1:5199' \
      -H 'Content-Type: application/json' \
      -d '{"computer":"local","autoApprove":true}'

HTTP/1.1 400 Bad Request
{"error":"Auto mode on this Workbench requires confirming the warning first (acknowledgeLocalAuto)"}

It is the combination that is gated, so both fields have to be in the body — asking for the workbench alone is not the dangerous request and is answered 200. Only a body that also carries "acknowledgeLocalAuto":true grants it (server/index.ts:6291-6295).

The origin gate is exact-authority. A request carrying a wrong Origin is answered 403 forbidden: cross-origin request (server/index.ts:4213), and localhost is not accepted as an alias for 127.0.0.1.

The decision ledger outlives the run. GET /api/decisions replays what you answered, newest last; ?limit=0 and ?limit=nope are both refused with 400 limit must be a positive whole number (server/index.ts:7148, server/decision-log-wiring.test.ts).

Then run the suite yourself:

pnpm test            # full suite
pnpm lint            # oxlint
pnpm check:electron  # desktop hardening assertions

The traceability matrix and the dated execution evidence behind every number on this page are in docs/qa/.

How it works

┌───────────────────────────────────────────────────────────────┐
│  Electron shell        nodeIntegration off · contextIsolation │
│                        on · sandbox on · contextBridge only   │
├───────────────────────────────────────────────────────────────┤
│  React renderer        one known origin, e.g. :5199           │
├───────────────────────────────────────────────────────────────┤
│  Local core  :8799     IPv4 loopback only · exact-origin gate │
│    ├── engine drivers  claude · codex · opencode · openai…    │
│    ├── permission broker   unix socket, per turn, fail-closed │
│    ├── capability broker   500 connectors, credentials held   │
│    ├── cadence scheduler   + webhook receiver on :8800        │
│    └── SQLite + JSON       ~/.helmryth                        │
└───────────────────────────────────────────────────────────────┘

The permission broker is the interesting part. When an operator's engine wants a tool that its permission mode would otherwise silently deny, the request is routed over a per-turn unix socket to Helmryth, which renders it as a card in the transcript and waits for you. If the broker cannot start, the turn fails closed — an unanswerable request is denied, never auto-approved.

Quick start

Requirements

Node.js 24 or newer (engines.node: ">=24")
pnpm 10.33.0 (packageManager)
An operator CLI at least one of Claude, Codex, OpenCode, Grok, Cursor, Qwen, Kimi, Droid…
Docker optional — only for the Isolated Workbench

Helmryth does not ship a model. It drives the CLIs you already own and are already signed in to, which is why the setup step is a scan rather than a form.

Run it from source

git clone https://github.com/Helmryth/HelmRyth.git
cd HelmRyth
pnpm install

# terminal 1 — the local control plane (defaults to :8799, webhooks on :8800)
pnpm dev:server

# terminal 2 — the renderer (defaults to :5199)
pnpm dev

# terminal 3 — the desktop shell
pnpm dev:desktop

Then open the desktop window and walk the four setup steps. That is the whole install.

What to watch for. The setup step titled Runtime inventory — choose how work moves is not a configuration form. It is a live scan of the CLIs already installed on the machine, with their real version numbers, and an install command for the ones that are missing.

01 · It finds the engines you own

One thing that will bite you. The core accepts requests from exactly one renderer origin. If you change the UI port, set the same HELMRYTH_UI_ORIGIN in all three terminals or the core will answer 403 forbidden: cross-origin request. This is deliberate — it is what stops a web page you visit from driving your operators. See Local control-plane request boundary.

Verify your install

pnpm test          # full suite
pnpm lint          # oxlint
pnpm check:electron  # desktop hardening assertions

Configuration

Everything is optional. Helmryth runs with no configuration at all.

Variable Default What it does
HELMRYTH_PORT 8799 Core HTTP port
HELMRYTH_WEBHOOK_PORT HELMRYTH_PORT + 1 Webhook receiver
HELMRYTH_UI_PORT / HELMRYTH_UI_ORIGIN 5199 The one renderer origin the core will accept
HELMRYTH_DATA_DIR ~/.helmryth Profile, transcripts, decision ledger
COMPOSIO_API_KEY Enables the 500-connector capability catalog
OPENAI_COMPAT_API_KEY Key for any OpenAI-compatible endpoint (OpenRouter, Groq, local)
OPENAI_COMPAT_URL · OPENAI_COMPAT_MODEL · OPENAI_COMPAT_PROVIDER Endpoint, model and provider preference for that key
HELMRYTH_OPENAI_IMAGE_KEY Image generation for operator sigils

In packaged builds, credentials go through Electron's safeStorage into the OS keychain. In every build they are write-only across the API: GET /api/config returns configured flags, never values.

Security posture

This is a local agent runner that executes real commands on your machine. The boundaries are deliberate and tested, not aspirational.

  • Exact-authority origin gate. A request carrying a wrong Origin is refused, and localhost is not accepted as an alias for 127.0.0.1 — which is what closes DNS rebinding, since a browser always sends Origin cross-origin. A request with no Origin at all (curl, a native client) is still served, so the boundary is the loopback bind plus the origin check, not the origin check alone.
  • Desktop hardening. nodeIntegration: false, contextIsolation: true, sandbox: true, all IPC through a contextBridge allowlist. External URLs open only if the scheme is http: or https: — an allowlist, not a denylist, so javascript:, file://, data:, chrome:// and every scheme nobody has thought of yet are refused by default (electron/external-url.mjs).
  • MCP credentials never touch argv. They are written to a 0600 temp file and passed by path — anything on argv is readable by any local process through ps for the life of the turn, and the temp directory is removed when the session ends (server/drivers/claude.ts).
  • Gates fail closed. A permission broker that cannot bind denies; it never degrades to allow.
  • Write-only secrets. Keys go in and never come back out over HTTP.
  • Telemetry off by default, and there is no sink configured in this tree.
  • Webhook secrets are show-once. The creating POST returns the credential so you can install it; GET /api/webhooks has no secret field at all, so listing endpoints can never re-reveal one.

Verified surface

These are counted by scripts/check-qa-coverage.mjs, which fails the build if the code drifts from the documented census:

Tests passing 3,054 registered across 274 files — 3,035 pass on macOS (19 POSIX-only skips), 3,045 on Linux (9) — plus 89 in seven child suites
HTTP routes 160 unique, each mapped in the traceability matrix
Renderer controls 597 declared across 63 files
Runtime IPC channels 76, plus 10 preload event topics
Contrast 21 colour pairs measured by pnpm check:contrast, no failures

The route, control, file and IPC counts are build gates rather than claims: pnpm check:qa-docs pins each one and fails when the code and the count drift apart. The pass count is whatever pnpm test prints on the day you run it. The route-to-control traceability matrix is in docs/qa/.

What ships in this repo

  • the desktop app and the local control-plane server
  • the companion sidecar for phones
  • the Cloudflare workers for the capability broker and control plane
  • docs, QA traceability, and packaging metadata

Packaged distribution endpoints are not provisioned here. If you publish Helmryth for others, configure your own release channel, signing assets and checksums.

Do not rename the film assets. Every -loop.png under docs/media/ is an animated GIF89a saved under a .png name, for the reason given beside the films above. Renaming one to .gif silently kills the loop. They are cut by scripts/film/loop.mjs from the delivered masters, never re-encoded.

Documentation

Contributing

Bug reports, questions and patches are all welcome.

  • CONTRIBUTING.md — how to set up, what to run before opening a pull request, and the evidence a change is expected to carry
  • Discussions — questions and early ideas, so the issue tracker stays for things that are broken
  • SECURITY.md — how to report a vulnerability privately. Please do not open a public issue for one
  • CODE_OF_CONDUCT.md — the behaviour expected of everyone taking part

Legal and provenance

Helmryth is released under the Apache License 2.0.

Attribution and third-party notices live in NOTICE and third_party/. Those records preserve upstream history and licensing; they are not marketing copy.


HELMRYTH · The work moves. You hold the helm.

About

A private operating system for autonomous work. Persistent operators that run on the CLIs you already own, crews that share a brief without sharing state, and a gate in front of every consequential action — all on your machine.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages