Skip to content

feat(http): operator-configurable [http] host limits + follow-up fixes#1054

Merged
joshuajbouw merged 5 commits into
mainfrom
feat/http-config-knobs
Jun 24, 2026
Merged

feat(http): operator-configurable [http] host limits + follow-up fixes#1054
joshuajbouw merged 5 commits into
mainfrom
feat/http-config-knobs

Conversation

@joshuajbouw

Copy link
Copy Markdown
Contributor

Linked Issue

Closes #1053

Summary

Makes the seven hardcoded astrid:http host limits operator-tunable via a new global [http] config section, and folds in three follow-up review fixes from #1050. Builds on the now-merged astrid:http@1.1.0 host work (#1050); defaults equal today's constants, so an absent [http] section changes nothing.

Changes

[http] operator config (astrid-config → daemon → kernel → HostState, mirroring [security.capsule_local_egress]):

  • New HttpSection (7 knobs: default_timeout_secs, stream_connect_timeout_secs, stream_read_timeout_secs, header_deadline_secs, max_redirects, max_concurrent_streams, max_response_bytes) → resolved by the daemon into a typed HttpLimits → stored on the kernel → snapshotted onto every HostState (global, like the runtime concurrency limits). The http host reads its defaults/ceilings off HostState.http_limits instead of module constants.
  • Operator-only: merge::restrict reverts the whole [http] section to the operator baseline if a workspace/project layer touches it — untrusted project config cannot widen the host's HTTP limits.
  • Can only lower: a per-request request-options may tighten within the ceiling; a configured ceiling never lets a caller exceed it; max_response_bytes is additionally hard-clamped to MAX_GUEST_PAYLOAD_LEN.
  • Added to config show + defaults.toml (documented, commented).

Follow-up fixes from #1050 review (separate commit):

  • Honour an explicit total-ms equal to the configured default on the streaming path (explicit-flag instead of value-equality).
  • Simplify classify_redirectredirect_target_blocked (IP-literal airlock only); the manual-follow loop already owns the hop ceiling.
  • Map genuine DNS-resolution failures to the typed DnsError (a dns_failed resolver flag mirroring the airlock tripped channel; airlock takes precedence). TLS error typing left as a documented TODO — rustls is transitive (hyper-rustls), so there's no stable typed error to downcast to without a brittle string matcher.

Verification

  • cargo build --workspace, cargo clippy --workspace --all-features -- -D warnings (0), cargo fmt --all --check: green on the rebased branch. 45 http tests + 104 astrid-config tests pass locally.
  • New tests: defaults reproduce the constants; config show surfaces the section; the operator-only merge guard rejects a workspace override; and config actually changes behaviour (max_concurrent_streams=2 → 3rd stream Quota; default_timeout_secs=5 drives the resolved default; max_redirects=3 clamps a larger caller value). Plus regressions for each of the three follow-up fixes.
  • Operator-only boundary reviewed adversarially: test_http_section_cannot_be_set_by_workspace proves a workspace raising max_redirects=100/max_concurrent_streams=64/max_response_bytes=1GiB is fully reverted to the operator baseline.

Checklist

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

…ble via [http] config

The astrid:http host had seven hardcoded tunable constants (timeouts, redirect
and stream caps, buffered-body ceiling). A new [http] config section makes each
operator-configurable, defaulting to today's exact constants so an absent
section changes nothing.

The daemon resolves [http] once into a typed HttpLimits (seconds pre-converted
to Duration off the request hot path), the kernel stores it as one global value
and forwards it unmodified to every capsule's WasmEngine via the loader, and it
is snapshotted onto every pooled HostState at load. The request path reads its
defaults and ceilings off HostState.http_limits instead of module constants: a
per-request request-options value may still tighten a limit but a configured
ceiling can only lower the bound, never let a caller exceed it. max_response_bytes
stays hard-clamped to the shared absolute MAX_GUEST_PAYLOAD_LEN host payload limit.

Operator config only: a project/workspace layer cannot set or widen [http]
(merge::restrict reverts the whole section to the operator baseline), mirroring
[security.capsule_local_egress].
…map DNS errors

Three Copilot review findings on the astrid:http@1.1.0 host, each with a
regression test that fails without the fix:

A. total-timeout explicitness. The streaming path cleared the total when it
equalled the configured default (a value check), so a caller who EXPLICITLY set
timeouts.total-ms to a value equal to the configured default had their deadline
silently cleared. Track explicitness (total_explicit) instead of value: set true
in from_options only when the caller passed total-ms; the streaming path clears
the default total only when !total_explicit. An explicit total equal to the
default is now honoured.

B. dead redirect branch. The manual-follow loop already owns the hop ceiling
(TooManyRedirects when redirect_count >= opts.max_redirects), and only checked
classify_redirect == Block, so the Stop arm and prior_hops/max_redirects
threading were dead and misleading. Replaced classify_redirect (3 args, 3-variant
enum) with redirect_target_blocked(host) -> bool doing ONLY the per-hop IP-literal
airlock check. No behavioural change; removes the dead RedirectAction enum and the
ceiling param.

C. typed DnsError. reqwest collapses a dns_resolver failure into an opaque
is_connect() error, so a resolution miss was indistinguishable from a dead server.
Mirror the airlock tripped channel: thread a second dns_failed Arc<AtomicBool>
into SafeDnsResolver, set it on a genuine NXDOMAIN/no-address miss, and have
airlock_or map it to DnsError (airlock takes precedence). TLS-error detection is
left as a focused TODO: the rustls backend is transitive, so there is no stable
typed error to downcast to without a brittle string matcher; the reliable DNS half
ships now.
Copilot AI review requested due to automatic review settings June 24, 2026 14:33

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 makes the astrid:http host’s previously hardcoded limits configurable via a new global [http] configuration section, threading the resolved limits from astrid-config through the daemon/kernel/loader into each capsule HostState. It also includes follow-up fixes to redirect handling and error typing, plus related test updates and documentation/defaults updates.

Changes:

  • Add [http] config (HttpSection) and enforce operator-only control via merge::restrict, with defaults matching historical constants.
  • Thread resolved HttpLimits through daemon → kernel → capsule loader/engine → HostState, and update the HTTP host implementation to read limits from HostState.http_limits.
  • Apply follow-up behavior fixes (redirect hop checks, streaming timeout explicitness, DNS error typing) and add/adjust tests + defaults/docs + changelog entry.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/astrid-daemon/src/lib.rs Resolves [http] config into typed HttpLimits and passes into Kernel::new.
crates/astrid-kernel/src/lib.rs Stores http_limits and forwards it into capsule loader creation.
crates/astrid-capsule/src/engine/wasm/limits.rs Introduces HttpLimits (typed, Duration-based) with historical-default Default.
crates/astrid-capsule/src/engine/wasm/host_state.rs Adds http_limits to HostState and includes it in debug output.
crates/astrid-capsule/src/engine/wasm/mod.rs Threads http_limits into pooled HostState creation; lifecycle path currently uses defaults.
crates/astrid-capsule/src/loader.rs Extends loader to carry HttpLimits to WasmEngine.
crates/astrid-capsule/src/lib.rs Re-exports HttpLimits.
crates/astrid-capsule/src/engine/wasm/host/http/backend.rs Uses HostState.http_limits for quotas/timeouts; adds DNS-miss typing via resolver flags.
crates/astrid-capsule/src/engine/wasm/host/http/options.rs Resolves request options against HttpLimits; fixes streaming explicit-total handling.
crates/astrid-capsule/src/engine/wasm/host/http/ssrf.rs Simplifies redirect hop logic; adds dns_failed flag + typed DNS error mapping helpers.
crates/astrid-capsule/src/engine/wasm/host/http/mod.rs Updates host trait impls to resolve options using HostState.http_limits.
crates/astrid-capsule/src/engine/wasm/host/http/tests.rs Updates/unit-tests redirect behavior and option resolution against limits.
crates/astrid-capsule/src/engine/wasm/host/http/regression.rs Adds regressions for configured limits + DNS error typing and updates existing regressions.
crates/astrid-config/src/types.rs Adds HttpSection config type with defaults + parsing tests.
crates/astrid-config/src/merge/restrict.rs Blocks workspace/project overrides of [http] as operator-only.
crates/astrid-config/src/merge/tests.rs Adds tests asserting workspace layers cannot introduce/override [http].
crates/astrid-config/src/show.rs Adds a config show test ensuring [http] is visible.
crates/astrid-config/src/defaults.toml Documents [http] defaults and semantics.
crates/astrid-hooks/src/handler/wasm.rs Initializes hook HostState.http_limits to defaults (not resolved config).
crates/astrid-integration-tests/tests/*.rs Updates integration tests for CapsuleLoader::new/Kernel::new signature changes.
crates/astrid-capsule/src/engine/wasm/test_fixtures.rs Updates minimal host state fixture to include default http_limits.
CHANGELOG.md Adds an Unreleased entry documenting the new [http] config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/astrid-daemon/src/lib.rs
Comment thread crates/astrid-config/src/types.rs Outdated
Comment thread crates/astrid-hooks/src/handler/wasm.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/mod.rs Outdated
@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 operator-tunable configuration for astrid:http host limits, allowing for greater control over outbound HTTP traffic. It also includes several critical follow-up fixes related to streaming request timeouts, redirect target blocking, and DNS error handling, while ensuring that these security-sensitive limits cannot be bypassed by untrusted project configurations.

Highlights

  • Operator-tunable HTTP limits: Introduced a new [http] configuration section allowing operators to tune seven previously hardcoded HTTP host limits.
  • Security enforcement: Implemented merge::restrict to prevent untrusted project or workspace configurations from relaxing or widening these HTTP limits.
  • Follow-up fixes: Addressed three issues from previous work, including improved total-ms handling on streaming paths, simplified redirect blocking, and better DNS error mapping.
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.


Limits set by operator hand, HTTP traffic across the land. Configured tight, the bounds are clear, With DNS errors no longer to fear.

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 operator-configurable HTTP limits for the astrid:http host, replacing previously hardcoded constants with a new [http] configuration section. These limits (including timeouts, redirect hops, concurrent streams, and response body size caps) are resolved by the daemon, stored on the kernel, and snapshotted onto each capsule's HostState. The changes also include a fix mapping DNS resolution failures to a typed DnsError and several tests. Feedback on the changes points out a redundant per-principal stream count check in backend.rs that can be removed to avoid an unnecessary O(N) iteration on every stream start.

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-capsule/src/engine/wasm/host/http/backend.rs Outdated
…ttp] semantics; fix test clippy

Addresses CI clippy and review comments on the [http] config branch.

Clippy (--all-targets): the three new config tests used
field_reassign_with_default (let mut x = T::default(); x.field = v). Converted
each to struct-init form (T { field: v, ..Default::default() }). CI clippy now
runs --all-targets, which surfaced these.

Threading: two HostState build sites used HttpLimits::default() instead of the
configured limits, so operator [http] policy didn't reach all HTTP-capable WASM.
Both now receive the real resolved limits:
- lifecycle hooks: added http_limits to LifecycleConfig; astrid-capsule-install
  resolves it from the global [http] config and threads it in (mod.rs uses
  cfg.http_limits).
- WASM hook handler (astrid-hooks): WasmHandler resolves [http] once at
  construction and applies it to every hook HostState (self.http_limits).
Both load global config ([http] is operator-only global policy). Added a shared
HttpLimits::from_config_values constructor so the seconds->Duration conversion
lives in one place (daemon, install, hooks). New tests: from_config_values
conversion + defaults, and a hook HostState reflecting a configured limit.

Docs: corrected the [http] field docs (types.rs, defaults.toml, daemon
resolve_http_limits, HttpLimits) that overclaimed every field is a hard ceiling.
Precise now: the four timeout fields are per-request DEFAULTS a caller may
override with a LARGER value; max_redirects/max_concurrent_streams are caller
ceilings; max_response_bytes is a default+ceiling hard-clamped to
MAX_GUEST_PAYLOAD_LEN (the one hard cap). The operator is the trust root and may
raise or lower the soft limits; only a workspace/project layer is barred from
setting/widening [http].

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 29 changed files in this pull request and generated 3 comments.

Comment thread crates/astrid-capsule/src/engine/wasm/host/http/ssrf.rs
Comment thread crates/astrid-hooks/src/handler/wasm.rs Outdated
Comment thread crates/astrid-capsule-install/src/lifecycle.rs Outdated
…, warn on config-load failure

Three PR re-review findings on the [http] config branch:

1. Redundant per-principal stream-quota clause (backend.rs http_stream_backend).
The gate was per_principal_count >= max || active_http_streams.len() >= max with
the SAME ceiling; since per_principal_count <= active_http_streams.len() the
per-principal clause could never trip first — dead. Removed it (and the unused
per_principal_count computation); kept active_http_streams.len() >= max as the
per-capsule cap (matches the field doc). A real per-principal limit would need a
separate, smaller bound — noted as out of scope. Behaviour-preserving; both quota
tests still pass.

2. dns_failed was over-broad (ssrf.rs SafeDnsResolver::resolve): it set the flag
for ANY lookup_host error, mislabeling a transient resolver timeout / I/O error
as DnsError. Narrowed to ErrorKind::NotFound (genuine NXDOMAIN) via a new
lookup_err_is_not_found helper; other kinds leave the flag clear and fall through
to the generic reqwest classification. The empty-resolved-addresses path still
sets dns_failed unconditionally (unambiguous no-resolve). Safe degradation: a
not-found that surfaces as a different kind under-produces DnsError.

3. Silent fallback on config-load failure (resolve_http_limits in astrid-hooks
and astrid-capsule-install): a malformed global config silently used
HttpLimits::default(). Now emits tracing::warn! on the load-error branch before
falling back, so the divergence is signalled. No other behaviour change.

Tests: the prior .invalid resolver test asserted dns_failed on the lookup_host
error path, but that host's error kind is platform-dependent (macOS reports
Uncategorized, not NotFound), so after narrowing it would be flaky. Replaced it
with two hermetic, platform-independent tests: one asserts the narrowing decision
directly via lookup_err_is_not_found (NotFound -> set, TimedOut/Other -> clear),
the other asserts flag_error maps the flags to DnsError/AirlockRejected with
airlock precedence.
Both configured_max_concurrent_streams_lowers_quota and
stream_quota_triggers_and_releases opened real streams via http_stream_backend
against a one_shot_server that serves a single connection then closes. An open
with no live responder blocked on the 120s streaming header-deadline floor, so
the tests sat ~120s (reported as a 60s+ hang on CI). A test defect, not an impl
bug.

Fix (hermetic approach): the concurrency quota gate runs at the TOP of
http_stream_backend, BEFORE any network, reading only active_http_streams.len().
The tests now pre-populate active_http_streams to the cap with synthetic streams
(no sockets, no network) and assert a single over-cap http_stream_backend call
returns ErrorCode::Quota immediately; close/drop are asserted to release a slot.
The synthetic stream comes from a new #[cfg(test)] ActiveHttpStream::dummy_for_test
that builds a no-network reqwest::Response via reqwest::Response::from(http::Response).
Added http as a dev-dependency (test-only) for that construction.

Both test bodies are wrapped in tokio::time::timeout(5s) as a hard backstop so
this can never hang CI again. Assertions are unchanged (cap=2 -> 3rd open Quota;
fill-to-cap -> over-cap Quota; close/drop release the slot). Each now runs in
~0.01s; the full http test module completes in 0.01s with no hang.
@joshuajbouw joshuajbouw merged commit 8f12344 into main Jun 24, 2026
13 checks passed
@joshuajbouw joshuajbouw deleted the feat/http-config-knobs branch June 24, 2026 17:14
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.

Make the seven astrid:http host limits operator-configurable via [http] config

2 participants