Skip to content

One shared daemon per user, and masking on the OpenAI route - #89

Merged
its-janghoon merged 2 commits into
developfrom
feature/shared-daemon
Oct 13, 2026
Merged

its-janghoon merged 2 commits into
developfrom
feature/shared-daemon

Conversation

@its-janghoon

Copy link
Copy Markdown
Member

Summary

Builds section 3 of docs/LOCAL-ENGINE-API.md: one shared engine per user, and closes a masking gap on the OpenAI-compatible route.

Daemon

  • redrob serve --register: per-start secret, ~/.redrob/run/server.json (0600: id, version, url, port, pid, secret), exits when nobody has been attached for --idle-exit minutes (default 10, 0 = stay up) or when another daemon takes the record over
  • redrob service start|status|stop|restart; start --json prints the record for a non-JS caller (the launcher)
  • redrob acp --attach: the ACP face becomes a thin client of the daemon instead of running an engine of its own
  • /api/clients (new protocol group): attach, heartbeat, detach, list with idleForMs / idleExitAfterMs; attachments without a heartbeat for 90 s are dropped
  • /experimental/capabilities grows sharedDaemon: { version: 1 }
  • @redrob-labs/sdk/daemon: connectOrStart(), attach() (heartbeat built in), currentDaemon(), resolveRedrobBin()
  • generated client + SDK bindings regenerated; docs/LOCAL-ENGINE-API.md updated to describe what is built

Masking

  • POST /v1/chat/completions now goes through the same Mask as the agent runner. It called the provider directly before, so an app on this route had no masking while the switch said on. One vault per request; streamed and assembled answers are both put back; the model tier uses the configured local model as the runner does.

Verified

By hand against a dev daemon (REDROB_RUN_DIR in scratch): service start -> record with pid; /api/health 200, attach/heartbeat/detach/list 200/200/204/404, wrong secret 401; a second service start reused the pid; idle exit after the 1-minute window with the record removed; acp --attach answered ACP initialize with the engine pid being the daemon's, not the ACP process's; SDK connectOrStart + attach from a separate process.

Tests

  • packages/redrob: 724 pass (new test/daemon.test.ts, test/server/chat-completion-mask.test.ts; help-text snapshot updated for the new flags)
  • test:httpapi exercisers: 223/223 in coverage, auth and effect modes (4 new scenarios)
  • packages/core 1043 pass, packages/sdk 1 pass, bun turbo typecheck 18/18

Follow-ups (other repos)

Design engine-only, launcher engine card, cowork/office connectOrStart, browser/cut/cad acp --attach.

Every Redrob app that runs on this engine started one of its own: Cowork and Office spawned
`redrob serve`, Design, Cut, CAD and Browser spawned `redrob acp`, so a person with three apps open
ran three engines. docs/LOCAL-ENGINE-API.md section 3 described the alternative; this builds it.

`redrob serve --register` is the daemon. It draws a secret per start, writes
~/.redrob/run/server.json (0600: id, version, url, port, pid, secret), and exits once nobody has
been attached for --idle-exit minutes (default 10) or another daemon has taken the record over.
`redrob service start|status|stop|restart` drives it by hand; `start --json` prints the record
for a caller in another language. `redrob acp --attach` makes the ACP face a thin client of the
daemon instead of an engine of its own.

Attachments are counted at /api/clients (attach, heartbeat, detach, list), a new protocol group
with its generated client and SDK bindings; an app whose heartbeats stop is dropped after 90 s, so a
crashed app cannot pin the daemon. `/experimental/capabilities` grows `sharedDaemon`.

`@redrob-labs/sdk/daemon` gives JavaScript apps `connectOrStart()` (a file read and a health check
on the fast path; `redrob service start --json` only when there is no healthy daemon), `attach()`
with the heartbeat built in, and `resolveRedrobBin()` with the documented resolution order.

Separately, `POST /v1/chat/completions` now goes through the same PII masker as the agent runner.
It called the provider directly, so a downstream app on this route had no masking at all while the
switch said on. One vault per request: the caller sends real values, the provider sees
placeholders, the answer is put back, streamed or assembled; the model tier asks the configured
local model for names exactly as the runner does.

Verified by hand with a dev daemon: service start/status/stop, reuse of a running daemon, attach,
heartbeat, detach, 401 on a wrong secret, idle exit after the window with the record removed, and
`acp --attach` answering ACP initialize while the engine pid is the daemon's. Tests: redrob
724 pass (daemon registry + clients registry, chat/completions masking, httpapi exercisers 223/223
in all three modes with the four new scenarios), core 1043 pass, sdk 1 pass, turbo typecheck 18/18.
@its-janghoon
its-janghoon merged commit e1f4f43 into develop Oct 13, 2026
15 checks passed
@its-janghoon
its-janghoon deleted the feature/shared-daemon branch October 13, 2026 04:27
its-janghoon added a commit that referenced this pull request Oct 13, 2026
Every job checked out `develop` by name when it started. A merge landing between dispatch and
the tag step (#89 landed two minutes after today's release dispatch) meant the binaries were
built from one commit and the tag pointed at another. The version job now records the commit it
computed from, and the build and publish jobs check out that sha, so a release is one commit from
version to tag. The tag push is by explicit ref and works from the detached checkout.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant