The offer
run_browse requires the agent to declare workflow: "BROWSER_TASK" — startBrowserTask checks it
explicitly (routes/instances-browse.ts:51). Across the 26 instances on this account:
| workflow |
instances |
null |
17 |
CODING_SESSION |
7 |
JOB_APPLY |
1 |
BROWSER_TASK |
1 |
The console offers "Run browser task" on all 26. ACTION_LABELS (TriggersSection.tsx:97-103)
hardcodes the seven actions with no gating, and lib/triggers.ts:44 accepts any of them for any
instance. A cron trigger that can never do anything saves cleanly and looks healthy in the list.
The ordering bug that makes it permanent
startBrowserTask runs its two preconditions in the wrong order:
await requireLiveRuntime(env, instanceId, userId); // :45 — HttpError(503), transient
…
if (caps.workflow !== "BROWSER_TASK")
throw new BrowseError("this agent is not a browser-task agent", 400); // :51 — permanent, free
The permanent, deterministic, zero-cost check runs after the transient, environmental one.
That matters because lib/triggers.ts:346 classifies the two outcomes differently, and correctly:
if (status !== 503 && status !== 409) throw e; // 400 → real failure
// 503/409 → skipped, failure_count stays 0, retry next schedule
So an agent with a runner that simply isn't a browser agent gets the honest 400 and the trigger fails
loudly. An agent with no runner at all never reaches the capability check. For the 17 cloud-only
instances the 503 fires first and the trigger is recorded as a skip — failure_count stays 0, so it
never surfaces as unhealthy — and the user is notified, every single fire, forever
(lib/triggers.ts:354):
⏭️ Scheduled run skipped — your runner is offline — run pags up. It'll try again next schedule.
The same agent's Settings tab says (SettingsTab.tsx:806):
This agent runs entirely in the cloud — no local runner (pags up) needed.
Two surfaces giving contradictory advice about one agent, and the wrong one is the one on a schedule.
pags up cannot help: up.ts skips instances whose capabilities.runtime is null, so following the
instruction produces "None of your agents need a local runner" and the trigger keeps skipping.
Fix
Swap the two checks in startBrowserTask. Ask the free permanent question before the expensive
environmental one. A non-browser agent then fails with a reason that is true and actionable, and the
trigger's failure count reflects reality. This alone converts an infinite polite lie into one honest
error.
Then gate creation, server-side. run_browse should be rejected at createTrigger for an
instance whose capabilities don't declare BROWSER_TASK — the same wiring-time posture as the
supervision rules (#183) and the connection filters (#182): reject while the human is present, rather
than at 3am on a schedule.
The gate must not live in the console. There are three equal surfaces onto this — the console, the
API, and the MCP tool create_instance_trigger — so a TriggersSection fix would leave two doors
open. Console-side, the action picker should then derive its options from the instance's resolved
capabilities (already attached to every entry of /v1/instances/my/instances), so the picker and
the validator cannot disagree.
Relationship to #353
Same component, different axis. #353 gates the sync_connector connector picker on deployment
configuration; this gates the action picker on agent capability. Both are TriggersSection
offering work that cannot happen, and whoever picks up either should do both — the shared conclusion
is that this picker's whole vocabulary is hardcoded and gated on nothing.
Class: #351, #353, #354 — a surface reporting a capability the runtime will refuse.
The offer
run_browserequires the agent to declareworkflow: "BROWSER_TASK"—startBrowserTaskchecks itexplicitly (
routes/instances-browse.ts:51). Across the 26 instances on this account:nullCODING_SESSIONJOB_APPLYBROWSER_TASKThe console offers "Run browser task" on all 26.
ACTION_LABELS(TriggersSection.tsx:97-103)hardcodes the seven actions with no gating, and
lib/triggers.ts:44accepts any of them for anyinstance. A cron trigger that can never do anything saves cleanly and looks healthy in the list.
The ordering bug that makes it permanent
startBrowserTaskruns its two preconditions in the wrong order:The permanent, deterministic, zero-cost check runs after the transient, environmental one.
That matters because
lib/triggers.ts:346classifies the two outcomes differently, and correctly:So an agent with a runner that simply isn't a browser agent gets the honest 400 and the trigger fails
loudly. An agent with no runner at all never reaches the capability check. For the 17 cloud-only
instances the 503 fires first and the trigger is recorded as a skip —
failure_countstays 0, so itnever surfaces as unhealthy — and the user is notified, every single fire, forever
(
lib/triggers.ts:354):The same agent's Settings tab says (
SettingsTab.tsx:806):Two surfaces giving contradictory advice about one agent, and the wrong one is the one on a schedule.
pags upcannot help:up.tsskips instances whosecapabilities.runtimeis null, so following theinstruction produces "None of your agents need a local runner" and the trigger keeps skipping.
Fix
Swap the two checks in
startBrowserTask. Ask the free permanent question before the expensiveenvironmental one. A non-browser agent then fails with a reason that is true and actionable, and the
trigger's failure count reflects reality. This alone converts an infinite polite lie into one honest
error.
Then gate creation, server-side.
run_browseshould be rejected atcreateTriggerfor aninstance whose capabilities don't declare
BROWSER_TASK— the same wiring-time posture as thesupervision rules (#183) and the connection filters (#182): reject while the human is present, rather
than at 3am on a schedule.
The gate must not live in the console. There are three equal surfaces onto this — the console, the
API, and the MCP tool
create_instance_trigger— so aTriggersSectionfix would leave two doorsopen. Console-side, the action picker should then derive its options from the instance's resolved
capabilities(already attached to every entry of/v1/instances/my/instances), so the picker andthe validator cannot disagree.
Relationship to #353
Same component, different axis. #353 gates the
sync_connectorconnector picker on deploymentconfiguration; this gates the action picker on agent capability. Both are
TriggersSectionoffering work that cannot happen, and whoever picks up either should do both — the shared conclusion
is that this picker's whole vocabulary is hardcoded and gated on nothing.
Class: #351, #353, #354 — a surface reporting a capability the runtime will refuse.