feat(process): implement the persistent-process tier registry#867
Conversation
Bumps the core/wit submodule to the astrid:[email protected] persistent-tier WIT (astrid-runtime/wit#12) and regenerates the host bindings, so the kernel now speaks the full contract. Implements: - SIGSTOP / SIGCONT mapping in ProcessHandle.signal. - The 15 new persistent Host fns as fail-secure stubs matching the WIT's (NOT YET IMPLEMENTED) notes: spawn-persistent -> persist-unsupported; every id-keyed op -> no-such-process (no registry => no id resolves, which also denies a cross-principal existence oracle); list-processes / status-many -> empty. Nothing here can leak or escape. This is the contract-wiring increment: the surface is fixed and the SDK can generate against it. The host-owned PersistentProcessRegistry that makes spawn-persistent / read-logs / stop / status / list actually work (Tier 1, which unblocks the shell capsule's background-process tools) lands next. Workspace builds; clippy clean.
Bumps core/wit to wit@46704fd (review fixes) and updates the stop stub's return type to exit-info; drops the now-unused KillResult import.
Re-point the wit submodule from the pre-merge feature-branch commit (46704fd) to merged origin/main (8946e98). host/ content is byte-identical between the two, so the committed wit-staging/ stays in lockstep — only the gitlink moves. This puts any release on a real, pushed main ref rather than a feature branch that gets GC'd post-merge.
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 the astrid:[email protected] persistent-process tier, enabling background child processes to persist beyond the lifecycle of the stateless WASM instances that spawn them. The changes include updating the WIT contract and providing a stubbed host implementation, ensuring the API surface is available for SDKs while the underlying registry logic is developed in a follow-up. Highlights
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 process lives beyond the pool, Persistence is the golden rule. Though stubs are all we have today, The future path is paved this way. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request wires up the host ABI for the persistent-process tier under the astrid:[email protected] package. It updates the WIT contract to define the persistent tier types and functions, maps the new Stop and Cont process signals to their corresponding OS signals, and implements fail-secure stub methods in the host state. There are no review comments provided, and the changes look clean and well-documented, so I have no feedback to provide.
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 updates the astrid:[email protected] WIT contract to include the new persistent-process tier and wires the corresponding host-side API surface in astrid-capsule as an explicit stub implementation (plus live SIGSTOP/SIGCONT signal mapping), allowing guests/SDKs to compile against the full interface before the real persistent registry lands.
Changes:
- Updated the staged WIT contract for
astrid:[email protected]to include persistent-tier types/functions and new error/signal variants. - Added stub host implementations for the new persistent-tier methods in
astrid-capsule(returningpersist-unsupported/no-such-process/ empty). - Implemented SIGSTOP/SIGCONT mapping in
ProcessHandle.signaland added a changelog entry.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| crates/astrid-capsule/wit-staging/deps/astrid-process/[email protected] | Expands the process WIT contract to include the persistent tier and supporting types/errors. |
| crates/astrid-capsule/src/engine/wasm/host/process/mod.rs | Wires the new persistent-tier host functions as stubs in the WASM host implementation. |
| crates/astrid-capsule/src/engine/wasm/host/process/handle.rs | Adds SIGSTOP/SIGCONT support to the existing signal mapping. |
| CHANGELOG.md | Documents the new (stubbed) persistent-tier host ABI surface under Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace the spawn-persistent / id-keyed stubs with a real host-owned PersistentProcessRegistry, shared across a capsule's pooled instances (cloned into every HostState like ProcessTracker) so a process-id survives instance reset. Implements: spawn-persistent (256-bit base32 id, keyed-BLAKE3-hashed, owner-fallback refused), status/status-many/list-processes, read-logs (drain) + read-since (non-draining cursor), signal/wait/stop/release-process, write-stdin/close-stdin. Per-call (principal,capsule) re-check vs creator; concurrent + retained caps; idle/lifetime/exit-retention reaper; kill-all on unload/shutdown. Linux + macOS (daemon-hard-crash orphan caveat documented). attach + watch remain deferred.
…keyed ops Copilot review on #867: (1) spawn-persistent runs the host_process capability gate BEFORE the owner-fallback / persistence checks, so an ungranted capsule always sees capability-denied (consistent with spawn/spawn-background + the WIT Security-gated header) not persist-unsupported. (2) Every id-keyed persistent op now audits via a new audit_process_id helper logging a non-reversible 8-byte BLAKE3 prefix of the process-id (never the raw token, per the WIT) + op/principal/capsule.
Copilot review on #867 (second pass, on the registry impl): 1. os_pid==0: child.id() can be None (reaped) → killpg(0)/kill(0) would hit the daemon's OWN process group. spawn-persistent now rejects a missing/zero pid (drops the child, kill_on_drop reaps), and send_signal guards pid==0 as defense-in-depth. 2. stdin prelude: enforce the WIT 4 MiB per-spawn cap (too-large) and surface write failures (kill the orphan + fail) instead of swallowing them. 3. concurrent cap is now genuinely SHARED across tiers: spawn-background counts live persistent processes; spawn-persistent subtracts live ephemeral handles — mixing the two can no longer exceed the cap. 4. ProcessInfo.id was always empty, breaking list-processes reattach. The entry now stores the raw id and status/list return it (map still keyed by the BLAKE3 hash; audit still logs only the hash). 5. LogRing backpressure is now all-or-nothing — it never evicts (a crossing push is rejected whole), so framed streams aren't corrupted. Reader reads <= min ring cap so an over-cap chunk can't deadlock. 6. clamp_label caps by BYTES (char-boundary-safe), not chars, so non-ASCII labels can't exceed 128 bytes. Tests added for 4/5/6.
## Summary Adds the ergonomic SDK surface for the **persistent-process tier** of `astrid:[email protected]` (host side: astrid-runtime/astrid#867; contract: astrid-runtime/wit#12), so capsule authors can spawn a background child that **outlives the pooled, stateless instance** that started it — unlike `process::Process`, whose kernel resource is reaped on instance reset. ## Changes - **`contracts` submodule → the merged `astrid:[email protected]` persistent tier** (wit commit `1a06cf4`, the #12 squash). Scoped deliberately to the process package: `1a06cf4` predates #11, so the `interfaces/` bundle (`astrid-sdk/wit/astrid-contracts.wit`) is untouched and the open hook-event-request PR #50 is unaffected (`sync-contracts-wit.sh --check` stays green). `astrid-sys/build.rs` restages `wit-staging/deps/astrid-process/`; the raw `wit_process::*` bindings regenerate. - **`astrid-sdk/src/process.rs` ergonomic layer:** - `Command` gains the persistent-only builder knobs (`label`, `keep_stdin_open`, `overflow`, `log_ring_bytes`, `max_lifetime`, `idle_timeout`, `exit_retention`, `limits`) and a `spawn_persistent()` terminal → `PersistentProcess`. - `PersistentProcess` (keyed by an opaque `ProcessId`): `status`, `read_logs` (drain), `read_since` (non-draining cursor, byte-faithful `LogChunk`), `write_stdin`, `close_stdin`, `signal`, `wait` (bounded), `stop` (consumes), `release` (consumes). - Module fns: `attach(id)` — an id-wrapper so reattach from a later invocation works **without** the host's deferred `attach` resource fn (the first id-keyed call validates ownership) — plus `list` and `status_many`. - New types: `ProcessId`, `ProcessInfo`, `ProcessPhase`, `LogStream`, `LogCursor`, `LogChunk`, `OverflowPolicy`, `ResourceLimits`. `Signal` gains `Stop` / `Cont`. - **`Command::into_wit` updated** for the new `SpawnRequest` fields (a required fix — the WIT bump added 8 fields). ## Not exposed (host-deferred) `watch` / `unwatch` (open publish-authority RFC question — poll via `status` + bounded `wait`) and resource-limit enforcement (`limits` is plumbed through but the host does not enforce it yet). ## Test Plan - [x] `cargo build --workspace` + `cargo test --workspace` green - [x] No new clippy warnings — my diff is `astrid-sdk` (clean); the 2 `astrid-sdk-macros` backtick warnings are pre-existing and untouched - [x] `cargo fmt --all --check` clean - [x] `scripts/sync-contracts-wit.sh --check` passes (interfaces bundle untouched — #50 unaffected)
## Summary Mirrors the Rust SDK and the host `astrid:[email protected]` **persistent-process tier** (host: astrid-runtime/astrid#867; Rust SDK: astrid-runtime/sdk-rust#53; contract: astrid-runtime/wit#12), so JS/TS capsule authors can spawn a background child that **outlives the pooled, stateless instance** that started it — unlike `process.BackgroundProcessHandle`, whose kernel resource is reaped on instance reset. ## Changes - **`contracts` submodule → the merged `astrid:[email protected]` persistent tier** (wit commit `1a06cf4`, the #12 squash). Deliberately the **#12-only** commit (predates #11), so the `astrid:contracts` events bundle (`astrid-contracts.wit` / generated `contracts.ts`) is untouched (`sync-contracts-wit.sh --check` stays green) and unrelated open SDK PRs are unaffected. On `main`, `astrid-build` reads the host WIT **live from `contracts/host/`** (`CANONICAL_WIT_DIR`), so the bump is all ComponentizeJS needs. - **`wit-imports.d.ts`** (hand-written host ABI types): persistent types + free-function declarations on `astrid:process/[email protected]` — `spawnPersistent`, `attach`, `listProcesses`, `status`, `statusMany`, `readLogs`, `readSince`, `writeStdin`, `closeStdin`, `signal`, `wait`, `stop`, `releaseProcess`, `watch`, `unwatch`; `ProcessInfo`, `ProcessPhase`, `LogStream`, `LogCursor`, `LogChunk`, `OverflowPolicy`, `ResourceLimits`; `ErrorCode` gains `no-such-process` / `registry-full` / `persist-unsupported`; `ProcessSignal` gains `stop` / `cont`; `SpawnRequest` gains the 8 persistent fields. - **`process.ts`** ergonomic layer: `spawnPersistent(cmd, args, options)` + the persistent `SpawnPersistentOptions` knobs; `PersistentProcess` class (`status` / `readLogs` (drain) / `readSince` (non-draining cursor → byte-faithful `LogChunkResult`; `logCursorStart()`) / `writeStdin` / `closeStdin` / `signal` / `wait` (bounded) / `stop` (consumes — SIGTERM→grace→SIGKILL) / `release`); module fns `attach(id)` (id-wrapper reattach — works without the host's deferred `attach` resource fn), `listProcesses`, `statusMany`; ergonomic types. `buildSpawnRequest` updated for the new `SpawnRequest` fields (required). - **`index.ts`**: top-level re-exports for the new public types. ## Not exposed (host-deferred) `watch` / `unwatch` (open publish-authority RFC question — poll via `status` + bounded `wait`) and resource-limit enforcement (`limits` is plumbed through but the host doesn't enforce it yet). ## Test Plan - [x] `npm run build` (tsc) green across the workspace - [x] **End-to-end:** the `examples/test-capsule` **componentizes** against the persistent WIT — ComponentizeJS produced a 12.96 MB WASM with **169 host imports**, proving the new host functions wire through the JS build path - [x] `scripts/sync-contracts-wit.sh --check` passes (events bundle untouched) - [x] Git diff scoped to 5 files (contracts pointer + 3 SDK sources + CHANGELOG); no `contracts.ts` drift
…e-stdin WIT The id-keyed persistent write-stdin/close-stdin landed implemented in #867 but were never test-locked and the WIT still tagged them (NOT YET IMPLEMENTED). - Adds a registry test proving the acceptance: write-stdin delivers bytes to a persistent child, a second by-id write (needing only the registry + id, i.e. what a post-reset instance has) still reaches the same child, close-stdin yields a clean EOF exit, over-cap -> too-large, wrong-owner -> no-such-process, post-close -> closed. - Bumps the wit submodule to drop the stale (NOT YET IMPLEMENTED) tags on the id-keyed forms and correct the two persistent-tier banners (registry has landed). Restages wit-staging. The ProcessHandle (ephemeral) form, attach, and watch/unwatch stay deferred and keep their tags.
…e-stdin WIT (#871) ## Linked Issue Closes #870 ## Summary The id-keyed persistent `write-stdin` / `close-stdin` were implemented in #867 (the registry retains the child's stdin pipe host-side, survives pooled-instance reset, 1 MiB-capped, backpressured, ownership-checked, audited) but had **no test**, and the WIT still tagged them `(NOT YET IMPLEMENTED)`. This adds the acceptance test and un-stubs the now-accurate WIT docs. Doc-only on the contract — no signature change. ## Changes - **Test** (`registry_tests.rs`): write-stdin delivers bytes to a persistent child; a second by-id write (needing only registry + id — exactly what a post-reset instance holds) still reaches the same child; `close-stdin` → clean EOF exit; over-cap → `too-large`; wrong-owner → `no-such-process`; post-close → `closed`. - **WIT** (submodule bump → astrid-runtime/wit#14): drops the stale `(NOT YET IMPLEMENTED)` tags on the id-keyed forms and corrects the two `PERSISTENT TIER` "stubbed until the registry lands" banners. Restages `wit-staging`. Signatures unchanged. - The ephemeral `ProcessHandle` form, `attach`, and `watch` / `unwatch` stay genuinely deferred and tagged. - CHANGELOG updated under `[Unreleased]`. ## Test Plan ### Automated - [x] `cargo test --workspace` passes - [x] No new clippy warnings Specifically: `cargo test -p astrid-capsule registry_tests` (the new acceptance test + existing registry tests pass); build green (WIT parses via bindgen). ## Checklist - [x] Linked to an issue - [x] CHANGELOG.md updated under `[Unreleased]`
) The persistent-tier registry landed in astrid-runtime/astrid#867 — the id-keyed write-stdin/close-stdin are implemented (pipe retained host-side, survives pooled-instance reset, 1 MiB cap, backpressure, ownership-checked, audited). Drops the stale (NOT YET IMPLEMENTED) tags on those two funcs and corrects the two PERSISTENT TIER banners that claimed the whole tier was stubbed until the registry lands. Doc-only: signatures unchanged, no wire/binding impact. The ProcessHandle (ephemeral) write-stdin/close-stdin, attach, and watch/unwatch remain genuinely deferred and keep their notes.
## 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 #866
Summary
Implements the persistent-process tier of
astrid:[email protected](merged contract: astrid-runtime/wit#12; design: astrid-runtime/rfcs#22). A capsule can now spawn a background child that outlives the pooled, stateless WASM instance that started it — previously an ephemeralprocess-handleis reaped when its instance resets on return to the dynamic pool, so the splitspawn → read → stoppattern across tool invocations was impossible.The core is a host-owned
PersistentProcessRegistry, cloned into every pooledHostStateexactly like the cancellationProcessTracker, so aprocess-idsurvives instance churn. It owns the child (spawned on the daemon runtime under the samebwrap/Seatbelt sandbox as the ephemeral tier), its per-stream log rings, and its stdin pipe.Changes
witsubmodule → merged main (8946e98); committedwit-staging/ships the contract on the published-crate path (zero drift;host/byte-identical).PersistentProcessRegistry(engine/wasm/host/process/persistent/, split intomod/ring/entry/ids/config+ tests):spawn-persistent— 256-bit host-minted CSPRNGprocess-id, lowercase base32 (doubles as an IPC topic suffix); registry stores only a keyed BLAKE3 hash, never the raw token. Refuses the owner-fallback principal (persist-unsupported).status/status-many/list-processes(non-draining),read-logs(drain) +read-since(non-draining, cursor-addressed, byte-faithfullist<u8>),signal(incl.stop/cont), boundedwait,stop(SIGTERM→grace→SIGKILL, frees the slot),release-process,write-stdin/close-stdin(viakeep-stdin-opencapture).(principal, capsule)vs the recorded creator; unknown / wrong-owner / wrong-capsule / reaped all collapse tono-such-process(no oracle).Child(kill_on_dropbackstop); reaping happens outside the registry lock.HostStategainspersistent_processes, wired through the engine (one registry perWasmEngine, cloned into the pool + the lifecycle/hooks construction sites).die-with-parent); a graceful shutdown / unload reaps. Documented, not gated behindpersist-unsupported.Deferred (honest)
attach(resource-handle materialisation — needs dual-typed resource-table dispatch; the id-keyed ops ARE its documented equivalent).watch/unwatch— host-published lifecycle events raise an OPEN publish-authority question in RFC host_abi;status+ boundedwaitis the working polling alternative.(NOT YET …)items: resource-limit enforcement,cpu-ms/mem-bytes-peak, instance-local pollables.Test Plan
Automated
cargo test --workspacepasses — 1990 passed, 0 failed, incl. 3 new integration tests that spawn real processes through the registry (spawn/status/wait/read + owner-isolation; concurrent-cap reject + stop-reaps + slot-free; non-draining cursorread-since) and pure-logic unit tests for the log ring, cursor, and id minting.astrid-capsule+astrid-hooks, both clean; the pre-existingsys.rs:365/astrid-workspacelints are untouched.Manual
cargo build --workspacegreen;cargo fmt --all --checkclean.Checklist
[Unreleased]