Skip to content

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

Open
sarah-chitty-1 wants to merge 2 commits into
mainfrom
sarahchitty/triggers-http-when
Open

sarah-chitty-1 wants to merge 2 commits 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. Off unless used: the /website proxy hands requests to the trigger engine only while a website trigger is registered. An env without one pays a single check per request: no body parsing, no time-trigger tick, no state re-check.
  2. State re-checks: with a website trigger registered, a successful request that changes something (anything but GET/HEAD/OPTIONS/TRACE) also re-checks the state triggers, as a write through a tool does.
  3. Error messages keep their old wording as a substring (for example "only valid on action triggers and website triggers"), so anything matching the old text still matches.
  4. Scope: only /website/<app>/ traffic counts. Direct backend calls through /svc/<app>/ don't. A broader http type could be a follow-up.
  5. 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.
  6. Roles: a plain browser sends no AgentEnv-Role header and counts as default. A client sets the header to act as another role.
  7. Hub UI: the hub frontend keeps its own copy of the trigger timeline parser; until it gets the same small change as the explorer here, it shows ? for a website detection.
  8. 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, 26 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, the error path, and that without a website trigger the proxy neither reads requests nor re-checks state triggers
  • Integration (slow): deploys the slack website env and fires a trigger through /website/slack/.
  • Results:
    • ruff clean
    • unit suite 8394 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

Three agents opened a stayncook link through our front door on Modal, each with its own browser: a plain Playwright script, claude-opus-5-5 driving headless Chrome, and Meta Muse's own cloud browser. Their requests went through the gateway's /website/stayncook/ proxy; none of them used the env's browser_* tools.

With Muse, the barrier held the first page about 57 ms, and the gateway sent the 200 0.3 ms after fired. Muse then found the stay under Canceled reservations, and the state grader counted the cancellation as the world's change, not the agent's (verdict: pass).

  • 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

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable issues were found.

Summary

Adds website triggers for requests through the gateway’s website proxy.

  • Website requests can now fire matching env triggers.
  • The run viewer labels website triggers and requests with their details.

No actionable issues were found. Tests were inspected but not run during this review.

Acknowledged scope from sarah-chitty-1: the Hub parser update is deferred; deadline unless checks remain tool-only; website requests are not separately recorded in the trajectory; barriers hold responses without rebuilding their contents.

Diagram

sequenceDiagram
  participant Browser
  participant Gateway
  participant Website
  participant Triggers
  Browser->>Gateway: /website/app/path
  Gateway->>Website: Forward request
  Website-->>Gateway: Response headers
  alt Website triggers registered
    Gateway->>Triggers: Match request and schedule actions
    opt Successful write
      Triggers->>Triggers: Re-check state triggers
    end
    opt Provoking-call barrier
      Gateway->>Triggers: Wait for actions or timeout
    end
  end
  Gateway-->>Browser: Stream website response
Loading

Reviews (1) · Last reviewed commit: "review: only a registered website trigge..." · Reviewed by Greptile

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>
…keep old error text; one-line comments

- The /website proxy feeds the trigger engine only while a website trigger is
  registered, so an env without one pays one check per request and its state
  triggers are never re-checked on website traffic. The request body is read
  inside the fail-open path.
- Error messages keep their previous wording as a substring (e.g. "only
  valid on action triggers"), so the existing test fragments are unchanged.
- Pre-existing docstrings are back as they were; every added comment and
  docstring is one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sarah-chitty-1
sarah-chitty-1 marked this pull request as ready for review October 19, 2026 19:42
@sarah-chitty-1
sarah-chitty-1 requested a review from a team as a code owner October 19, 2026 19:42

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