Skip to content

feat(website): record a website env's traffic as PCAP, served through traffic/v1 (FD-4683) - #469

Open
williamloo-scale wants to merge 8 commits into
mainfrom
wloo/website-traffic-capture
Open

williamloo-scale wants to merge 8 commits into
mainfrom
wloo/website-traffic-capture

Conversation

@williamloo-scale

@williamloo-scale williamloo-scale commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

A website env can record every packet its websites' server and frontend containers send and receive, as PCAP, and serve the files through its card. This is opt-in.

  • Turning it on: deploy_env takes capture_traffic, and agent-env env deploy takes --capture-traffic. It works for a website env, or a multi env with a website, on a VM. Any other env, and container mode, refuse it.

  • Capture: each website container gets a tcpdump service beside it.

    • tcpdump comes from the gateway image and runs in that container's network namespace (network_mode: "service:<container>").
    • It's the only service holding NET_RAW/NET_ADMIN.
    • It writes rotating classic pcap files, 10 of 20 MB each with the oldest overwritten, to a volume no website container mounts. The gateway mounts it read-only.
    • The deploy is refused if a capture isn't running once the gateway is up.
  • Protocol: the new agentenv_protocol.traffic defines urn:agentenv:traffic/v1 with two ops:

    • list returns a TrafficCaptureListing, oldest file first;
    • get streams one file as application/vnd.tcpdump.pcap.

    Client helpers: list_traffic_captures, fetch_traffic_capture, serves_traffic. The gateway's constants.py mirrors the names, with a test keeping them equal.

  • Access: both ops require the per-deploy control token (DeployedEnv.control_token). The gateway removes Authorization from a proxied request only when it carries that token (compared with hmac.compare_digest), so no site can read the token, and a site's own bearer auth still reaches it.

  • Collection: the new collect_traffic_capture step stores each capture as a pcap FileArtifact in one FileArtifactUniverse, recorded under context.metadata["traffic_captures"][env_id]. It's best effort unless fail_task_on_error is set. Teardown also collects any capturing env the run didn't collect, before the env closes, so a capture isn't lost with its VM.

Before merging

  • Not yet run on a deployed VM. The compose wiring, gateway routes, token stripping, step and teardown are covered by unit tests.

Test

Rebased onto main after #444, #508, #510 and #514. Unit tests over packages/agentenv-protocol and tst/unit/{env,providers,task_step,task,cli}: 8,207 passed; the two failures, in test_disk_recipe.py, fail on main on the local machine too. Lint clean.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

No new blocking finding was established; this does not clear the earlier unnumbered threads.

Summary

Adds opt-in website packet capture, token-protected traffic/v1 downloads, and artifact collection before teardown.

  • Website deployments can record packets when the deployer opts in.
  • The gateway lists and streams packet captures through traffic/v1.
  • Task runs save packet captures as artifacts, including during teardown.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Website[Website container] --> Capture[tcpdump sidecar]
  Capture --> Volume[Rotating capture files]
  Volume --> Gateway[Gateway reads files]
  Client[Collector with control token] --> Gateway
  Gateway --> Artifacts[File artifacts and universe]
  Artifacts --> Run[Saved run reference]
Loading

Reviews (12) · Last reviewed commit: "docs(website): traffic capture records w..." · Reviewed by Greptile

@williamloo-scale
williamloo-scale requested a review from a team as a code owner November 3, 2026 21:53
Comment thread src/agent_env/env/gateway/gateway.py Resolved
Comment thread src/agent_env/env/gateway/gateway.py Resolved
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from a767a25 to d160d31 Compare November 3, 2026 22:52
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 194572e to 3b757da Compare November 3, 2026 22:52
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from d160d31 to 5953d5a Compare November 3, 2026 23:53
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 3b757da to 73241f9 Compare November 4, 2026 00:00
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from 5953d5a to 9f2a5ca Compare November 4, 2026 00:32
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 73241f9 to 0192f08 Compare November 4, 2026 00:33
Comment thread src/agent_env/task/teardown.py Resolved
Comment thread src/agent_env/task/teardown.py Resolved
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 0192f08 to 77c1ede Compare November 4, 2026 01:06
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch 2 times, most recently from 4283738 to 0cca3db Compare November 4, 2026 01:30
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 77c1ede to 965deb7 Compare November 4, 2026 01:30
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from 0cca3db to a1ec8b4 Compare November 4, 2026 03:37
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 965deb7 to 9ac178a Compare November 4, 2026 03:37
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from a1ec8b4 to d044c00 Compare November 4, 2026 03:56
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 9ac178a to 0e9e82d Compare November 4, 2026 03:56
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from d044c00 to f813a59 Compare November 4, 2026 05:07
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from 0e9e82d to dcc3d9f Compare November 4, 2026 05:07
@williamloo-scale
williamloo-scale force-pushed the wloo/website-computer-routing branch from f813a59 to faf8e7c Compare November 4, 2026 05:10
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from dcc3d9f to bdec057 Compare November 4, 2026 05:10
@earakely-scale
earakely-scale force-pushed the wloo/website-computer-routing branch from 27a865f to 1f97138 Compare November 5, 2026 17:59
Base automatically changed from wloo/website-computer-routing to main November 5, 2026 23:35
williamloo-scale and others added 8 commits November 6, 2026 10:05
… traffic/v1 (FD-4683)

This PR's change, rebased as one commit onto #448.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…refix of its own, and capture loopback too (FD-4683)

- The gateway's traffic/v1 get copies a capture before sending any of it, copying again when tcpdump truncates the
  file to reuse it, and answers 503 when every copy was rewritten.
- collect_traffic_capture writes each collection under its own attempt prefix, and the upload owns its temp file
  (finish_on_thread), so a cancelled collection never removes a file an upload still reads.
- tcpdump captures every interface of the container, its loopback among them.
- The read size and error limits are named.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the env closes (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…collects a retry's rolled-back deployments too (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…time runs out partway (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… stored linked to the run as partial (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…thread, so teardown's time limit holds (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nd from just after each starts, not before (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@williamloo-scale
williamloo-scale force-pushed the wloo/website-traffic-capture branch from ffa4b7b to 01650f0 Compare November 6, 2026 18:38
@earakely-scale

Copy link
Copy Markdown
Collaborator

🤖 Automated Claude routine — Env Pod PR Context Review. This is a bot, not Edgar Arakelyan; not a human review.

Context from outside this diff: #492 ("fix(env): the gateway's control routes take the deploy's control token") is also open against main and builds the same AGENTENV_CONTROL_TOKEN mechanism in the same three files. Neither description mentions the other, so neither CI run sees the pair.

Both add CONTROL_TOKEN_ENV = "AGENTENV_CONTROL_TOKEN" to env/gateway/constants.py (here at :157, in #492 at :71), both add control_token to DeployedGateway (env_gateway_provider.py:142 here, :134 there), and both set self._control_bearer = f"Bearer {control_token}".encode() in Gateway.__init__ (gateway.py:186 here, :169 there). Those read as ordinary conflicts. The one that doesn't is the pair of wrapper steps:

  • Here (gateway.py:504-505, head 01650f0): the first thing app() does is scope = self._take_control_token(scope), which removes the Authorization header when it matches the bearer — before the scope["type"] != "http" early return. Each framework-only route then asks _refuse_uncontrolled (:530), which reads the agentenv.control scope key the strip left behind.
  • In fix(env): the gateway's control routes take the deploy's control token #492 (gateway.py:480, head eda2a8c): the gate sits after that early return and reads the header back out of the scope — dict(scope.get("headers", ())).get(b"authorization", b"") compared against self._control_bearer — 401ing any path in CONTROL_ROUTES (constants.py:165: the /tools/*, /triggers/*, /clock/* and /notifications/* routes).

On the natural keep-both merge the strip lands first, so by the time #492's gate looks the header is gone and compare_digest(b"", bearer) fails. That would 401 every call to those eleven routes on a token-bearing gateway — including the framework's own, which do send it: Env.control_headers → agentenv_protocol/client.py:47-49 emits exactly Authorization: Bearer <token>, and task/task.py:654 grants it for every deployed env. Worth agreeing up front which PR owns the wrapper gate rather than resolving it at merge time, since both orderings look right in isolation.

Two smaller divergences in the same pair, if they're settled together:

Not a review of this diff — only that the two halves are in flight at once and the merge order decides the behaviour.


Generated by Claude Code

This branch has not been deployed

No deployments
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.

2 participants