vault, starters, ingress: the daemon locks, templates go, webhooks reach their connector (plan 6.9, 6.10, and fixes) - #92
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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 serveforwards 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 warningsclean 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