Repository navigation
feat(website): WebsiteEnvironment serves a site on the website/v1 contract, and a conformance check tests any server (FD-4683) - #428
Open
williamloo-scale wants to merge 49 commits into
Conversation
… plane off the site's route (FD-4683) A website whose server image serves its pages runs as one container: /website/<name>/ and /svc/<name>/ both reach it, and it's checked at its declared health path. A site's route never forwards its data plane or env card, however the path is spelled, so an agent browsing the site can't read what grades it; agentenv reaches them through /svc/<name>/. A site built on a separate frontend image deploys as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and verify its state at /go (FD-4683) agent-env env website put takes the server image from --server-dockerfile, stored with its build context as an a2a agent's image is, from --server-image-ref, or from GitHub, with --health-path; a separate frontend image is optional and warned of. The website_state_verifier step reads each checked session of a website env's sites, or a multi env's, at the site's /go?sid= through /svc/<name>/, the verifier endpoint CUA-Gym sites serve, and checks dot paths of its current_state, initial_state or state_diff; with env_version pinned, put and preflight refuse checks of sites that version doesn't serve. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tract, and a conformance check tests any server (FD-4683) A site declares itself with @website(domains, static, health) and its routes with @api, which bind path, query and body from type hints and take an ApiContext with the signed-in user, role and public URL. It serves its built pages, its health path, the data plane and a card that declares website/v1 with its routes, an OpenAPI document and a session operation. self.now() reads the universe clock, and each response carries X-Universe-Now. A site on the built-in Collections store loads, exports and resets its state and serves /go without data hooks. python -m agentenv_protocol.website.conformance URL checks a running server over HTTP alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s websites as its MCP servers (FD-4683) sync_env_clock reaches a website env's site, and a multi env's websites beside its MCP servers; a site that doesn't follow the clock is tolerated. snapshot_env exports each website child through its data plane and skips, with a warning, one that has none. A config broadcast to every service reaches websites too, and skips one without a card or the extension, so a multi env with an older site behaves as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E_KEY (FD-4683) The gateway forwards the role a browser sends on /website/<name>/, so a page or a fetch could claim any role and call a roles= route. ctx.role is now the AgentEnv-Role a caller names only when it also sends AgentEnv-Role-Key equal to the site's WEBSITE_ROLE_KEY, compared in constant time; every other caller, and every caller of a site with no key set, is default. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # AGENTS.md # src/agent_env/env/envs/multi_env.py # src/agent_env/env/envs/website.py
# Conflicts: # AGENTS.md
…no image source (FD-4683) website put now takes one of --server-dockerfile, --server-dockerfile-github-url and --server-image-ref, so the missing-source check names those options instead of --backend-dockerfile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/agent_env/task_step/task_steps/snapshot_env.py
ruff's F401 failed the unit job's lint step on this branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in main's sync_env_clock timezone (#442) beside this branch's websites: every synced server, MCP server or website, is told the zone, and a website that follows the clock but can't take it fails the step as an MCP server does. A website sharing an MCP server's name, synced with no record so its card is read live, now gets the env's control headers instead of reading them off the missing record. #442's tests stub _backing_servers, which replaced _service_names here. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…4683) main's sync_env_clock (#442) tells every synced server the task's timezone and fails the step on one whose sync_time can't take it; websites are synced servers here, so WebsiteEnvironment.sync_time takes an optional timezone, advertised on the card. The clock keeps it and the site reads it as self.timezone; now() stays UTC. A later sync naming none drops it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
williamloo-scale
marked this pull request as ready for review
November 2, 2026 23:27
…the site receives is spelled (FD-4683) The guard checked the decoded path, but the proxy forwards the raw one and httpx removes its dot segments, so /website/mail/public%2Fx/../agentenv passed the guard and reached the site as /agentenv. The guard now also checks the path the site's server receives: the forwarded URL as httpx normalizes it, decoded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s website/v1 ready (FD-4683) The env card is the protocol layer, so a site's readiness is read from it, as computer-app/v1's ready is read of an app: once the gateway serves its card, the deploy reads each site's card through /svc/<name>/ and polls website/v1's ready until it says so, failing after 300s naming each site's problems. A site whose card has no ready, or that serves no card, is read at its stored health path instead, with a deprecation warning. The compose health check now only says the server is up: it answers its card, or for a site serving none its health path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # tst/unit/providers/env_providers/env_gateway_provider_test.py
…(FD-4683) WebsiteEnvironment serves website/v1's ready (GET): ready, and the problems, the names of the @ready_check methods not yet true, as computer-app/v1's ready does of an app. A deploy reads it through the card; the health path still answers for a reader that hasn't moved. Conformance's health check reads ready when the card advertises it, and the health path, noted as deprecated, when it doesn't. A framework test keeps the URI the deploy reads the one the SDK serves. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hat would match them (FD-4683)
The built-in routes are registered before the site's own, so a route such as /{slug} or /api/{name} no longer
swallows them; an @api route at their very path still replaces them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… new one (FD-4683) sync() reads under the lock refresh() holds, so it waits for a refresh in flight and that refresh's read of the old clock can't land after the new clock's or measure a rate across the two. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ructed (FD-4683) clock_patch put a class in Date's place, and calling Date() without new threw. It is now a function that returns the universe's time as a string when called and constructs through Reflect.construct, keeping Date.now at the universe's time, Date.parse, Date.UTC, instanceof Date and subclasses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…D-4683) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… isn't reserved, not in a comment (FD-4683) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cks reset clears, and checks every answer's time (FD-4683) The data-plane round trip now fails a reset after which data/get still returns the exported data, adds the export back when a step after the reset fails, and is skipped for a server whose /go shows changes since its initial state, which data/add would fold into the baseline. universe_now runs after the other checks, so it reads X-Universe-Now on the data-plane, clock, forwarded and session answers too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
williamloo-scale
added a commit
that referenced
this pull request
Nov 3, 2026
Brings in #428's review fixes beside this branch's: the clock's sync reads under the refresh lock and keeps the answered rate; conformance checks X-Universe-Now last, on every answer, at the recommended level, with the reserved-path probes counted; the handwritten test server stamps its 404s and 405s like every other answer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 2xx JSON-RPC result (FD-4683)
_rpc took any JSON object without an error member as success, so a server
answering {} (even with a 500) to every data request passed data_plane.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d bodies as validated (FD-4683) Response schemas were built in validation mode, so a returned model with a computed field or a type-changing serializer was published with the wrong shape. Models the modes see alike keep one shared component. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ead of being skipped (FD-4683) Without a stored card, v1_base_url probed a website with supports_v1, which reads a timeout, a 5xx or a connection error as "legacy", so snapshot_env skipped the website as one serving no data plane and the snapshot could succeed without its state. A website's live probe is now a card read that is None only on a 404 and raises otherwise, so export_one fails that website and the snapshot with it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…answers at the data plane's paths (FD-4683)
openapi() advertised a JSON body for a typed handler declared 204 or
304, which endpoint() never sends; such a response now has no content.
A broad route (/{rest:path}) answered unknown /agentenv/... paths
before the fallback could refuse them; each @api endpoint now answers
404 at a reserved path, while agentenv's own routes there answer first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e or registry write (FD-4683) `agent-env env website put` now checks --health-path with health_path_problem right after its other option checks, so a path like `healthz` no longer costs a build or leaves a saved image artifact behind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r is never served (FD-4683) The static fallback now resolves a directory's index.html before checking it lies under the static root, so a symlinked index pointing outside answers like a missing file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… named (FD-4683) RATE_AGREEMENT names how close two measured clock rates must be to be taken as the rate, and SECRET_BYTES the size of a generated session key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#391 and #392 were squash-merged into wloo/web-app-contract, which then gained the deploy's refusal of a site serving a page at its card's path; this branch's copies of those commits conflict line for line with the squash, so the provider takes the base's code and the tests keep both sides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # tst/unit/providers/env_providers/env_gateway_provider_test.py
Comment on lines
+341
to
+345
| elif held and message["type"] == "http.response.body": | ||
| held["body"].append(message.get("body", b"")) | ||
| if message.get("more_body"): | ||
| return | ||
| body = _with_script(b"".join(held["body"]), clock_patch_script(held["now"])) |
There was a problem hiding this comment.
With clock_patch=True, _Universe saves every HTML chunk until the response ends. A mounted app that streams HTML sends nothing to the browser until its last chunk. An open-ended response never appears and keeps growing in memory.
Skip script injection for streaming responses, or inject it without buffering the whole response.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/website/environment.py
Line: 341-345
Comment:
**Streamed pages stay blank**
With `clock_patch=True`, `_Universe` saves every HTML chunk until the response ends. A mounted app that streams HTML sends nothing to the browser until its last chunk. An open-ended response never appears and keeps growing in memory.
Skip script injection for streaming responses, or inject it without buffering the whole response.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Comment on lines
+125
to
+127
| answer = self._seen(self.client.request(ready.get("method", "GET"), self.base_url + ready["endpoint"])).json() | ||
| if isinstance(answer, dict) and answer.get("ready") is True: | ||
| return "passed", "website/v1 ready says it is ready" |
There was a problem hiding this comment.
_health() accepts {"ready": true} even when the endpoint returns an HTTP error such as 503. Server authors can therefore get a passing conformance result for a broken readiness endpoint.
Keep the response and require a successful HTTP status before accepting its body.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/website/conformance.py
Line: 125-127
Comment:
**Broken readiness passes**
`_health()` accepts `{"ready": true}` even when the endpoint returns an HTTP error such as 503. Server authors can therefore get a passing conformance result for a broken readiness endpoint.
Keep the response and require a successful HTTP status before accepting its body.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
williamloo-scale
force-pushed
the
wloo/website-harness-gaps
branch
from
November 3, 2026 20:56
f5523cf to
7fc14cd
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #427.
agentenv_protocol.website: the easy path to a synthetic website that follows website/v1 (urn:agentenv:website/v1).@api.get/post/put/patch/delete(path, name=, roles=, status=): path, query and body are bound from type hints, errors are exceptions mapped to statuses, and an OpenAPI 3.1 document is generated.ApiContext: the signed-in user (else the acting user, so the agent starts signed in), the caller's role, and the public URL fromX-Forwarded-Host/-Proto.AgentEnv-Rolea browser sends, so a site believes a non-defaultrole only when the request also carriesAgentEnv-Role-Keyequal to the site'sWEBSITE_ROLE_KEY(compared in constant time). Every other caller, and every caller of a site with no key set, isdefault, soroles=routes are closed until a key is set.index.htmlfallback, the health path with@ready_check, the data plane and a card declaring website/v1 with its routes andopenapi/sessionoperations./agentenv…routes are matched before the site's own.self.now()reads the universe clock (clock/v1sync_time); every response carriesX-Universe-Now;clock_patch=Truepatches the page'sDate.sync_timealso takes main's optionaltimezone(feat(sync_env_clock): an optional timezone reads virtual_time in the zone and tells every synced server (FD-5003) #442), which the site reads asself.timezone;now()stays UTC.Collectionsstore: load, export, reset, initial/current/diff state and/gowith no data hooks written.python -m agentenv_protocol.website.conformance URLchecks any server over HTTP: card, any host name, health, universe time, data plane, clock, forwarded headers, cookies.Known gaps
Each is fixed later in the stack, as noted.
WEBSITE_ROLE_KEYon a deployed site yet, and the gateway still trustsAgentEnv-Roleon MCP calls, non-SDK sites and website trigger attribution. A per-deploy key in the gateway is the full fix (feat(gateway): a keyed deploy's role key, forwarded headers, and triggers by route (FD-4683) #443, opt-in withkeyed_roles).X-Forwarded-*, so behind it sites must use relative URLs (feat(gateway): a keyed deploy's role key, forwarded headers, and triggers by route (FD-4683) #443).X-Universe-Nowand a filled-in card, stricter than a non-SDK server may be (feat(website): a session cookie per site, /go?sid=, the clock's rate, conformance levels and models= (FD-4683) #444 splits required from recommended)./goignoressid(feat(website): a session cookie per site, /go?sid=, the clock's rate, conformance levels and models= (FD-4683) #444).Test
Unit suite and protocol tests: 12,659 passed, lint clean.
packages/agentenv-protocol/tests: 946 passed; the website tests (59, with the Notes ones) pass after the role-key fix. Deployed locally behind the gateway, a spoofedAgentEnv-Role: adminnow gets 403 on both/website/and/svc/(200 before).🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge with the noted stack dependencies; the two new findings are non-blocking.Fix with agent prompt
Summary
Adds
WebsiteEnvironment, typed API routes, sessions, a collection store, universe time, and an HTTP conformance checker.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Browser --> Site[WebsiteEnvironment] Site --> Builtins[Health and session routes] Site --> API[Typed API handlers] Site --> Files[Static files or mounted app] Checker[Conformance checker] --> Site Site --> Store[Collections] Site --> Clock[UniverseClock]Reviews (7) · Last reviewed commit: "Merge wloo/website-harness-gaps into wlo..." · Reviewed by Greptile