Skip to content

feat: M1 daemon base (store, RPC, crews and bots, workspaces) - #1

Merged
httpsphl merged 7 commits into
mainfrom
feat/m1-daemon-base
Sep 28, 2026
Merged

httpsphl merged 7 commits into
mainfrom
feat/m1-daemon-base

Conversation

@httpsphl

Copy link
Copy Markdown
Owner

Milestone M1 — daemon base (spec section 17): config, store and migrations, /health, /rpc with session.hello, crews and bots CRUD, and workspace generation.

Done when (spec): an integration test creates a crew and a bot via RPC. That is crates/botloftd/tests/rpc_crews_bots.rs, which runs a real daemon on an ephemeral port and talks to it over a WebSocket.

What's in it

  • botloft-core: prefixed ULID ids (crw_, bot_, msg_, dlv_, tsk_), slug and handle rules, avatar palette, field validation, and the M1 protocol types. The types are exported to app/src/lib/protocol.gen.ts by cargo test -p botloft-core export_bindings; CI fails if that file is stale.
  • botloft-store: SQLite with WAL, foreign keys and busy timeout; numbered migrations in PRAGMA user_version (a database from a newer build is refused); crews and bots repositories.
  • botloftd
    • botloftd serve [--config]: data folder (BOTLOFT_HOME or %LOCALAPPDATA%\Botloft), daily logs kept 14 days, single-instance lock, owner token, database, listens on 127.0.0.1 only.
    • Owner token in secrets\owner.token with a protected DACL for the current user only; the daemon keeps just its SHA-256 and compares in constant time.
    • /rpc: JSON-RPC 2.0 over WebSocket. session.hello first (10 s), otherwise -32001 and close; foreign browser Origin gets 403. crews.* and bots.* from spec 11.2, with crew.changed / bot.changed notifications to every connection.
    • Workspace generation (spec 5.1): .claude/settings.json (exec-form hooks, crossSessionInbound: accept, deny rule for the secrets folder), .botloft/mcp.json, .claude/rules/botloft.md, and a CLAUDE.md created once and then left to the bot.

Spec changes

  • Section 19: each Claude Code behavior was checked against the official docs, with the result and the milestone of the real test recorded.
  • Permission rule paths (7.5): absolute paths must be //c/Users/.... A single leading slash is relative to the settings file, so the previous wording would have left the secrets folder readable.
  • Inbox message line (9.2): not documented. The M3 courier must not depend on it before a real test.
  • Single instance (14): lock file per data folder instead of a fixed-name mutex, so a dev daemon can run next to an installed one.
  • Data and protocol: color on bots (mascot avatars, 15.3), slug and handle rules, Origin and hello details, standard JSON-RPC error codes, log level scope.

Not in this PR

  • Archiving a bot does not delete its workspace yet; spec 14 requires stopping the process first, which needs the M2 runtime.
  • CI builds the daemon on Windows only; the Unix fallbacks in platform/ are compiled but not tested.

Test plan

  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (64 tests)
  • pnpm check in app/
  • Generated TypeScript bindings are up to date
  • Owner-only ACL checked against the real Windows DACL (unit test and icacls)
  • Manual run of botloftd serve with BOTLOFT_HOME=.dev\home: health check, second instance refused, WebSocket session from an external client, workspace files on disk, no token or message text in the logs

Needs real Claude Code (M2)

M1 does not start Claude Code, but it writes files whose effect can only be confirmed with a real bot (Claude Code >= 2.1.234):

  1. Start claude in a generated workspace and confirm .claude/rules/botloft.md is loaded (ask it who it is).
  2. Confirm the exec-form hooks run botloftd.exe hook <event> without a shell.
  3. Ask the bot to read owner.token in the secrets folder and confirm the deny rule blocks it.
  4. Confirm the botloft MCP server gets the Authorization header with ${BOTLOFT_BOT_TOKEN} expanded.

Section 19 now lists what the official documentation confirms and which
real test (M2/M3) still has to run. Two corrections came out of it:

- Absolute paths in permission rules need the `//` prefix and the POSIX
  form Claude Code uses on Windows (`//c/Users/...`). A single leading
  slash is relative to the settings file, so the old wording would have
  left the secrets folder readable.
- The inbox message line format is not documented; the courier must not
  depend on it before the M3 real test.
- Prefixed ULID ids (crw_, bot_, msg_, dlv_, tsk_) that validate their
  prefix on parse and deserialize
- Slugs and handles from display names: transliterated, max 32 chars,
  Windows device names avoided, numeric suffix for collisions
- Avatar palette and #RRGGBB validation (spec 15.3)
- Field validation for names, roles and instructions
- M1 protocol types (hello, status, crews and bots) exported to
  app/src/lib/protocol.gen.ts by the export_bindings test; ts-rs is a
  dev-dependency only, and CI fails if the generated file is stale
- WAL, foreign keys and a 5 s busy timeout on every connection (spec 12)
- Numbered migrations tracked in PRAGMA user_version; a database from a
  newer build is refused instead of being modified
- 0001_init: crews, bots and settings. Handles are unique among a
  crew's active bots (partial index); slugs stay unique forever so an
  archived bot's folder is never reused. Bots get a color column for
  the avatar (spec 15.3)
- Crew archiving cascades to its bots in one transaction
- config.toml with the spec 6 defaults; unknown keys are rejected
- BOTLOFT_HOME / %LOCALAPPDATA%\Botloft and %USERPROFILE%\Botloft
  resolution, plus the //c/... form Claude Code expects in permission
  rules
- Owner token: 32 random bytes in secrets\owner.token, only its
  SHA-256 kept in memory, compared in constant time
- platform: protected DACL with a single user SID on Windows (tested
  against the real ACL), 0700/0600 elsewhere; Windows console
  close/logoff/shutdown signals
- Single instance through an exclusive lock file per data folder
  instead of a fixed-name mutex, so a dev daemon can run next to an
  installed one; spec 14 updated
Writes the files of spec 5.1 into <root>\<crew>\<bot>:

- .claude/settings.json: exec-form hooks calling botloftd.exe,
  crossSessionInbound=accept and a deny rule for the secrets folder in
  the //c/... form Claude Code matches on Windows
- .botloft/mcp.json: the local MCP endpoint with the bot token expanded
  from the environment, so it never touches disk
- .claude/rules/botloft.md: identity, role, instructions, shared folder
  and how to use the crew tools; regenerated on every change
- CLAUDE.md: created once and then left to the bot

Every write goes through a temp file and a rename.
- /rpc: JSON-RPC 2.0 over WebSocket. session.hello with the owner token
  must come first (10 s); anything else, a wrong token or another
  protocol version closes the connection. Browser origins other than
  the app get 403; clients without Origin still need the token
- /health for the app's daemon check
- crews.* and bots.* (spec 11.2) run under the store lock, write the
  workspace before the database and broadcast crew.changed and
  bot.changed to every connection; a client that falls 1024 events
  behind is dropped and reloads on reconnect
- Handles follow the bot's name and must be unique among active bots of
  the crew; folders keep their slug. Bots get a palette color unless
  one is given
- Integration tests run a real daemon on an ephemeral port with a
  WebSocket client: the M1 acceptance flow (create a crew and a bot via
  RPC and get a ready workspace), updates, archiving, auth and errors

Spec: color in bots.create/update and the bots table, slug and handle
rules, Origin and hello details, standard JSON-RPC codes.
`botloftd serve [--config <file>]` resolves the data folder, loads the
config, starts daily-rotated logs (14 kept), takes the instance lock,
prepares the owner token and database and listens on 127.0.0.1 until
Ctrl+C or a Windows close/logoff/shutdown signal. log_level applies to
Botloft's crates only; dependencies stay at warn because the WebSocket
stack logs frames at debug and trace.
@httpsphl
httpsphl merged commit 2104941 into main Sep 28, 2026
3 checks passed
@httpsphl
httpsphl deleted the feat/m1-daemon-base branch September 28, 2026 18:07
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