feat(emit): astrid-emit — agent-agnostic stdio→bus hook pipe - #842
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces astrid-emit, a lightweight utility designed to bridge stdio streams to the Astrid event bus. By acting as a simple pipe, it enables agent hook processes to publish events to the daemon while delegating trust, validation, and protocol-specific logic to the sage validator. This change simplifies the integration of external agents by providing a stable, minimal interface for event emission. Highlights
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. A pipe for the bus, so small and so neat, / To carry the bytes, and make it complete. / No logic inside, just a path to the core, / Astrid is ready, for hooks and for more. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces astrid-emit, a new agent-agnostic companion binary and crate that reads stdin to EOF, wraps it in a six-field envelope, and publishes it to a specified topic on the event bus. It also adds an --emit-path discovery flag to the main astrid CLI to print the path of this companion binary. A security review of the new code identified a potential Denial of Service (DoS) or Out-Of-Memory (OOM) vulnerability in crates/astrid-emit/src/lib.rs where stdin is read to EOF without a size limit, and suggested enforcing a defensive limit of 10 MiB.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR introduces astrid-emit, a small native helper that reads stdin, wraps it into a fixed sage-validated hook envelope, and publishes it onto the Astrid event bus—enabling agent hook subprocesses to forward events without embedding any agent-protocol semantics in core.
Changes:
- Adds new
crates/astrid-emit(lib + bin) implementing the stdio→bus pipe with a testableEmitterseam and unit tests for the envelope contract. - Co-installs
astrid-emitalongsideastridvia anastridpackage[[bin]]shim, and addsastrid --emit-pathfor side-effect-free discovery. - Updates packaging/release workflow and changelog to ship and document the new companion binary.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/astrid-emit/src/main.rs | Standalone astrid-emit binary entrypoint delegating to astrid_emit::run(). |
| crates/astrid-emit/src/lib.rs | Core implementation: arg/env handling, stdin read, envelope build, publish seam, tests, and “never exit 2” behavior. |
| crates/astrid-emit/Cargo.toml | New crate manifest defining lib + bin and workspace dependencies. |
| crates/astrid-cli/src/emit.rs | CLI-bundled astrid-emit shim binary delegating to astrid_emit::run(). |
| crates/astrid-cli/src/dispatch.rs | Early handling for --emit-path to print the companion binary path before banner/config. |
| crates/astrid-cli/src/cli.rs | Adds the --emit-path flag to the top-level CLI. |
| crates/astrid-cli/Cargo.toml | Adds astrid-emit dependency and new [[bin]] astrid-emit entry. |
| CHANGELOG.md | Documents the new astrid-emit binary and astrid --emit-path. |
| Cargo.toml | Adds crates/astrid-emit to workspace members and workspace dependency list. |
| .github/workflows/release.yml | Ships astrid-emit in release archives and updates install snippet accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Addressed in a223ec2:
Not changed: the
|
Add a tiny native astrid-emit binary (astrid#814) that any agent's hook process shells out to: it reads stdin, wraps it in the fixed six-field sage HookEnvelope {hook, payload, correlation_id:null, principal_id, session_id, token}, connects to astridd as an uplink, and publishes via publish-as on the given topic. Zero agent-protocol knowledge — no hook-name map, no stdin parsing, no verdict shaping, no merge, no fail-closed. Sage is the trust anchor: it validates the per-session token from kernel-namespaced KV and republishes on canonical hook.v1.event.*.
Contract pinned against the shipped sage validator (sage HookEnvelope, spawn-env vars, layout.rs invocation): topic accepted BOTH positionally (sage writes 'astrid-emit <topic>') AND via --topic (issue AC); hook = topic trailing segment (matches sage's tail check); payload forwarded as a verbatim UTF-8 string, never base64; ASTRID_PRINCIPAL_ID/ASTRID_SESSION_ID/ASTRID_HOOK_TOKEN all required.
Always writes {"continue":true} and exits 0 (published) or 1 (any failure incl. malformed argv) — NEVER 2, so a misconfigured hook command can't wedge the agent with a spurious block. Co-installed beside astrid (mirrors astrid-daemon/astrid-build), added to the release matrix, discoverable via 'astrid --emit-path'. 12 unit tests; clippy clean.
Refs #814
- read_stdin_lossy now reads at most 10 MiB (.take(10<<20)) so a large or
unbounded pipe cannot OOM the process. Hook payloads are small JSON
blobs; anything near this is pathological (gemini, security-medium).
- CONTINUE_LINE doc no longer claims 'every code path' unconditionally —
--help/--version are handled by clap and exit before any hook logic, so
the continue line is written on every *hook-invocation* path (Copilot).
NOTE (not changed): Copilot flagged derive_hook returning '' for a
trailing-dot topic. That is a deliberate, documented mirror of the PINNED
`topic.rsplit('.').next()` contract (sage never emits trailing-dot
topics); deviating would break the pinned-mirror guarantee. Left as-is —
flagged for a maintainer decision rather than silently diverging.
a223ec2 to
80973c5
Compare
## Linked Issue Closes #853 ## Summary `chore: release` — bumps all workspace crates **0.7.0 → 0.8.0** and rolls `CHANGELOG [Unreleased]` into `[0.8.0] - 2026-06-10`. Consolidates the ~50 PRs landed since v0.7.0: - **HTTP admin gateway** (#756 + follow-ups) — `astrid-gateway`: principals/caps/quotas/groups/invites/env over HTTP, audit SSE + historical queries, agent-prompt SSE, OpenAPI emission, bus-direct admin path (285× throughput), rustls TLS, CORS, metrics, invite + keypair CLI verbs. - **Runtime concurrency overhaul** (#813/#816/#819/#820) — routed per-(capsule, topic, principal) IPC with DRR fairness, async Wasmtime, dynamic instance pools, split blocking/IO semaphores, per-principal fuel + memory ledgers with enforcement and usage reporting. - **Host process + introspection surface** — `astrid:process` persistent tier (#866/#867/#871/#873), capability introspection (#868/#869), `astrid mcp serve` (#879/#880), `astrid-emit` (#814/#842). - **Security** — macOS 15+ sandbox no longer silently disabled (#855/#857), audit-feed principal scoping (#850/#851), failed-redeem audit rows (#846), `self:agent:list` roster leak (#848), pair-device redeem rate-limit (#844), bearer revocation + wire format v2 (#772). Breaking: `Capsule.toml` `[publish]`/`[subscribe]`-only IPC surface (#858/#864/#865, `[[interceptor]]` / `ipc_*` arrays / `[[topic]]` removed), bearer v2, MSRV 1.95, `astrid-openclaw` removed (#833). ## Changes - `[workspace.package].version` `0.7.0` → `0.8.0` + the 23 in-workspace path-dependency pins (now including `astrid-emit`; `axum`/`axum-server` stay on their external 0.7). - CHANGELOG: `[Unreleased]` → `[0.8.0] - 2026-06-10` with a release **synopsis** (extracted into the GitHub release body by release.yml on tag). Keeps the earlier roll's curation (canonical section order, bullets verbatim) and merges the 13 entries added to `[Unreleased]` by the 12 PRs that landed after the branch was first cut (#851–#885) — verified both directions that no bullet was lost. Branch history note: rebuilt on current `main` (was `CONFLICTING` and 12 commits behind); previous tip was `54c7294`. ## Release steps (maintainer) 1. Merge this PR. 2. Tag `v0.8.0` on the merge commit → release.yml creates the GitHub release from the CHANGELOG block. 3. Publish/distribute the new `astrid` binaries as usual. ## Test Plan Release PR — no code changes. `cargo check --workspace` passes on the rebuilt branch; full test/clippy/MSRV matrix runs in CI. The changelog merge was verified bullet-by-bullet in both directions: every `[Unreleased]` bullet on `main` appears in the rolled `[0.8.0]` section, and every bullet from the earlier curated roll is preserved verbatim.
Linked Issue
Closes #814
Summary
Ships the native
astrid-emitbinary — the core deliverable of #814. It's a tiny, agent-agnostic stdio→bus pipe that any agent's hook process shells out to: it reads stdin, wraps it in the fixed six-field sageHookEnvelope, connects toastriddas an uplink, publishes viapublish-ason the given topic, writes{"continue":true}to stdout, and exits0/1— never2. It carries zero agent-protocol knowledge (no hook-name map, no stdin parsing, no verdict shaping, no merge, no fail-closed). Sage is the trust anchor: it token-validates the event on thesage.v1.hook.*intake topic and republishes on canonicalhook.v1.event.*; the Claude→Astrid hook-name map lives in sage, not core.Changes
crates/astrid-emit(lib + bin): the pipe, plus anEmitterseam so the entire envelope contract is unit-testable without a running daemon.astridvia anastrid-cli[[bin]]shim (src/emit.rs), mirroring theastrid-daemon/astrid-buildprecedent; added to the release matrix (release.yml) and README install snippet.astrid --emit-pathdiscovery flag, handled before banner/config so hook-bridge installers (sage) can resolve the path on a half-configured host without side effects.[Unreleased].Contract (pinned against the shipped sage validator)
{hook, payload, correlation_id:null, principal_id, session_id, token}— matches sage'sHookEnvelopebyte-for-byte.astrid-emit <topic>, what sage writes intosettings.local.json) and via--topic(issue AC); exactly one required.hook= topic's trailing dot-segment (matches sage's tail check);payloadforwarded as a verbatim UTF-8 string (never base64, since sage forwards it opaquely to canonical subscribers).ASTRID_PRINCIPAL_ID/ASTRID_SESSION_ID/ASTRID_HOOK_TOKENall required; missing-or-empty is a soft failure.Test Plan
Automated
cargo test --workspacepasses — 12astrid-emitunit tests (incl. the feat(cli): astrid-emit — sage-validated hook events to the bus #814 AC scenario verbatim) + 192astridCLI testscargo clippy -p astrid-emit --all-targetscleanManual
astrid --emit-path→ prints the co-installed binary path, exit 0ASTRID_PRINCIPAL_ID=alice ASTRID_SESSION_ID=s1 ASTRID_HOOK_TOKEN=tk astrid-emit sage.v1.hook.before_tool_call <<<'{}'(and the--topicform) →{"continue":true}; soft-fail exit 1 when the daemon is down or env is missingastrid-emit --bogus) →{"continue":true}+ exit 1 (never 2);--help/--version→ exit 0Notes for reviewers
main, which includes async per-capsule WASM (feat(runtime): async per-capsule WASM + instance pool + per-principal isolation (foundation for #821–826) #828) and the budget feature (feat(capsule): share the per-principal CPU fuel ledger across all capsules #821–826). TheIpcPayload::RawJson→ bare-object delivery that sage'sserde_json::from_str(&msg.payload)relies on is verified intact post-rebase:to_guest_bytesforRawJsonstill returns the inner value with no type tag (astrid-types/src/ipc.rs), and the dispatcher still deliverspayload.to_guest_bytes()to the guest. No conflicts, no API breakage; full workspace build + tests green.astrid-emitis onPATH, hooks flow end-to-end with zero further sage changes.publish-asfor diagnostics only.Checklist
[Unreleased]