Providers map images to per-backend content blocks, so the block rides
the message seam beside the tool blocks and round-trips through session
checkpoints. Untagged serde keeps old payloads loading; the new variant
sits last so the field sets disambiguate. Exhaustive matches on Content
across the adapters and views gained a mechanical Image arm; sending
images is a later effort.
privacy.rs bound the opened handle in a name only the unix build
used, so `-D warnings` failed the wasm target; the bind is now
`_file` and the unix branch uses it.
The docker images resolve the whole workspace manifest graph before
`cargo fetch`, so they now copy the tools contract with its workspace
root plus the sandbox manifests rhema-code path-depends on, and the
stub loop covers tools/*/ too.
The diagnostics page showed the status job "waiting" while its runs
and the report-now kick died without a trace: any exception before the
first log line was invisible, indistinguishable from a job that never
starts. Every worker now wraps its run — a crash logs its type, message,
and first frame; a cancellation logs as such and rethrows. The screen
gains the report-now kick's own row (a one-time job's empty list is
"never run", not an error) and the tap lands in the log, so the next
stale-row mystery names its cause instead of hiding it.
A stale "last seen" had no client-side answer: a dead worker, a down
claw, and a claw that drops writes all looked identical from the
settings screen. The new page separates them: the WorkManager jobs'
live state (a cancelled schedule renders as "not scheduled"), the two
clocks — the phone's last landed report against the claw's last stamp
— whose disagreement names the broken end, auto-detected system checks
(battery optimization, background restriction, notifications) with
jumps into the exact settings page, a report-now kick, and a 200-line
in-memory flight recorder where every worker run logs its outcome.
One [workspace.lints.rust] table replaces the deny attribute
scattered across every lib.rs, and each crate opts in through
[lints]. The lint now also reaches the integration-test crates,
whose roots gain their one-line docs.
BackendError and WatchShapeError replace the String registry and
watch seams, the tool operations report ToolError, the accessors
drop the get_ prefix, the too-many-arguments allows become
expects, and the llama catalog passes through unchanged.
The last forge content in config.rs was vocabulary, not data:
`ForgeKind` (name/parse/token_env/default_base) and the pure
`resolve_token` helper. By the same rule that moved `Watch` to the
watch table and left `CalendarWatch` in the calendar integration,
the kind belongs to the crate that speaks the forges.
- `rhema-forge-integration` gains `kind::ForgeKind` and
`resolve_token`, with the parse and token-fallback tests moved
alongside — the claw's config tests were testing the integration's
type through a re-export.
- The claw's surfaces import the type from the integration directly;
the crate root re-exports it, so `rhema_claw::ForgeKind` keeps
resolving for everything downstream.
config.rs is now config: credentials, the surface, paths, cadence,
and nothing that knows what a forge or a calendar is.
The seam crate carried every backend behind features — local, client,
server — so a wasm worker linked the axum daemon and a host daemon
linked reqwest it never dialed. The family mirrors the provider family
now, as the design always said it would: rhema-sandbox keeps the
contract, the ShellSandbox adapter, the wire protocol, the seeding
helper, the scripted mock, and — behind the new `spawn` feature — the
process family's shared machinery (exec_util, fsroot); the backends
are sibling crates, rhema-sandbox-local, -docker, -ssh, each dependent
only on the seam; the HTTPS client is rhema-sandbox-http (reqwest
behind its `native` feature, off on wasm, whose worker brings its own
HttpTransport over host fetch); the daemon is rhema-sandbox-server,
and the rhema-sandboxd binary moves with it.
Consumers rewire, semantics don't: rhema-code names its backends
directly; rhema-cloud-agent keeps its transitional re-exports;
rhema-cloudflare-code and rhema-vercel-agent take the HTTP client
reqwest-free and native respectively; the sandboxd image builds
-p rhema-sandbox-server. Cargo paths only — no behavior, no protocol,
no spec change.
The streaming progress seam grows from a bare output callback to
ToolProgress: output chunks as before, plus an optional ToolStatus —
the phase a tool's inner work is in and, when the tool runs model
calls of its own, their cumulative spend and the model they ran on.
The engine stamps the call's id on it and the sink receives it as an
ephemeral ToolStatus event, replacing any earlier one for the call;
status is screen-facing, never history.
The subagent tool is the first emitter: its inner turn's lifecycle
becomes phase transitions (thinking, running, writing output) and a
running token total. Every sink decides what a status is worth: the
cloud wire forwards it, ACP and the claw have no slot and drop it.
The calendar table gets the same contract treatment as every other
data domain: a named route constant, three allowlist rows (GET, POST,
DELETE /v1/calendars/{name}), mock-router arms that enforce the
door's loudest rules — a named calendar, an ics with a URL — and
conformance cases (add, the invalid refused, listed, removed) that
run against the mock and the real daemon alike, so the two cannot
drift. The claw's auth middleware reads the same list, which is how
the rows land there with no claw-side code.
The spec names the doors and the pattern they establish: domains the
daemon reconfigures at runtime live in tables behind their routes,
validation runs at the door, and claw.yaml carries only what boot
needs.
Same move as the watches, and the config's last piece of integration
state goes with it:
- `rhema-calendar-integration` owns the calendar table now
(`store.rs`): SQLite in the shared dispatch database, the pattern
the claw's watch table established. `CalendarWatch` gains the
`validate` the config's boot check used to run — per-kind URL and
credential rules, plus the lead-under-horizon check — so an invalid
calendar is loud at the one door every writer shares.
- `CalendarSource::from_store` reads the table fresh on every detect
pass (the forge source's pattern): a calendar added over the route
is live next tick, a removed one stops polling, no restart in the
story. The static constructor stays for tests.
- `POST/DELETE/GET /v1/calendars` on the daemon: the POST body is the
integration's `CalendarWatch` shape, validated at the table; DELETE
takes `?name=`; the api listing names the door.
- `ClawConfig.calendars` is gone. A config still carrying it is
refused at parse time, like the retired `watches:` seed. The
config-side validation and its tests moved with the type.
The config file now carries only what boot needs: credentials, the
surface, paths, the poll cadence. Everything the daemon reconfigures
at runtime lives in its tables, behind its HTTP doors.
The YAML `watches:` seed was the last piece of forge state the config
file carried. Everything else about watches already lived in the
extension: the table (SQLite, runtime-managed), the HTTP routes
(`GET/POST/DELETE /v1/watches`), the chat tools, the poller that reads
the table fresh every pass. A static seed list on top meant two homes
for one fact — and the seed's own semantics (import-once, ignored
once the table has rows) had to be explained to every reader.
Now the config carries none of it:
- `ClawConfig.watches` is gone, with the `Watch` seed type and the
boot-time validation loop that guarded it. A config still carrying
`watches:` is refused at parse time — `deny_unknown_fields` turns
the retirement into a loud migration, not a silent no-op.
- `Watch` moved to `watch_store` (re-exported from the crate root),
where the row's vocabulary belongs; `validate_watch` keeps guarding
the one door every writer shares — the routes and the tools both
call it, so the moved validation tests test it there.
- `seed_from_config` and the daemon's seeding are gone; the operator
seeds once over HTTP or `watches add`, and the table stays true.
The one guarantee the seed carried that nothing else did — boot
refuses a tokenless watch — goes with it, deliberately: a tokenless
watch now fails at its first poll, loudly, like any other watch the
agent adds at runtime. Boot-time certainty was paying for config
state the daemon no longer owns.
The agent token is deliberately barred from /v1/ingest: the events
are the inbound channel, and the model must not feed it itself. But
the same bar closes the door on every honest integration too — a
sidecar poller with events to hand in had to hold the human token,
which is wider than its job. This adds the missing class: a third
bearer, the ingest token, answering the event door and nothing else.
`ingest_allowed` is the whole credential in one function — one
route row, `POST /v1/ingest` — symmetric with `agent_allowed` and
suite-covered the same way: the mock and the daemon's real router
both answer the new checks (a push succeeds with the contract header;
a watch read and a consent answer are 403). The middleware classifier
gains the third arm, boot refuses an ingest token equal to either
other credential — one value playing two roles silently widens the
scoped one — and the daemon provisions it to tool shells as
`RHEMA_INGEST_TOKEN`, so a tool that only pushes events never sees
the wider agent token at all.
The door is the first half of the extensions story (rhema-tools spec
§8): a self-timed sidecar is now a tier-1 tool with a credential
narrow enough to hold.
The claw is the server half of the client tools. The middleware now
knows two bearer classes: the human token answers every /v1 route as
before; the agent token answers only the routes the contract
allowlists, and only with the contract header at the matching version
— consents, sessions, ingest, and the phones answer it 403, so a
model holding the token cannot prompt-inject its way past the scoping
to answer its own secret asks. Boot refuses the configuration where
agent_token is the human token: the scoping is the safety, and one
value playing both roles silently unscopes everything the shell
holds.
Three new routes write the tables the resident already reads: push to
the notification queue, schedule and cancel tasks the resident's tick
fires — the tools get native-shaped writes without the daemon growing
a second brain. A JSON fallback gives the one error shape everywhere,
unknown routes included. The daemon composes the tools' environment
(RHEMA_URL, RHEMA_TOKEN, the tools' bin dir on PATH) and hands it to
the resident's shell through the agent-tools env layer; with no
agent_token it provisions nothing, exactly as before.
The proof is the contract's conformance suite run against this router
over real HTTP — same answers as the mock, checked at test time.
Desktop notifications are the wave-1 built-in the resident has no
native way to send: the daemon's pushes land in the poll queue, and
this is the program that turns one into an OS notification —
`notify push --title --body [--device]` on the shared plumbing, the
claw's `POST /v1/notifications` on the far end.
The binary is the template for every later client: connection from
the environment the daemon provisions, one contract-headered call,
JSON or error shape, exit code. The e2e test drives the real binary
against a mock claw — a wrong token is a 401 with the code to match,
a consent route is a 403, a good push prints the row — so a packaging
regression cannot pass silently. `skills/` ships the markdown that
teaches the agent the tool exists and what its answers mean.
A client tool is three moves: read the connection from the
environment, make one HTTP call carrying the bearer and the contract
header, and print either the answer as one JSON line or the one error
shape — then exit with the code that says which happened. Common is
those three moves, so no client reimplements them and all clients
fail identically: `Config::from_env` (`RHEMA_URL`, `RHEMA_TOKEN`),
the `Client` call with the contract envelope, `print`/`fail` and
the `ToolError` exit codes.
The HTTP client is `ureq`, blocking, no TLS: the contract speaks to
a loopback or otherwise trusted daemon, and a remote TLS endpoint is
a stated non-goal in v1 — a thin client stays thin.
The client tools need one shared definition of what a tool may do,
and the daemon needs to enforce exactly that definition, not a
hand-copied copy of it. This crate is that definition, pure: no HTTP
client, no process — the allowlist as data, the version header as
constants, the request/response and error types, and the conformance
suite that drives any transport through the checks the contract
promises (the bearer gate, the version handshake, the allowlist's
denials, round-trips through every allowed route, one error shape).
`agent_allowed` is the whole security surface in one function:
method and path segments with `*` and `**`, exact otherwise — a
daemon's middleware asks it and nothing else, so the claw's scoping
and the tools' expectations cannot drift apart. The suite runs
against a mock router in this crate's tests and against the daemon's
real axum router in the claw's; same results is the definition of
implementing the contract.
A multi-stage build shipping the daemon: bash, git, and a CA store in
the runtime so git and tarball seeds work; the base directory is a
volume, the token is a flag with no env default, the port must be
passed because the binary defaults to an OS pick. Smoke-tested against
a real daemon: healthz auth, exec, put/get, session dirs under the
mounted base.
Fold thinking deltas into the turn's stream-ordered entries as a
thinking entry, merged back-to-back the way the stream delivers
them, instead of one flat string rendered above every call. Each
thinking block now sits where it streamed — between the calls it
precedes. Live turns keep their thinking expanded; finished turns
render it collapsed.
phone loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half.
It stays replay-unsafe: a resumed run must not re-queue a control
command or re-retire a device. With this every registry-served tool
spec in the daemon comes from a manifest; only the ToolResolver-style
tools (identity_exec) keep their specs inline.
coding_agent loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-enqueue a coding job
against an instance that is already running it.
ask_secret loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-prompt the human or
re-store the credential the first ask already landed.
attach_file loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe: a resumed run must not re-attach a file the
transcript already carries a card for.
identity_set loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half. It
stays replay-unsafe - a resumed run must not re-write the identity.
identity_save, identity_list, and identity_forget load their specs
from embedded manifests under crates/rhema-claw/tools; the Rust side
keeps only the invoke half. identity_list declares replay_safety:
safe - the store answers with its own redacted records - while save
and forget stay unsafe: a resumed run must not re-store an account or
drop one twice. identity_exec keeps its spec inline; it is a
ToolResolver, the exec seam itself, not a registry tool.
A failed post returns retry(), which moves the next run onto
WorkManager's exponential backoff — 30s doubling, capped at five hours.
A couple of failures while the claw is unreachable is enough to leave
the fleet without a report for most of a day, which reads exactly like
the phone being gone.
The snapshot is only saved when every post landed, so the next
scheduled run re-derives the whole transition set and posts it again.
The fifteen-minute schedule is the retry; return success and keep it.
The fleet card reads last-seen from the claw, and only a landed report
moves it. Periodic work is deferred while the device idles — Doze
stretches the fifteen-minute floor to hours — so a phone sitting in the
human's hand could show itself as "last seen 3h ago" with nothing wrong
on the claw.
Kick a status run on resume, the same way the commands worker already
is, and reload the fleet whenever a run lands. The landed-run signal is
a new clock in StatusRepository, written in the same edit that saves the
snapshot (which only happens once every post of the run succeeded), so
the card reloads on a report that actually reached the claw.
notify loads its spec from an embedded manifest under
crates/rhema-claw/tools; the Rust side keeps only the invoke half.
The drift test pins the shipped set: the manifest loads against its
builtin, the registry serves the manifest spec, and notify stays
replay-unsafe - a resumed run must not re-send a notification.
watch_repo, unwatch_repo, and list_watches load their specs from
embedded manifests under crates/rhema-claw/tools; the Rust side keeps
only the invoke half. The manifests stay name-sorted, which is the
order the registry serves and the roster shows. list_watches declares
replay_safety: safe - re-reading the table on a resumed run is free -
and the drift tests pin the shipped set: every manifest loads against
its builtin, every builtin is named by a manifest, and replay safety
reaches the resolver from the file.
bootstrap_save now loads its spec from an embedded manifest, one
folder under crates/rhema-claw/tools; the Rust side keeps only the
invoke half, behind the run.builtin id. Drift tests follow in the
registry's own suite shape once the family grows; for now the
registry asserts the manifest loads, and the render tests exercise
the manifest-served spec through the roster.
The section-header restructure renamed the onboarding offer from
"## Setup" to "## Onboarding" in ONBOARDING.md; three prompt
assertions still looked for the old heading and failed. They follow
the file now.
The crate was missing from the per-member manifest COPY list, so the
workspace failed to resolve in the registry-cache layer: rhema-claw
path-depends on rhema-transcript, whose Cargo.toml never landed in
the context.
schedule_task, list_tasks, and cancel_task load their specs from
embedded manifests, one folder per tool under crates/rhema-claw/tools;
the Rust side keeps only the invoke half. list_tasks declares
replay_safety: safe, so a resumed run may re-execute it. Drift tests
pin the shipped set: every manifest loads against its builtin, every
builtin is named by a manifest, and the registry serves the manifest
specs verbatim.
First landed flag-gated: the mechanics were unproven at coding-traffic
scale and the view changes what the model sees past the budget. The
claw then ran every surface on it and nothing fell over, the only user
is the deployment's own, and the sessions most likely to hit the
budget were the ones least likely to find a flag. Below the budget the
decorator is a pass-through, so the default's only always-on cost is
the zoom spec in the prompt.
transcript: None is the off switch; rhema-code spells it
--no-transcript, the claw's transcript: false now writes the resolved
value so false still means off. Saved sessions view fine: the log
stays complete and the view derives per request. The system preview
names zoom too, so what is previewed is what turns send.
The claw's sessions never end by design — one interactive chat, forge
threads alive as long as their pull requests, coding jobs stashed
across runs — so off-by-default would mean the view is never on where
it matters most. On: chat turns wrap the provider seam with the view
and join zoom to the resolver chain, the mirror living on the handler
so it survives the per-dispatch agent rebuild; forge threads and
coding jobs set the coding agent's transcript field. The config turns
it off with transcript: false.
CalendarWatch, the client resolution, and the slot policy were all
feed domain living in the claw's config and source tree; only the
extension adapter was the claw's. The reminder source now ships in
the integration as the default answer to "what does a start time
mean": one slot per lead time, calendar:<name> keys, fed to the
consumer's dispatcher. The crate doc drops its no-policy claim to
match, and the claw's resident prefixes take the calendar prefix
from the integration so the spelling cannot drift.
The triage mapped the forges' notification reasons onto the claw's
verdicts, but it co-evolves with the vocabulary, not with the claw:
a reason that enters it (Gitea's collapsed shape) needs a verdict in
the same change, and its tests pin the mapping against the
vocabulary's own types. It now lives in the integration crate as the
default classification; a consumer may reclassify on top. The claw
imports it like the rest of the forge seam.
The daemon named every feature twice: once in the interactive
handler's sixteen-argument constructor, once across the source list,
the tick, and the boot pass. Both assemblies are now loops over one
ClawExtension trait, so an integration is a struct plus a line in
the standard set. The chain order, the detect order, and the boot
order are unchanged; the surface suite pins them. Each extension
struct is the boundary a later move into the integration crates
would lift wholesale.
rhema-code grew a path dependency on the new rhema-subagents crate, but
the image's manifest-only staging layer never copied its Cargo.toml, so
workspace resolution died before cargo fetch could run.
The credential-resolution commit landed with two violations this
editor's format-on-save caught — an unsorted import in the handler and
wrapping in the daemon and source — reformatted here so the tree is
cargo-fmt clean.
A repo watch row carries no account link: its only credentials were the
token stored on the row and the forge's environment variable, so a
watch added without either polled — and replied — anonymously. Against
a private repository that is a 404 on every pass, and nothing ever
lands: no dispatch, no reply, only a per-pass warning in the logs.
The watch credential now resolves in three tiers: the row's token,
then the env var, then the token-kind account identity of the same
forge — instance-scoped on a self-hosted forge (the gitea watch of
git.example borrows the gitea account saved with that base URL). A
password-kind account is skipped, never borrowed. ForgeSource and the
ClawHandler resolve through the same chain and key their client caches
on the resolved value, so the reply authenticates as the identity the
poll did — and a rotation or a later-saved account is a new client on
the next pass, no restart.
A saved account whose forge notifications are not actually followed was
invisible: the save's watch flag defaults to false on the tool and the
HTTP route, and GET /v1/watches listed only the repo-watch table. The
first question a silent forge thread raises — is my account even
subscribed? — was unanswerable from outside. The route now answers it.
Gitea's notifications feed names no per-row reason, so the adapter
collapses every row under one fixed NotificationReason::Other spelling
— and the claw's triage mapped every Other to Surface: the notification
reached the device queue and never a dispatch. A comment on a thread
the account authored could therefore never wake the agent on a
reason-less forge, whatever the watches said.
The adapter now publishes that spelling as gitea::COLLAPSED_REASON —
it is the seam's contract, not a detail — and the triage treats it as
the known shape it is: a notification on a thread of the account's own
dispatches the implementer. A spelling no adapter owns still surfaces;
an unknown shape remains the user's to judge, never the agent's to
guess at.
The stored `colors` key names a palette — rhema (the default,
Jadeite), aquamarine, black-and-white, or dracula — and the
Appearance page of /settings cycles it live, like the theme and
icon toggles beside it. Each palette carries a dark and a light
appearance under the existing `theme` key; dracula's light side is
derived, the official palette ships only dark.
The appearance decision moves off token comparison: Theme now
answers is_dark() from the ground/text luminance, so palettes
beyond the original two get the right per-profile fallback tones.