Skip to content

feat: make Astrid local admin standalone - #1473

Merged
joshuajbouw merged 13 commits into
mainfrom
codex/standalone-admin
Aug 10, 2026
Merged

feat: make Astrid local admin standalone#1473
joshuajbouw merged 13 commits into
mainfrom
codex/standalone-admin

Conversation

@joshuajbouw

@joshuajbouw joshuajbouw commented Aug 10, 2026

Copy link
Copy Markdown
Member

Linked Issue

Closes #1471

Summary

Makes Astrid's baseline local administrative transport part of Astrid itself, so a clean runtime can start and be managed without installing an AOS or other distribution-provided uplink capsule.

The kernel remains tool- and business-logic-blind: the new native uplink is a typed, authenticated transport boundary owned by the Astrid runtime. Distribution frontends remain optional compositions and cannot compete for the canonical listener or become boot dependencies.

Changes

  • Add the Astrid-owned native Unix-socket uplink, including authenticated handshake, request correlation, typed routing, timeout handling, and orderly shutdown.
  • Start the native uplink during daemon boot and remove the hard dependency on a capsule advertising the CLI socket uplink.
  • Update the native CLI/admin client path and preserve capsule-uplink compatibility for non-canonical transports.
  • Filter client-facing egress at publication time into bounded per-connection queues with a 4 MiB byte budget, demultiplexed by the kernel-stamped principal, device key, and chat session so one principal cannot lag another.
  • Bound established connections globally while reserving eight one-request admin lanes so validated status and stop requests remain available under long-lived connection pressure.
  • Preserve the existing bare payload wire contract, stamp kernel responses and keepalives with their acting principal, bound socket writes, and keep stalled handshakes out of the accept loop.
  • Add clean-home runtime/CLI smoke coverage and update daemon and architecture documentation.
  • Document the change under CHANGELOG.md [Unreleased].

Verification

  • cargo test -p astrid-daemon — 8 tests passed.
  • cargo test -p astrid-kernel — 297 tests passed.
  • cargo test -p astrid-uplink --lib — 43 tests passed, including stalled-handshake isolation, established-connection capacity release, full-frame ordered egress, explicit lag propagation, bounded stream reconciliation, principal isolation, and exact wire-payload regressions.
  • cargo test -p astrid-capsule engine::wasm::host::ipc::audit_scope_tests::publish_inherits_device_key_id_from_caller_context -- --exact passed.
  • cargo test -p astrid-capsule engine::wasm::host::ipc::audit_scope_tests::self_triggered_publish_stamps_no_device_key_id -- --exact passed.
  • cargo clippy --workspace --all-targets --all-features -- -D warnings passed.
  • ./scripts/check-wasm-portability.sh passed for all kernel-semantics crates.
  • Twenty clean-home start, CLI operation, stop, and restart cycles passed before review; the exact-head GitHub runtime sweep remains the final integration gate.
  • An existing AOS CLI capsule remained loadable and ready while staying transport-inert beside the canonical Astrid-owned listener.

AI / Tool Assistance

Assisted-by: Codex:GPT-5.6

Codex assisted with repository analysis, implementation across the daemon, uplink, kernel, and CLI boundaries, regression tests, documentation, review remediation, and local validation. Joshua reviewed the complete change, its architecture, risks, and validation before publication.

Checklist

  • Linked to an issue
  • CHANGELOG.md updated (entry under [Unreleased] — or [Unreleased] rolled into a version section for a release PR; not applicable to docs/CI-only changes)
  • I understand every change in this PR and can explain its design, risks, and validation.
  • I reviewed and tested any meaningful tool-generated output included in this PR.
  • Every non-bot, non-merge commit has a matching Signed-off-by trailer.

Copilot AI balanced review requested due to automatic review settings August 10, 2026 07:57

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

Makes Astrid’s local administrative transport runtime-owned, removing the boot dependency on distribution uplink capsules.

Changes:

  • Adds an authenticated native uplink server with routing and lifecycle handling.
  • Starts it during daemon boot and updates CLI/runtime documentation.
  • Adds capsule-free lifecycle smoke coverage.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/e2e/runtime-harness.sh Runs standalone admin smoke coverage.
scripts/e2e/runtime-cli-smoke.sh Tests capsule-free start/status/stop/restart.
docs/astrid-native-uplink.md Documents the native transport boundary.
crates/astrid-uplink/src/socket_client.rs Exposes handshake helper for crate tests.
crates/astrid-uplink/src/native/routing.rs Adds topic policy and response demultiplexing.
crates/astrid-uplink/src/native/mod.rs Implements the native socket server.
crates/astrid-uplink/src/native/handshake.rs Implements server-side authentication.
crates/astrid-uplink/src/lib.rs Exports and documents native uplink support.
crates/astrid-uplink/Cargo.toml Adds native server dependencies.
crates/astrid-kernel/src/lib.rs Assigns canonical listener ownership.
crates/astrid-integration-tests/tests/gateway_e2e.rs Updates gateway test scope documentation.
crates/astrid-integration-tests/tests/daemon_wedge_e2e.rs Removes obsolete capsule dependency note.
crates/astrid-daemon/src/lib.rs Starts and shuts down the native uplink.
crates/astrid-daemon/README.md Documents the revised boot sequence.
crates/astrid-daemon/Cargo.toml Adds the uplink dependency.
crates/astrid-cli/src/principal.rs Documents handshake-bound identity.
crates/astrid-cli/src/main.rs Updates principal-resolution commentary.
crates/astrid-cli/src/commands/mcp/watch.rs Updates watcher identity documentation.
crates/astrid-cli/src/cli.rs Updates principal option documentation.
crates/astrid-cli/src/admin_client.rs Updates admin identity documentation.
crates/astrid-capsule/src/readiness.rs Documents capsule-free boot behavior.
CHANGELOG.md Records standalone administration support.
Cargo.lock Locks new dependency relationships.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/astrid-uplink/src/native/routing.rs
Comment thread crates/astrid-uplink/src/native/routing.rs Outdated
Comment thread crates/astrid-uplink/src/native/mod.rs Outdated
Comment thread crates/astrid-uplink/src/native/mod.rs Outdated
Comment thread crates/astrid-uplink/src/native/mod.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

Copilot reviewed 27 out of 28 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

crates/astrid-uplink/src/native/handshake.rs:71

  • The CLI has no daemon subcommand; restart is a top-level command. This recovery instruction therefore fails if copied. Use astrid restart, consistent with the CLI definition and the rest of the repository.
            "Protocol version mismatch (client={}, server={}). Restart the daemon with `astrid daemon restart`.",

crates/astrid-uplink/src/native/routing.rs:44

  • The delta and terminal topics are subscribed to separately, and spawn_egress_hub drains each subscription in an independent task. That can reorder a final response ahead of an earlier delta, causing the CLI to stop on the final frame while the late delta remains queued (and also invalidating reconcile_stream's prefix assumption). Use one agent.v1.* routed subscription; the existing egress allowlist will still discard unrelated agent topics while preserving per-principal FIFO order between deltas and the terminal response.
    CHAT_DELTA_TOPIC,
    CHAT_RESPONSE_TOPIC,

Comment thread crates/astrid-uplink/src/native/routing.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

Copilot reviewed 27 out of 28 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

crates/astrid-uplink/src/native/routing.rs:58

  • This treats astrid.v1.onboarding.required as a global broadcast, but its producer runs with a per-principal CapsuleContext and currently publishes without stamping ctx.principal (crates/astrid-capsule/src/engine/mod.rs:265-275). During dynamic loads, one principal's capsule ID and onboarding field definitions are therefore delivered to every connected principal. Stamp the producer with its context principal and remove this topic from the principal-less broadcast allowlist.
    "astrid.v1.onboarding.required",

Comment thread crates/astrid-uplink/src/native/routing.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

Copilot reviewed 28 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/astrid-uplink/src/native/mod.rs:157

  • subscribe_topic_routed is lossy before this hub can apply its principal demultiplexing: routed subscriptions share a 1 MiB budget across all principals, evict oldest queued heads under pressure, and reject a single event when payload bytes + topic length exceeds that budget (astrid-events/src/route/entry.rs:17-21, 212-233). Thus an otherwise valid 1 MiB IPC payload is silently undeliverable, and a burst for one principal can evict another principal's response, leaving that client to time out with no lag signal. Use an egress queue whose bound covers the full allowed frame and whose loss/backpressure accounting is isolated per principal (or propagate route eviction so affected connections are closed/reconciled).
        let mut receiver = event_bus.subscribe_topic_routed(
            uuid::Uuid::new_v4(),
            *pattern,
            EVENT_SOURCE,
            EVENT_SOURCE,
        );

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

Copilot reviewed 28 out of 29 changed files in this pull request and generated no new comments.

Suppressed comments (2)

crates/astrid-uplink/src/native/mod.rs:24

  • The hard eight-connection cap can make the canonical admin transport unavailable under normal use. Each astrid mcp serve holds three long-lived socket connections (the primary client in commands/mcp/mod.rs:132, session guard in session_guard.rs:40, and watcher in watch.rs:84), so three MCP shims can occupy all permits indefinitely. Subsequent astrid status/stop connections then remain unaccepted and time out. Reserve capacity for administrative connections or separate the handshake limit from a suitably configurable established-connection limit.
const MAX_CONNECTIONS: usize = 8;

crates/astrid-integration-tests/tests/gateway_e2e.rs:31

  • This overstates the new smoke coverage: run_standalone_admin_smoke only executes CLI start/status/stop cycles and never starts the HTTP gateway or calls /api/auth/redeem. Please distinguish native-socket lifecycle coverage from the mocked router coverage of the redeem endpoint.
//! * **`/api/auth/redeem` over the real socket.** This in-process fixture does
//!   not spawn the daemon-owned accept loop. Covered by the standalone admin
//!   lifecycle smoke test and by the

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

Copilot reviewed 28 out of 29 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

crates/astrid-uplink/src/native/routing.rs:22

  • The ingress allowlist omits sage.v1.hook.*, but the co-installed astrid-emit connects through SocketClient and publishes those topics (crates/astrid-emit/src/lib.rs:168-197). The native server silently drops the frame after the client write succeeds, so hook invocations report success while no event reaches the bus. Preserve this existing ingress family and add a native-server regression.
const ALLOWED_INGRESS_PREFIXES: &[&str] = &[
    "astrid.v1.request.",
    "astrid.v1.admin.",
    "astrid.v1.elicit.response.",
    "astrid.v1.approval.response.",

crates/astrid-uplink/src/native/mod.rs:265

  • This adds a second trusted LocalSocket stamping boundary, but the security-critical policy documentation in crates/astrid-capsule/src/engine/wasm/host/consent_egress.rs:311-319 still says only the uplink capsule can stamp this origin and that its capability grant is the enforcement boundary. Update that trust assumption to include the native server's signed-handshake verification so future changes audit the actual boundary.
                    trusted.origin = if identity.is_principal_verified() {
                        MessageOrigin::LocalSocket
                    } else {
                        MessageOrigin::System
                    };

Comment thread crates/astrid-daemon/src/lib.rs

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

Copilot reviewed 29 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/astrid-uplink/src/native/routing.rs:96

  • Egress is filtered only by principal, so every authenticated device for that principal receives every response, regardless of the device scope used to authorize the request. This bypasses the fail-closed device attenuation enforced in kernel_router/device_scope.rs:8-54; for example, a use-only device can passively receive the PairToken returned when a full-scope device calls auth.pair.issue (pair_device_handlers.rs:121-150). Route correlated responses only to the requesting connection, or stamp responses with the authenticating device_key_id and require it to match here.
    match message.principal.as_deref() {
        Some(target) if target != principal => return false,
        None if !SYSTEM_BROADCAST_EXACT.contains(&message.topic.as_str()) => return false,
        Some(_) | None => {},
    }
    match outbound_session(message) {
        Some(target) => session == Some(target),
        None => true,

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

Copilot reviewed 29 out of 30 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/astrid-uplink/src/native/routing.rs:149

  • RawJson stream deltas use the established {"session_id": ..., "delta": ...} shape (see crates/astrid-gateway/src/routes/agent/tests.rs:114-126), but this extractor only looks for text. Such deltas are forwarded but never added to the accumulator, so a subsequent full terminal response cannot have the streamed prefix removed and the CLI can print the answer twice. Recognize delta for raw delta payloads (or make extraction topic-aware).
        IpcPayload::RawJson(value) => value.get("text").and_then(|value| value.as_str()),

Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
@joshuajbouw
joshuajbouw requested a balanced review from Copilot and removed request for Copilot August 10, 2026 21:54

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

Copilot reviewed 33 out of 34 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

crates/astrid-uplink/src/native/egress.rs:87

  • Completing a turn removes only the admission guard; the owning ClientQueue.session remains set. If another connection starts a later turn with the same principal/session, the old connection still satisfies should_deliver and receives that turn's deltas and terminal response as well. Associate the turn with its connection and clear that connection's session after its terminal event has been enqueued.
                registry
                    .active_turns
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .remove(&(principal.to_owned(), session.to_owned()));

crates/astrid-uplink/src/native/mod.rs:329

  • The TUI's CancelTurn path sends a second user.v1.prompt for the active session with context.action == "cancel_turn" (crates/astrid-cli/src/tui/mod.rs:544-557). This unconditional begin_turn rejects that frame because the original turn is still active, so cancellation is silently dropped by the native uplink. Recognize the cancellation control payload before new-turn admission and forward it only for the connection's current active session.
    if message.topic.as_str() == routing::CHAT_REQUEST_TOPIC {
        let session = routing::payload_session_id(&message.payload)
            .ok_or("chat request is missing a session ID")?;
        if !receiver.begin_turn(session) {
            return Err("a turn is already active for this principal and session");
        }

Comment thread crates/astrid-uplink/src/native/egress.rs
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
@joshuajbouw
joshuajbouw requested a balanced review from Copilot August 10, 2026 22:05

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

Copilot reviewed 34 out of 35 changed files in this pull request and generated no new comments.

Suppressed comments (3)

crates/astrid-uplink/src/native/egress.rs:278

  • When a chat connection disappears before a final agent.v1.response is published, dropping its subscription removes only the client queue; the (principal, session) entry remains in active_turns. Every retry for that session is then rejected until daemon restart, and repeated abandoned sessions grow this set without bound (the session ID itself can be payload-sized). Release or expire the owned turn on disconnect while ensuring a still-running old turn cannot overlap a retry.
                let mut state = self
                    .state
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                if state.overflowed {

crates/astrid-uplink/src/native/routing.rs:63

  • This admits every astrid.v1.admin.* operation into the eight reserved slots, not just liveness/shutdown. Eight slow admin operations can therefore occupy the entire reserve for up to a minute, making the status/stop requests this reserve is intended to preserve fail admission. Restrict reserved admission to the validated GetStatus and Shutdown variants.
pub(super) fn reserved_admin_response_topic(request_topic: &str) -> Option<String> {
    request_topic
        .strip_prefix("astrid.v1.admin.")
        .filter(|suffix| !suffix.is_empty() && !suffix.starts_with("response."))
        .map(|suffix| format!("astrid.v1.admin.response.{suffix}"))

crates/astrid-uplink/src/native/mod.rs:313

  • For reserved kernel requests, request_id is None, so this treats the first same-topic frame as completion. The kernel emits KernelResponse::Working on that exact topic after five seconds, while KernelClient intentionally skips it and waits for a terminal response; a slow status request will therefore receive one keepalive and then see the reserved connection close. Exclude Working frames from completion matching.
        return false;
    };
    message.topic.as_str() == expected.topic
        && expected.request_id.as_deref().is_none_or(|request_id| {

Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
@joshuajbouw
joshuajbouw requested a balanced review from Copilot August 10, 2026 22:08

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

Copilot reviewed 34 out of 35 changed files in this pull request and generated no new comments.

Suppressed comments (2)

crates/astrid-uplink/src/native/mod.rs:281

  • This branch admits every astrid.v1.admin.* operation into the eight reserved lanes based only on a request_id; it does not deserialize the request or restrict it to liveness operations. Eight arbitrary or unhandled admin requests can therefore occupy the lanes for up to one minute and make the real status/shutdown requests below unavailable—the exact pressure the reserve is intended to survive. Restrict reserved admission to the validated status/shutdown variants (or give those operations a separate reserve).
        .topic
        .as_str()
        .strip_prefix("astrid.v1.request.")?
        .split_once('.')?;
    if correlation.is_empty() || correlation.contains('.') {

crates/astrid-uplink/src/native/egress.rs:141

  • Clearing the subscription's session immediately after enqueueing the final chat event races with delivery. forward_outbound re-runs should_deliver using the now-cleared session (native/mod.rs:509), so the terminal response—and any queued deltas ahead of it—can be discarded when an agent publishes faster than the socket writer. Treat publication-time filtering as authoritative for queued events, or defer clearing the session until the accepted terminal frame has been forwarded.
                *client
                    .session
                    .write()
                    .unwrap_or_else(std::sync::PoisonError::into_inner) = None;

Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>

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

Copilot reviewed 34 out of 35 changed files in this pull request and generated 1 comment.

Comment thread crates/astrid-uplink/src/native/mod.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

Copilot reviewed 34 out of 35 changed files in this pull request and generated no new comments.

Suppressed comments (1)

crates/astrid-uplink/src/native/routing.rs:117

  • Fail closed when a chat frame has no usable session_id. As written, outbound_session returns None for a malformed/raw agent.v1.response or agent.v1.stream.delta, and this fallback delivers it to every connection for the same principal/device, bypassing the advertised chat-session isolation. Require these two chat topics to match an active session; reserve the unconditional path for non-chat events.
    match outbound_session(message) {
        Some(target) => session == Some(target),
        None => true,
    }

Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>

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

Copilot reviewed 34 out of 35 changed files in this pull request and generated no new comments.

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

Copilot reviewed 34 out of 35 changed files in this pull request and generated no new comments.

@joshuajbouw
joshuajbouw marked this pull request as ready for review August 10, 2026 22:53
@joshuajbouw
joshuajbouw merged commit adc1280 into main Aug 10, 2026
40 checks passed
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.

Astrid standalone boot must not depend on a distribution-provided CLI uplink

2 participants