Skip to content

feat(triggers): fire env triggers on requests to the env's websites - #237

Draft
sarah-chitty-1 wants to merge 1 commit into
mainfrom
sarahchitty/triggers-http-when
Draft

sarah-chitty-1 wants to merge 1 commit into
mainfrom
sarahchitty/triggers-http-when

Conversation

@sarah-chitty-1

@sarah-chitty-1 sarah-chitty-1 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

A new trigger type, when.type: "website", that fires on requests the gateway proxies to an env's websites (/website/<app>/...).

Action triggers only see tool calls today. An agent that browses with its own browser or HTTP client (an external A2A agent, a computer-use agent with its own browser, a script) never calls the env's browser_* tools, so "when the agent opens the order page" never fires for it. Its page traffic does go through the gateway's website proxy, so the gateway can match on that.

{"id": "carrier-emails", "when": {"type": "website", "app": "porchbite", "method": "GET",
  "path": {"regex": "^/api/orders/"}}, "barrier": {"at": "provoking_call"}, "actions": [...]}
  • Fields:
    • app: required, and must be one of the env's websites (400 otherwise)
    • method: optional, upper-cased
    • path and status: optional, using the existing regex / equals / exists checks
  • Carried over from tool-call triggers: where on the request's args (query parameters plus form or JSON-object body fields; the body wins a clash), roles, repeat, ${args.*} in templates, and the provoking_call barrier, which holds the website's response until the trigger's actions have landed.
  • Status default: without a status check, only responses below 400 match, the same way a failed tool call never fires a trigger. A status check can match 4xx/5xx on purpose.
  • Firing log (GET /triggers/state): same contract. A detected event names the request as provoking: {source: "website", app, method, path, status, role}. The query string and body are never logged. The trajectory's trigger_fired also carries provoking_request.
  • Card: website is added to the triggers/v1 features list.
  • Errors never break a request: if trigger evaluation fails, the request is still answered normally.
  • Explorer: website triggers and requests get readable labels in the run viewer. Before this, they showed "matched on ? (default)".

Worth a look

  1. Behaviour change: a successful request that changes something (anything but GET/HEAD/OPTIONS/TRACE) now re-checks state triggers, as a write through a tool does. This affects any env that already combines state triggers with website traffic through the gateway.
  2. Scope: only /website/<app>/ traffic counts. Direct backend calls through /svc/<app>/ don't. A broader http type could be a follow-up.
  3. No response data: responses stream through unread, so ${result.*} and where on the result are refused at registration. Capturing small JSON responses could be a follow-up.
  4. Roles: a plain browser sends no AgentEnv-Role header and counts as default. A client sets the header to act as another role.
  5. Not in this PR: a deadline's unless still matches only tool calls, and website requests themselves aren't written to the trajectory, only provoking_request on trigger_fired.

Tests

  • Unit: tst/unit/env/gateway/triggers_website_test.py, 24 tests:
    • registration checks, matching, and firing-log and trajectory attribution
    • roles, repeat vs one-shot, barriers and state re-checks
    • through the real gateway app against a fake website: the barrier hold (fails without the barrier), path/role/args extraction, and the error path
  • Integration (slow): deploys the slack website env and fires a trigger through /website/slack/.
  • Results:
    • ruff clean
    • unit suite 8392 passed, 20 skipped
    • plugin-api: no break
    • uv lock --check OK
    • UI typecheck, lint and smoke clean
    • local slow integration for gateway triggers and register_env_triggers all pass

Tested end to end with an external agent's own browser

Headless Chrome loaded a stayncook page through our front door on Modal: first a plain Playwright script, then claude-opus-5-5 driving its own browser. The website's requests went through the gateway's /website/stayncook/ proxy. A real product (Meta's Muse) is next.

  • The trigger fired on the real GET /: provoking: {source: website, app: stayncook, method: GET, path: /, status: 200, role: psi_agent}, once (fire_count: 1).
  • The provoking_call barrier held the page until the host's cancellation and its verify step had landed, about 45 ms. The 200 went out 0.2 ms after fired in the gateway's own log.
  • Both sides saw the change: the agent's first trips fetch already showed the stay gone, and the state grader saw the cancellation as a world change.

Docs note: the provoking response is held, not re-rendered. A trigger meant to change what the agent sees should key on the page load (the HTML document), not on the data call it wants to change. Otherwise the barrier holds a response that already has the old data.

🤖 Generated with Claude Code

A `when.type: "website"` trigger matches the requests the gateway proxies
to one of its websites (`/website/<app>/...`): `app`, an optional `method`,
`path` and `status` predicates, plus `where` over the request's args (its
query and form/JSON-object body fields), `roles`, `repeat` and a
`provoking_call` barrier, which holds the website's response until the
actions have landed. Without a `status` predicate only a response below
400 matches, as only a tool call that did not fail provokes an action
trigger. An unsafe request that did not fail also prompts the state
triggers, as a non-readonly tool call does.

This lets an agent that browses with its own browser or HTTP client
provoke triggers, not only one calling the env's tools.

The firing log attributes the request (`provoking: {source: "website",
app, method, path, status, role}`), the trajectory's `trigger_fired`
carries `provoking_request`, the card declares the `website` feature, and
the explorer names website requests in the trigger timeline.

Co-Authored-By: Claude Opus 5.5 <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