v0.9.2
Fixed
- First-run consent no longer storms a
GrantRequiredfor every ungranted capsule in the caller's view. The dispatch access gate ran the grant-on-use check — and emitted aGrantRequiredsignal — for every ungranted capsule in the caller's view before checking whether the capsule's subscription matched the dispatched topic. A singletools/callon a fresh principal therefore fired a grant prompt for every ungranted view capsule, including all the capsules the call never touched, making first-run consent effectively unconvergeable. The gate now evaluates the cheap, manifest-local interceptor topic-match first (using the samecrate::topic::topic_matchesthe delivery push uses) and engages the per-principal access gate only for a capsule that actually provides an interceptor for the requested topic; a non-matching capsule never reaches the gate. A matching ungranted capsule still fail-closed drops and signals exactly once, and behaviour for granted capsules, interceptor ordering, and non-tool topics is unchanged. Closes #1113. - The MCP shim now resolves every grant a single
tools/callneeds, instead of only the first. The shim resolved exactly onegrant_requiredper call — it elicited consent, re-sent the call once, and passed the re-send's reply straight to the result reshaper — so when the re-sent call tripped the access gate on a different ungranted capsule, that secondgrant_requiredreply was returned to the client as the tool result (empty content,isError: false): a phantom empty success, while the broker kept a pending marker for a prompt that never surfaced. The shim now loops resolve→re-send→re-classify until the reply is grant-free or a bound (MAX_GRANT_RESOLUTIONS = 8, a distro's worth) trips; a present grant signal is never classified as terminal, so the only exits are a grant-free reply or an honestisError(malformed / denied / bound exceeded) — never a fabricated empty success. Ingress, approval-elicitation, grant dedup/marker, and fail-secure deny paths are preserved. Closes #1117. astrid mcp servenow actually pushesnotifications/tools/list_changedwhen a capsule is installed/loaded/unloaded during a live session, so newly hot-loaded tools light up without a session restart. The hot-reload watcher advertisedtools.listChangedbut never delivered a notification: (1) it seeded its baseline on a separate, short-lived uplink and then only ever read, so its own watch uplink was never bound to a principal — and the cli-proxy delivers a principal-stampedastrid.v1.capsules_loadedbroadcast only to bound uplinks, so the watcher was silently starved of every reload signal; and (2) on a reload it re-enumerated the tool set over a freshtools/listround trip that timed out precisely when a reload was in flight (the broker busy with its own describe fan-out), swallowing the push. The watcher now seeds its baseline on the watch uplink itself (that first request binds the connection) and reads the new tool surface directly from thecapsules_loadedpayload (capsules[].meta.tools[].name, principal-filtered) — the signal's documented intended contract — so the notification fires ~2 s after an install with no dependency on a follow-up request. Refs #1118.
Install
From source (requires Rust 1.95+):
cargo install astrid
Pre-built binaries:
Download the archive for your platform, extract, and add to PATH:
tar xzf astrid-*-$(uname -m)-*.tar.gz
sudo mv astrid-*/astrid astrid-*/astrid-daemon astrid-*/astrid-build astrid-*/astrid-emit /usr/local/bin/
Then run astrid init to set up capsules.
With many thanks from the following Astrinauts 🚀
- Joshua J. Bouw