Skip to content

feat(emit): astrid-emit — agent-agnostic stdio→bus hook pipe - #842

Merged
joshuajbouw merged 2 commits into
mainfrom
feat/astrid-emit
Jun 5, 2026
Merged

feat(emit): astrid-emit — agent-agnostic stdio→bus hook pipe#842
joshuajbouw merged 2 commits into
mainfrom
feat/astrid-emit

Conversation

@joshuajbouw

Copy link
Copy Markdown
Member

Linked Issue

Closes #814

Summary

Ships the native astrid-emit binary — 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 sage HookEnvelope, connects to astridd as an uplink, publishes via publish-as on the given topic, writes {"continue":true} to stdout, and exits 0/1never 2. 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 the sage.v1.hook.* intake topic and republishes on canonical hook.v1.event.*; the Claude→Astrid hook-name map lives in sage, not core.

Changes

  • New crates/astrid-emit (lib + bin): the pipe, plus an Emitter seam so the entire envelope contract is unit-testable without a running daemon.
  • Co-installed beside astrid via an astrid-cli [[bin]] shim (src/emit.rs), mirroring the astrid-daemon/astrid-build precedent; added to the release matrix (release.yml) and README install snippet.
  • astrid --emit-path discovery flag, handled before banner/config so hook-bridge installers (sage) can resolve the path on a half-configured host without side effects.
  • CHANGELOG entry under [Unreleased].

Contract (pinned against the shipped sage validator)

  • Six-field envelope {hook, payload, correlation_id:null, principal_id, session_id, token} — matches sage's HookEnvelope byte-for-byte.
  • Topic accepted both positionally (astrid-emit <topic>, what sage writes into settings.local.json) and via --topic (issue AC); exactly one required.
  • hook = topic's trailing dot-segment (matches sage's tail check); payload forwarded as a verbatim UTF-8 string (never base64, since sage forwards it opaquely to canonical subscribers).
  • ASTRID_PRINCIPAL_ID / ASTRID_SESSION_ID / ASTRID_HOOK_TOKEN all required; missing-or-empty is a soft failure.

Test Plan

Automated

Manual

  • astrid --emit-path → prints the co-installed binary path, exit 0
  • ASTRID_PRINCIPAL_ID=alice ASTRID_SESSION_ID=s1 ASTRID_HOOK_TOKEN=tk astrid-emit sage.v1.hook.before_tool_call <<<'{}' (and the --topic form) → {"continue":true}; soft-fail exit 1 when the daemon is down or env is missing
  • malformed argv (astrid-emit --bogus) → {"continue":true} + exit 1 (never 2); --help / --version → exit 0

Notes for reviewers

Checklist

  • Linked to an issue
  • CHANGELOG.md updated under [Unreleased]

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • New astrid-emit binary: Introduced a tiny, agent-agnostic stdio-to-bus pipe that allows agent hook processes to publish events to the Astrid daemon without requiring protocol-specific knowledge.
  • Discovery flag: Added the astrid --emit-path flag to the CLI, enabling hook-bridge installers to reliably locate the co-installed companion binary.
  • Infrastructure integration: Integrated the new crate into the workspace and release matrix, ensuring it is co-installed alongside the main astrid binary.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/release.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/astrid-emit/src/lib.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 testable Emitter seam and unit tests for the envelope contract.
  • Co-installs astrid-emit alongside astrid via an astrid package [[bin]] shim, and adds astrid --emit-path for 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.

Comment thread crates/astrid-emit/src/lib.rs
Comment thread crates/astrid-emit/src/lib.rs Outdated
@joshuajbouw

Copy link
Copy Markdown
Member Author

Addressed in a223ec2:

  • stdin DoS (gemini, security-medium): read_stdin_lossy now caps the read at 10 MiB (.take(10 << 20)) so a large/unbounded pipe can't OOM the process.
  • doc (Copilot): CONTINUE_LINE doc no longer claims 'every code path' unconditionally — --help/--version exit (via clap) before any hook logic, so the line is written on every hook-invocation path.

Not changed: the derive_hook trailing-dot-empty case Copilot raised 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, so I left it as-is and noted it for a maintainer decision.

cargo test -p astrid-emit green; clippy + fmt clean.

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.
@joshuajbouw
joshuajbouw merged commit de474f7 into main Jun 5, 2026
13 checks passed
@joshuajbouw
joshuajbouw deleted the feat/astrid-emit branch June 5, 2026 16:20
@joshuajbouw joshuajbouw mentioned this pull request Jun 10, 2026
joshuajbouw added a commit that referenced this pull request Jun 10, 2026
## 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.
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.

feat(cli): astrid-emit — sage-validated hook events to the bus

2 participants