Skip to content

feat(capsule): inbound TCP bind (bind_tcp) + per-principal run-loop resources + concurrent workers - #1321

Closed
jvsteiner wants to merge 3 commits into
astrid-runtime:mainfrom
jvsteiner:feat/capsule-bind-tcp-on-main
Closed

feat(capsule): inbound TCP bind (bind_tcp) + per-principal run-loop resources + concurrent workers#1321
jvsteiner wants to merge 3 commits into
astrid-runtime:mainfrom
jvsteiner:feat/capsule-bind-tcp-on-main

Conversation

@jvsteiner

Copy link
Copy Markdown

Capsule inbound TCP bind (bind_tcp) + per-principal resources for run-loop capsules + concurrent run-loop workers for TCP-server capsules.

What & why

Enables a capsule to bind an inbound TCP port and serve it from its run loop — the runtime primitive the srouter capsule needs (Anthropic-Messages loopback ingress on 127.0.0.1:8788). Three commits:

  1. feat(net): implement capsule inbound TCP bind (bind_tcp) — the host-side bind_tcp + manifest net_bind capability + manifest_gate enforcement.
  2. feat(capsule): deliver per-principal resources to run-loop capsules — run-loop capsules receive the owner principal's resources (overlaps the run-loop-KV gap tracked in Run-loop capsule KV writes land in the neutral store instead of the owning principal's KV #1197).
  3. feat(capsule): concurrent run-loop workers for TCP server capsules — a run-loop capsule can serve concurrent connections.

Rebase note

Originally authored on old main 6ea3489; cleanly cherry-picked onto current main (41 commits later) with zero conflicts — git auto-followed the capabilities.rs relocation (astrid-capsuleastrid-capsule-types).

Verification

  • cargo check -p astrid-capsule clean.
  • cargo test -p astrid-capsule --lib578 passed, 0 failed.

Draft pending author review of the replay onto the reorganized tree.

🤖 Generated with Claude Code

@joshuajbouw joshuajbouw added the newcomer-approved Maintainer has approved this new contributor's PR label Jul 24, 2026
@joshuajbouw

Copy link
Copy Markdown
Member

@copilot Are you able to add this user to the contrib CI check? Or can you make a new PR doing that? I think that we should remove contrib check for now and maybe bring it back in case we get too many fly by PRs, but at that point likely will restrict PRs.

joshuajbouw added a commit that referenced this pull request Jul 24, 2026
## Linked Issue

No linked issue. This CI-only maintenance was requested to unblock PR
#1321.

## Summary

Temporarily disables the newcomer approval contributor gate and exempts
workflow-only pull requests from the linked-issue requirement.

## Changes

- add an `ENFORCE_NEWCOMER_APPROVAL` repository-variable toggle,
defaulting to
  disabled
- preserve the existing `newcomer-approved` enforcement logic behind
that
  toggle
- exempt PRs whose changed files are all under `.github/workflows/` from
the
linked-issue gate in both PR-template validation and the dedicated check
- avoid running the full Rust CI matrix for changes to unrelated
workflows

## Verification

- `git diff --check`
- `actionlint .github/workflows/pr-checks.yml`
- confirmed #1359 changes only `.github/workflows/ci.yml` and
  `.github/workflows/pr-checks.yml`

## Checklist

- [x] Workflow-only traceability exception documented
- [x] CHANGELOG not required for CI-only changes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Joshua J. Bouw <jjb@unicity-labs.com>
jvsteiner and others added 3 commits July 24, 2026 17:25
Fills in the daemon's stubbed astrid:net bind_tcp host fn so a capsule can
bind a loopback TCP listener and accept inbound connections — the missing
substrate for capsule-hosted HTTP servers (e.g. an Anthropic-Messages shim
Claude Code points ANTHROPIC_BASE_URL at, routed by srouter).

Design:
- Authorization reuses the existing `net_bind` manifest field, whose own
  doc already reads "Unix/TCP socket bind addresses". TCP entries are
  `host:port` / `host:*` patterns matched with the SAME semantics as
  net_connect; a `unix:*` entry (the CLI proxy) never matches a TCP
  host:port, so the two socket families share the field without
  cross-authorizing. New gate method `check_net_tcp_bind(capsule, host,
  port)`, fail-closed default in the trait, allowlist match in
  ManifestSecurityGate.
- Host fn `bind_tcp`: capability-gate → loopback-confinement rail →
  tokio bind → resource-table slot. Loopback-only is enforced host-side
  (is_loopback_bind_host) regardless of the allowlist, mirroring how
  connect_tcp runs its is_safe_ip airlock AFTER the capability gate.
  Non-loopback bind is refused (AirlockRejected), not downgraded.
- TcpListenerSlot now holds the live Arc<tokio::net::TcpListener>.
  accept / poll_accept register the accepted stream as a NetStream::Tcp —
  the SAME representation outbound connect_tcp uses — so every existing
  read/write/peek/timeout host fn works on accepted connections with no
  extra wiring. Per-capsule MAX_ACTIVE_STREAMS cap applies; accept sets
  recv_yielded so a bound accept-loop is not epoch-trapped as a spinner;
  cancellable so capsule unload wins over a blocked accept.

Proven: a probe capsule bound 127.0.0.1:8799, accepted a curl connection,
and served an HTTP 200 through the patched daemon (LISTENING → ACCEPTED →
wrote 110 bytes). 6 new unit tests (gate host:port matching incl. the
unix-entry-doesn't-authorize-TCP case; loopback host classification).

POC for the Astrid router work — informs the upstream feature request
(gotchas + security posture documented separately).
An autostarted #[astrid::run] capsule drives its `run` export directly,
bypassing invoke_interceptor, so it never received the per-invocation
context that path installs: the operator env overlay, secret store, and
home:// fs all failed (manifest-declared env keys arrived empty, home://
denied). Only bus-invoked capsules (carrying an inbound principal) got them.

Install the owner (ctx.principal) resource context once on the run Store's
HostState before the run task spawns — load_invocation_env_overlay +
install_principal_overlays — mirroring what a bus invocation from the owner
installs. caller_context is deliberately left None so an inbound ipc::recv
can still scope per-publisher and effective_principal() keeps resolving the
owner. No regression: absent config falls back to the neutral floor exactly
as before.

Fixes the run-loop half of astrid-runtime#1224.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A run-loop (#[astrid::run]) capsule hosting a loopback TCP server was pinned
to a single Store, so it handled requests serially — parallel clients (e.g.
Claude Code subagents) queued behind each other.

Add `bind_workers` (CapabilitiesDef): a run-loop capsule declaring net_bind
and no host_process runs N worker Stores. Each executes `run()` and shares
ONE bound listener via a shared registry (Approach B), blocking on accept()
— the OS accept queue load-balances. SO_REUSEPORT was rejected: it does not
load-balance on macOS (delivers every connection to the most-recent bind).

- capabilities.rs: bind_workers: Option<usize> (default None => 1 worker).
- discovery.rs: promote bind_workers in the component->root capability merge
  (the field-by-field merge silently dropped the new scalar field otherwise).
- host_state.rs: shared_listeners registry, cloned into each worker HostState.
- host/net/mod.rs: bind_tcp dedupes onto the shared Arc<TcpListener> (first
  worker binds under the shard lock; siblings clone) — EADDRINUSE-safe on macOS.
- mod.rs: build N worker Stores, per-worker context install (ready_tx /
  interceptor auto-subscribe / owner overlay), spawn N run tasks; run_handles
  and ready_rxs become Vecs; wait_ready awaits all N; unload aborts all N.
  Interceptors + workers>1 is forced to 1 with a warn (N subscriptions would
  double-process events). N=1 is byte-identical to prior behavior (no regression).

Verified: 5 concurrent requests handled in parallel (~2.5s each, vs serial
2/4/6/8/10s); 8 workers spawn; clean teardown across daemon restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joshuajbouw

Copy link
Copy Markdown
Member

Review outcome: hold as a draft. This combines inbound TCP bind, per-principal run-loop resources, and concurrent workers across a large change surface. Please split the concerns or provide independent security and integration evidence for each, including the live/replay validation called out in the description. I have not approved the currently gated workflows while it remains in this state.

@jvsteiner

Copy link
Copy Markdown
Author

Retiring this PR in favour of focused, independently-reviewable PRs, per the review feedback. The three bundled concerns are being split:

  1. Per-principal run-loop resources → already extracted and under review as fix(capsule): install owner per-principal overlays before the run export (#1197, #1224) #1380 (fix(capsule): install owner per-principal overlays before the run export).
  2. Inbound TCP bind (bind_tcp) → tracked by feat(net): implement capsule inbound TCP bind (bind_tcp) — currently a CapabilityDenied stub #1230. This needs re-implementation against current main: the outbound-TCP work (feat(net): outbound TCP host fns (full std::net::TcpStream parity) #746) has since restructured the net module (net.rs → a net/ submodule with handshake.rs/stream.rs), so the original commit no longer cherry-picks cleanly — it's a rebase-and-integrate, not a mechanical extraction. It'll come back as its own PR closing feat(net): implement capsule inbound TCP bind (bind_tcp) — currently a CapabilityDenied stub #1230.
  3. Concurrent run-loop workers → tracked by feat(capsule): concurrent workers for run-loop TCP-server capsules (single-Store run-loop serializes requests) #1231. Depends on the per-principal run-loop resources (fix(capsule): install owner per-principal overlays before the run export (#1197, #1224) #1380), so it lands on top of that once fix(capsule): install owner per-principal overlays before the run export (#1197, #1224) #1380 merges (a ~615-line engine/wasm/mod.rs change — worth its own focused review).

Closing so review effort focuses on the split PRs. The original bundled branch feat/capsule-bind-tcp-on-main is preserved for reference.

@jvsteiner

Copy link
Copy Markdown
Author

bind_tcp is now up as its own focused PR: #1457 (closes #1230). Concurrent workers (#1231) still to follow on top of #1380.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

newcomer-approved Maintainer has approved this new contributor's PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants