Give the roster's researcher role a working model again on a subscription
cookie lane, with no API key and no new Brigade browser code, by adding
oracle as a one-shot CLI adapter.
Three facts set the shape of this phase:
research/llm.py:58resolve_backendalready returns aCliBackendfor aresearcheragent that declarescli, andresearch/engine.py:64only ever calls.complete(). Backend selection needs no change.research/sources/web.pyalready shipsPlaywrightProvider, a headless Chromium tier that searches DuckDuckGo and carriestrust = "browser", behindpip install 'brigade[research]'. Research already has web grounding. The gap is a planning and synthesis model, not search.- Agent Pantry already syncs the encrypted
gemini.google.comcookie jar between machines, andbrigade pantry expiry-alertalready warns before it lapses. Oracle's browser engine reads exactly those cookies from the default Chrome profile.
So the whole change is one adapter. Pantry supplies the cookies, oracle drives the session, Brigade shells out and reads text back.
- Require a user-installed
oracle(MIT, npm@steipete/oracle). Brigade does not install it, does not bundle Node, and does not add it to the component manifest in this phase. - Browser engine only (
--engine browser). The adapter never emits an API-key path, so the lane cannot silently fall off the subscription rail. - Read-only is hard by construction: oracle has no filesystem write path, so no flag and no prompt instruction are needed to enforce it.
- Scope is the
researcherrole. No run transport, no ChatGPT Pro seat, no multi-model panel, no oracle sessions or--followup, no MCP bridge, no cost reporting. Those are later phases. - Known side effect: registering the adapter makes
cli = "oracle"assignable to any seat, not only the researcher, because_ADAPTERSis a global table. That is accepted rather than gated, but it means the read-only enforcement contract has to hold forbrigade runfrom day one. - Keep the existing DuckDuckGo web tier as the search provider. Oracle does planning, query generation, and synthesis only.
Four layers, three of them already built:
| Layer | Component | Change |
|---|---|---|
| Auth | Agent Pantry cookie sync | none |
| Driver | oracle --engine browser |
none, external binary |
| Adapter | src/brigade/agents.py |
new, small |
| Caller | roster researcher |
config only |
| Timeout | src/brigade/research/llm.py |
one floor, see below |
The one non-adapter change: research/engine.py:138 asks for timeout=30 on
its planning call and research/types.py:50 defaults to timeout=60, both
hardcoded at the call site. A browser round trip will not meet 30 seconds, so
the lane fails on its first request without a floor. The roster already carries
per-agent timeout_seconds and resolve_backend currently discards it, so
CliBackend takes it as a min_timeout that raises short engine timings and
never lowers generous ones. No new config concept, no edits to the engine's
literals, and seats that declare no timeout_seconds keep today's behavior.
Adapter surface in src/brigade/agents.py:
def _oracle_argv(prompt: str, read_only: bool, sandbox: str | None, cwd: Path | None) -> List[str]:
# Oracle has no filesystem write path, so read-only needs no flag and no
# prompt instruction. --engine browser pins the run to the cookie lane so
# the adapter can never fall back to an API key.
return ["oracle", "--engine", "browser", "-p", prompt]Registrations:
_ADAPTERS["oracle"] = _oracle_argvREAD_ONLY_ENFORCEMENT["oracle"] = "hard"_MODEL_PIN["oracle"] = ("--model", _pin_after_cmd), producingoracle --model gemini-3.1-pro --engine browser -p <prompt>
command_for needs no entry: it falls through to the ref name, and the binary
is already called oracle.
Roster:
[agents.researcher]
cli = "oracle"
model = "gemini-3.1-pro"
role = "researcher"
timeout_seconds = 300Data flow: brigade research run "<question>" -> DeepResearcher ->
llm.complete() -> CliBackend("oracle", "gemini-3.1-pro") -> run_agent ->
_ADAPTERS -> subprocess -> stdout -> validate_final_output -> engine.
Models the browser engine accepts: gemini-3.5-flash, gemini-3.1-pro, and
gemini-3-deep-think (browser-only; oracle rejects it in API mode).
- oracle absent.
resolve_agent_executablealready returnsfailure_kind="command-not-found"atfailure_phase="dispatch". No work. - cookies expired. This must not surface as a generic nonzero exit.
_oracle_auth_detailrecognises oracle's login and expired-session messages and points the operator atbrigade pantry expiry-alert. It is a sibling of_provider_preflight_detail, not a branch inside it: that function is about workspace trust, a concept oracle does not have. Both failure paths inrun_agenttry the auth detail first, since it is the more specific diagnosis, and an auth hit reportsfailure_kind="browser-auth"rather than"workspace-trust". Keeping those kinds distinct matters downstream: outcome capture and the model scorecard readfailure_kind, and a stale cookie jar is an operator action while a trust refusal is a workspace problem. The first implementation shared the preflight branch and mislabelled every oracle auth failure asworkspace-trust; the regression tests that caught it exercise both call sites throughrun_agent, not the detail function alone. - browser too slow for the engine's timings. Not a hang, the common case.
Fixed by the
min_timeoutfloor above, driven by the seat'stimeout_seconds..brigade/research.tomlis the wrong home for this:Capshas no timeout field, andCaps.buildsilently drops unknown keys, so a config-only attempt would look applied and do nothing. - genuine browser hang. Covered by
run_agent(timeout=...)once the floor raises it to the seat's declared ceiling. - partial or scraped garbage.
validate_final_outputalready runs atagents.py:1182. If oracle's stdout wraps the answer in progress chrome, add an extraction function following the_parse_grok_final_outputprecedent (agents.py:360) rather than loosening validation. - no silent fallback. If the researcher role fails,
researchraises rather than quietly degrading to another seat, matching the existingNoResearcherErrorsemantics.
- Unit test
_oracle_argvargv construction, including model pin position. - Test that the adapter never emits an API-mode argv.
- Test
READ_ONLY_ENFORCEMENTreportshard, and thatbrigade run --read-onlyraises no soft-enforcement warning for an oracle seat. In scope despite the researcher-only boundary, because registering the adapter makes oracle dispatchable bybrigade run. - Test roster validation accepts a
cli = "oracle"researcher and thatresolve_backendreturns aCliBackend. - Test the
min_timeoutfloor raises a short engine timeout, never lowers a generous one, and leaves seats withouttimeout_secondsunchanged. - Test the expired-cookie preflight detail string.
- Run focused tests and
./scripts/verifythroughbrigade work verify run. - Live smoke: one
brigade research runagainst real synced cookies, recording the result or the environmental blocker.
What is proven, and by what:
| Claim | Evidence |
|---|---|
argv shape, model pin position, no --heartbeat, no API path |
unit, tests/test_agents_oracle.py |
argv survives a real exec |
stub binary on a narrowed PATH records its own argv |
| stdout is the answer channel at the Brigade layer | stub returns markdown, run_agent returns it verbatim |
| auth failure on the nonzero-exit path | run_agent returns failure_kind="browser-auth" |
| auth failure on the empty-output path | separate branch, same assertion |
| auth beats workspace-trust when both patterns appear | stub emits both, auth wins |
non-oracle seats keep workspace-trust |
codex regression guard |
| timeout floor survives the real engine | DeepResearcher run asserts 300, not the engine's 30 |
roster accepts cli = "oracle" |
clears is_known and limits.allow_models |
Still unproven, and only oracle itself can settle it: whether real oracle stdout carries progress chrome around the answer, and whether the browser session actually completes against synced cookies.
The adapter dispatches correctly and fails cleanly when the binary is absent.
agents.run_agent("oracle", "hello") returned ok=False,
failure_phase="dispatch", failure_kind="command-not-found", detail
oracle not installed, and build_argv produced exactly
['oracle', '--model', 'gemini-3.1-pro', '--engine', 'browser', '-p', 'hello'].
A full browser round trip could not run, blocked twice on this machine:
oracleis not installed (command not found, and no global npm package). Brigade does not install it by design.brigade pantry statusreports the agentpantry build rejected by version policy (unreleased or non-semver build; expected released >= 0.5.0), so the cookie substrate is not healthy here either.
Consequently the settled stdout decision is not yet empirically confirmed.
It rests on reading oracle's src/cli/renderOutput.ts (if (!richTty) return markdown;, richTty defaulting to process.stdout.isTTY) plus never passing
--heartbeat. The first real run should check whether stdout carried only the
answer; if it did not, add an extraction function following the
_parse_grok_final_output precedent rather than loosening
validate_final_output.
- Is stdout clean enough to skip an extraction function? Yes. Oracle's
src/cli/renderOutput.tsreturnsmarkdownunrendered whenrichTtyis false, andrichTtydefaults toprocess.stdout.isTTY. Brigade captures subprocess pipes, so that is false and no ANSI reaches stdout. Heartbeat progress is opt-in behind--heartbeat, which the adapter never passes. No extraction function, and a test asserts the flag is never emitted.
- Whether to map Brigade's
reasoningpin onto--browser-thinking-time <light|standard|extended|heavy>. Cheap and a natural fit, but not needed for research synthesis. Deferred to the ChatGPT Pro phase, where thinking depth is the whole point.
- Oracle's browser mode is labelled experimental by its author. It is a
third-party Node tool driving a web UI that can change without notice. The
cli = "oracle"seam keeps it replaceable: a future Brigade-owned driver swaps the adapter without touchingresearch/. - Cookie-driven automation of a consumer web session is a grey area against provider terms. This is a single-operator machine lane. It should not become a fleet default or a documented supported install path.
- ChatGPT Pro reviewer seat via a
transport = "browser"roster entry, with receipts and outcome capture. acpx is the sizing precedent. --browser-thinking-timereasoning pin.- Component manifest entry and a station-style doctor, if the lane proves durable enough to be worth pinning a version against.