Skip to content

feat(website): WebsiteEnvironment serves a site on the website/v1 contract, and a conformance check tests any server (FD-4683) - #428

Draft
williamloo-scale wants to merge 4 commits into
wloo/website-harness-gapsfrom
wloo/website-environment-sdk
Draft

williamloo-scale wants to merge 4 commits into
wloo/website-harness-gapsfrom
wloo/website-environment-sdk

Conversation

@williamloo-scale

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

Copy link
Copy Markdown
Contributor

Stacked on #427.

agentenv_protocol.website: the easy path to a synthetic website that follows website/v1 (urn:agentenv:website/v1).

@environment_card(name="notes")
@website(domains=["notes.test"], static="web/dist", health="/healthz", data="data/seed.json")
class Notes(WebsiteEnvironment):
    @api.get("/api/notes")
    def list_notes(self, ctx: ApiContext) -> list:
        return self.store.find("notes", owner=ctx.user)
  • @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 from X-Forwarded-Host/-Proto.
  • Roles: the gateway forwards whatever AgentEnv-Role a browser sends, so a site believes a non-default role only when the request also carries 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, so roles= routes are closed until a key is set.
  • Serves the built pages with an index.html fallback, the health path with @ready_check, the data plane and a card declaring website/v1 with its routes and openapi/session operations. /agentenv… routes are matched before the site's own.
  • self.now() reads the universe clock (clock/v1 sync_time); every response carries X-Universe-Now; clock_patch=True patches the page's Date.
  • Collections store: load, export, reset, initial/current/diff state and /go with no data hooks written.
  • python -m agentenv_protocol.website.conformance URL checks any server over HTTP: card, any host name, health, universe time, data plane, clock, forwarded headers, cookies.

Known gaps

  • Roles are authenticated only inside SDK sites: nothing sets WEBSITE_ROLE_KEY on a deployed site yet, and the gateway still trusts AgentEnv-Role on MCP calls, non-SDK sites and website trigger attribution. A per-deploy key in the gateway is the full fix.
  • The gateway sends no X-Forwarded-*, so behind it sites must use relative URLs.
  • SDK sites in one multi env share the session cookie name.
  • Conformance requires X-Universe-Now and a filled-in card, stricter than a non-SDK server may be.
  • /go ignores sid.

Test

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 spoofed AgentEnv-Role: admin now gets 403 on both /website/ and /svc/ (200 before).

🤖 Generated with Claude Code

…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>
williamloo-scale and others added 3 commits November 2, 2026 01:22
…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>
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>

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.

1 participant