Skip to content

vault, starters, ingress: the daemon locks, templates go, webhooks reach their connector (plan 6.9, 6.10, and fixes) - #92

Merged
radicalkjax merged 6 commits into
mainfrom
phase/lock-templates-and-fixes
Sep 6, 2026
Merged

vault, starters, ingress: the daemon locks, templates go, webhooks reach their connector (plan 6.9, 6.10, and fixes)#92
radicalkjax merged 6 commits into
mainfrom
phase/lock-templates-and-fixes

Conversation

@radicalkjax

Copy link
Copy Markdown
Member

Six pieces in one branch: four recorded defects, then the remaining phase 6 items and two gaps earlier work uncovered.

Four defects that a documentation pass had recorded but not fixed.

  • Guard mode was captured at deploy, so turning it on protected nothing until redeploy. It was worse than documented: a spawned formation built default constraints, so guard was off even after a redeploy. The toggle now reaches the live formation over the same command channel the other operator verbs use, spawning seeds from the stored row, and every read goes through one accessor. A test flips all five guarded verbs on a live formation with no redeploy.
  • A dissolve outcome reported a hardcoded zero failures, in both the gossip record and the durable note. It now reports the lifetime interference count, with a test.
  • The autonomy colour array still had five entries after the labels were cut to four.
  • The 1265-line dispatch module is split into a module directory, one concern per file, as the crate-structure rule requires. Pure move; every public path stays where it was.

The daemon locks instead of being killed (6.10, finding 113). Following the Secret Service model the plan cites, locking drops the key rather than fencing it: chat sources unwire, the scheduler and heartbeat pause, stream tickets clear, tasks are aborted and joined, the vault locks, and the runtime is dropped so the database handle closes. While locked the router serves only health, readiness reporting the locked state, and unlock, which reruns the boot pipeline. A test asserts the store handle is actually dropped, which any flag-based lock would fail. Sessions clear on lock, so a token minted before a lock cannot be replayed after one.

One way to start (6.9, finding 112). The template system is deleted entirely: the operations module, the routes, the command, the initialisation argument, and the interface types. The two scaffolds the recipe catalogue lacked are added as builtin recipes. Documentation follows.

Webhook ingress. The webhook route verified a signature and then hardcoded one connector's field extraction, so no other connector's webhook could produce chat. Connectors now ingest their own verified payloads, implemented for Telegram and Kick, which is what makes Kick chat arrive at all.

A bridge for subprocess clients. springtale mcp serve forwards a client that can only launch a subprocess onto the daemon's authenticated endpoint. It holds no protocol logic.

Plan errors found and worked around, all recorded: the session map the lock item assumed lives elsewhere, the HTTP middleware crate has no rate limiter so the existing pattern was used, the lock cannot live in the runtime crate because it must drop the runtime, unlock has nothing to compare in constant time since it derives a key, and the starter list is fourteen rather than eight, so six were dropped rather than mapped.

One behaviour change to know: boot no longer exits when the bot loop stops, because after a lock there is no bot loop and the daemon must stay up to accept the unlock.

Verification: clippy -D warnings clean across six crates; 24 test binaries green including ten lock tests; the surface check passes at 123 routes; the contract was regenerated the way CI does; frontend typecheck, lint and build pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8

radicalkjax and others added 6 commits September 6, 2026 10:39
…tch splits

Four defects a documentation pass recorded but did not fix.

1. Guard mode was captured at deploy. `toggle_formation_guard` wrote the
   `guard:{formation_id}` config row and nothing else, while enforcement in
   `tick_steps/handle_command.rs::guarded` reads the live formation's
   `constraints.guard_mode` — which `spawn_formation` set from
   `FormationConstraints::default()` (always false) and never refreshed.
   Engaging guard therefore protected nothing, redeploy included. The toggle
   now posts `FormationCommand::SetGuard` on the same channel the other
   operator verbs ride, spawn seeds the live flag from the config row, and
   every reader of that row goes through the new
   `config::formation_guard_engaged`, so row and live constraint cannot
   disagree. The `Recruit` arm's inline guard check now goes through
   `guarded` like the rest.

2. The dissolve outcome reported `failure_count: 0` on both the gossip
   `FormationOutcome` and the durable `OutcomeNote`, which read as "nothing
   ever failed here" and skewed the retrieval scorer's
   `success / (success + failure)` ratio. The real lifetime count is the
   momentum FSM's `interference_total` (`record_interference` bumps it and
   never resets it, unlike the per-run `interference_count` and
   `consecutive_successes`), now reported via `dissolve_failure_count`.

3. The autonomy pip colours in `DetailPanel` still had five entries after
   `AUTONOMY_LABELS` was cut to four; the fifth outlived the SELF-DIRECT
   level. Extracted to `AUTONOMY_COLORS` in `colony/types.ts` with one
   entry per level.

4. Split the 1265-line `dispatch.rs` into `dispatch/` — `mod.rs` (entry
   point + re-exports), `entry.rs`, `step.rs`, `connector.rs`, `chain.rs`,
   `extract.rs`. A move, not a rewrite: `dispatch::dispatch_action` and
   `dispatch::dispatch_actions` stay importable at the same paths.

`docs/guide/formations.md` no longer describes defects 1 and 2 as known.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Recipes are the starter system, so the parallel scaffold generator is
gone rather than kept alongside it. Deleted `operations::templates`,
`springtaled`'s `GET /templates` + `POST /templates/{name}` routes,
`springtale new`, the `template` argument of `springtale init`, the
`Template`/`TemplateFile`/`WriteReport` shared types and the
`listTemplates`/`writeTemplate` items on the frontend `DataProvider`.

The scaffolds map onto what already exists: telegram-bot and
discord-bot are onboarding; cron-runner, github-monitor, llm-assistant
and llm-swarm are recipes; blank-bot and cli-runner are `springtale
init` alone. `github-monitor` and `llm-swarm` were the two the recipe
catalogue lacked, added as builtins in the shape of the rest.

Docs drop `springtale new` throughout; `docs/guide/templates.md` is
deleted. Nothing migrates — this is pre-launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
`api/webhooks.rs` verified a signature through the connector host and
then hardcoded one connector's field extraction to reach the bot, so
Kick's chat could never arrive and every other connector's webhook was
second-class.

The `Connector` trait and the connector host gain an ingest method that
takes an already-verified payload and returns the chat messages and
events it means; the daemon delegates to it and the hardcoded special
case is gone. Telegram and Kick implement it, so Kick's chat source
now receives real traffic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
The MCP server lives in the daemon behind authentication and the old
per-connector subprocess transport is gone, which left editors that can
only launch a subprocess with nothing to launch.

`springtale mcp serve` is that subprocess: it speaks the stdio
transport on its own stdin/stdout and forwards each message to the
daemon's MCP endpoint with the stored token. It holds no protocol logic
of its own — no tool list, no method dispatch, no schema knowledge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
There was no locked state: the only way to protect the vault was to
kill springtaled. Following the Secret Service precedent the plan
cites, locking drops the key rather than fencing it — a flag over a
live runtime is worth nothing against memory capture.

A `RuntimeGuard` (`api/lock.rs`) holds an `ArcSwapOption<Live>` owning
the inner router, its `AppState`, the vault and every task handle. The
served router is an outer one over the guard: `/health`, `/ready`,
`POST /vault/lock` and `POST /vault/unlock` always answer; everything
else forwards into the inner router when unlocked and is refused with
503 when locked. Locking unwires every chat source, pauses the
scheduler and heartbeat, clears the SSE tickets, aborts and joins the
background tasks, locks the vault and drops the last `Live` — then
waits on a `Weak` to the store until nothing holds it, so the SQLite
handle really closes and the key really zeroizes. Unlock re-runs the
crypto and runtime initialisation through the same pipeline boot uses.

Two things had to change for the drop to be real rather than
cosmetic: the runtime's detached tasks each captured a store `Arc`, so
they now go through `springtale_runtime::TaskHandles` and are awaited
on shutdown; and `tower::buffer::Buffer` owns the router from a worker
task, so the release is observed rather than assumed.

The idle timer reads `bot:settings.auto_lock_secs` (default 300, `0`
never), moved from `BotConfig::vault_timeout_secs`, which nothing read
and which is deleted. Idleness is measured from an `ActivityClock`
touched by inbound chat and by API requests that carried a credential
and were not rejected, so bad tokens cannot hold the vault open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
Eleven conflicts, resolved so that the template deletion wins over
main's edits to the template system and main's version wins everywhere
else with this branch's change re-applied on top.

- Templates: `operations::templates`, `api::templates`, `commands/new.rs`
  and main's annotated template handlers, schemas and OpenAPI tag are
  gone, as are the two `/templates` surface exemptions. `Command::New`
  and `Init`'s template argument stay deleted; main's `Login`/`Logout`
  verbs are kept.
- Lock over main's router shape: `pipeline::build_live` picked up main's
  durable heartbeat interval and the new `AppState.sessions` map, and
  the lock now clears that map so a token issued before a lock cannot
  be replayed after it. `POST /vault/lock` authenticates against the
  live session map rather than the passphrase-derived hash, which main
  demoted to a login verifier only (plan 6.6).
- Settings: `auto_lock_secs` sits alongside main's `session_idle_secs`
  and `session_absolute_secs`.

Contract regenerated with `--dump-openapi` and `generate:api`: 6
insertions, 122 deletions in `openapi.json`, the `/templates` routes
being what went. `scripts/check-surface.sh`: 123 routes, 48 CLI paths,
67 provider paths — OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M415coMKAv5V2xJQsaSf8
@radicalkjax
radicalkjax merged commit 3564ea4 into main Sep 6, 2026
30 checks passed
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