std.http

June 9, 2026 · View on GitHub

Status: Phase 33S2 (closed 2026-06-09). Two tools execute real HTTP requests; three RuntimeChecked guarantees govern their behavior. Earlier slices shipped the envelope types and envelope-builder agents only; 33S2 promotes the module to executing.

Quick reference

import "./std/http" use http_get, http_post_json

agent fetch_status(url: String) -> Int:
    response = http_get(url)
    return response.status

agent publish_event(url: String, event_json: String) -> Int:
    response = http_post_json(url, event_json)
    return response.status

When this agent runs through corvid run, both calls flow through the configured [http] allow allowlist (after the always-on SSRF block) and return the typed HttpResponseEnvelope:

public type HttpResponseEnvelope:
    status: Int
    body: String
    attempts: Int
    elapsed_ms: Int
    effect_meta: EffectEnvelope

The two tools

http_get(url: String) -> HttpResponseEnvelope uses http_egress_get

Performs an HTTP GET. The http_egress_get effect carries io_source: net.egress and reversible: true — composes correctly with @reversible constraints elsewhere in the call graph.

http_post_json(url: String, body: String) -> HttpResponseEnvelope uses http_egress_post

Performs an HTTP POST with the supplied UTF-8 body and Content-Type: application/json. The http_egress_post effect carries io_source: net.egress and reversible: false so callers can reason about side effects in trace + budget constraints.

For composing requests with custom timeouts, retry policies, or header sets, the envelope-builder agents in std/http.cor (http_request_get, http_request_post_json, http_with_retry, http_with_timeout, http_ok) construct HttpRequestEnvelope values — those are still pure agents that BUILD a request; the two tool declarations above PERFORM the request.

Security model — [http] allow + always-on SSRF

The executing HTTP-client surface combines TWO independent properties on every URL:

1. Structural SSRF block — always on

Every URL whose host lexically resolves to a private range is refused, regardless of allowlist contents. The hosts blocked:

rangeexamples
RFC1918 private IPv410.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Loopback IPv4127.0.0.0/8
Link-local IPv4169.254.0.0/16
Unspecified IPv40.0.0.0/8
Loopback IPv6::1, [::1]:port
Link-local IPv6fe80::/10
ULA IPv6fc00::/7
Unspecified IPv6::
DNS aliaslocalhost (case-insensitive)

A misconfigured allowlist that contains 127.0.0.1 STILL gets refused — the SSRF block is the security FLOOR, not a configurable setting. There is no --allow-private-egress flag and no env override; the rule lives in HttpEgressPolicy::check's structural-properties section and can only be relaxed by patching the runtime source.

2. [http] allow allowlist — required, fail-closed

The executing HTTP-client surface refuses to operate without an explicit [http] allow list declared in corvid.toml. The corvid new-scaffolded project carries [http] allow = [] so the security boundary is visible from day one; users opt in to each host explicitly.

corvid.toml

[http]
allow = ["api.example.com", "api.anthropic.com"]

Comparison is exact-host, case-insensitive. No subdomain wildcards (yet — *.example.com is a planned future-scope extension, not in 33S2).

Env override

CORVID_HTTP_ALLOW=api.example.com,api.anthropic.com corvid run src/main.cor

Comma-separated host list; whitespace trimmed; empty entries stripped. Precedence: CORVID_HTTP_ALLOW wins over corvid.toml; whitespace-only env values fall back to corvid.toml (so CORVID_HTTP_ALLOW=" " doesn't accidentally clear an allowlist).

What's rejected

  • Missing [http] allow (no section, empty list, or unset env): every call fails closed with a diagnostic naming the missing config + the CORVID_HTTP_ALLOW env pathway + the fail-closed contract from the 33S0 security model.
  • Host not in allowlist: a structured diagnostic names the requested URL, the parsed host, and the configured allowlist so the operator can see exactly which entry is missing.
  • Private / loopback / link-local URL: the SSRF block fires before the allowlist is consulted; the diagnostic names "SSRF" and "structural property" so the operator understands the floor.

Determinism

Both tools are non-deterministic. The checker rejects calls inside a @deterministic body — this is the existing decl_replayability.rs::is_deterministic_compatible rule that treats every tool call as non-deterministic by declaration kind. Programs that need both deterministic logic AND HTTP egress must factor egress into a separate, non-deterministic agent.

Replay quarantine

When a Corvid program runs in Substitute-mode replay (replaying a recorded trace), the executing HTTP-client surface honors the same quarantine the existing HttpClient::quarantine hook provides. Two layers:

  1. Low-level HttpClient::send returns QuarantineViolation { surface: "http", .. } if called during replay. The detail names the method + URL so the operator can see exactly what was refused.
  2. Dispatch path (Runtime::call_tool("http_get" | "http_post_json", ...)) goes through the replay-substitution path FIRST. POST and GET both substitute from the recorded trace OR diverge — they never reach the live network.

Together the two layers prove the network is provably untouched during replay, regardless of allowlist contents (a fully- configured allowlist does not bypass the quarantine).

Guarantees

Three RuntimeChecked rows in the canonical guarantee registry (docs/reference/core-semantics.md):

idclassenforces
io_source.http_ssrf_structural_blockRuntimeCheckedPrivate / loopback / link-local hosts refused regardless of allowlist. Structural floor.
io_source.http_allowlist_enforcementRuntimeCheckedURL host must appear in [http] allow; missing allowlist fails closed.
io_source.http_quarantine_on_replayRuntimeCheckedPOST / GET dispatch during replay substitutes from trace or diverges; never reaches the network.

The @deterministic-rejection property is covered by the existing replay.deterministic_pure_path guarantee — it applies to all tool calls regardless of effect.

Worked example — webhook fan-out with allowlist scoping

import "./std/http" use http_post_json, http_ok

agent notify_webhook(url: String, event_json: String) -> Bool:
    response = http_post_json(url, event_json)
    return http_ok(response)

With corvid.toml:

[http]
allow = ["hooks.example.com", "audit.example.com"]

notify_webhook("https://hooks.example.com/new-order", "...") succeeds; notify_webhook("https://hooks.attacker.example/...", "...") is refused at the dispatch boundary with a diagnostic naming the unlisted host and the configured allowlist. Even if a future configuration mistake adds 127.0.0.1 to the allowlist, calls to loopback are still refused by the structural SSRF block.

  • core-semantics.md — full guarantee registry, including the three io_source.http_* rows.
  • inventions.md — invention proof matrix.
  • corvid tour --topic http-client — runnable demo (offline; the topic source compiles through the driver's tour-compile gate).
  • crates/corvid-runtime/src/http.rsHttpEgressPolicy source
    • inline guarantee anchors (GUARANTEE_ID_IO_SOURCE_HTTP_*).
  • crates/corvid-driver/src/run.rs::load_http_egress_policy — CLI loader: env > corvid.toml > unset.
  • std/http.cor — the tool declarations + envelope types.