Skip to content

Gate the controller's autoresearch lane behind a feature and a knob - #142

Merged
wseaton merged 5 commits into
mainfrom
controller-autoresearch-feature
Sep 23, 2026
Merged

wseaton merged 5 commits into
mainfrom
controller-autoresearch-feature

Conversation

@wseaton

@wseaton wseaton commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Playbooks are the controller's core. The machine-initiated scored lane (GitHub discovery and triage, ranking, scope proposal and its approval gate, builds, turns, scored loop runs) now needs the autoresearch cargo feature to be compiled in and CONTROLLER_AUTORESEARCH=true to run. Both default off, and boot refuses the knob on a build without the feature. The published controller image and release binaries are built with the feature, so a deployment that wants the lane only sets the variable.

With the lane off: its routes are not served (own OpenAPI doc, router fragment and authz binding table), its discovery polls and startup steps don't run, and reconcile moves only playbook launches. /api/approvals stays core because it also lists pending pack imports. /api/version reports autoresearch, and the UI hides the lane's nav and redirects its routes to the playbooks.

The first commit moves the lane-neutral engine helpers (resolve_bin, ITEM_ENV, fetch_object, the draft-PR push) from issues::engine to runs::engine so the rest of issues can be gated.

Stacked on #139. The engine half is #141, and a deployment turning the lane on needs both.

Tested: controller clippy clean in both feature sets; tests pass in both (lean 1083 lib plus the playbook standing-launch e2e; with the feature 1524, the same as before plus the new ones); modgraph clean; UI check plus 531 vitest. A lean just controller-local ran examples/playbook to a valid verdict with /api/issues returning 404.

An open guard (no API token, no issuer) resolved every request to
anonymous, a viewer, so a controller without auth could be looked at but
not driven. CONTROLLER_DEV_IDENTITY names the open-guard caller instead,
drops whatever X-Auth-Request-* headers the client wrote, and refuses to
boot on a guarded deployment where it would name every caller.

Running a draft through the local executor turned up four more breaks:

- A draft stores files with no mode, so its scripts unpacked 0644 and
  failed with EACCES. The pod mounts the pack with default_mode 0755;
  the local run now does the same.
- FORGE_STORAGE_ROOT was never set, so a report task read the pod path
  /var/lib/forge. The local run gets a per-run forge dir.
- run_env stripped HOME and USER, which the host claude CLI needs to find
  its login (the macOS keychain lookup keys on USER). The subprocess is
  the same OS user, so withholding them isolated nothing. It also
  stripped OPENSHELL_PODMAN_SOCKET, the only way the engine finds podman
  on macOS.
- Local mode refused backend = "openshell" as cluster-only. The engine
  boots the sandbox on the podman compute driver by default, and
  examples/revise-loop runs that way through the local controller. An
  openshell pack still needs sandbox_image, now in both modes.

With no inferable kube config the overrides ConfigMap watch is skipped
instead of warning every 30s. The recipe pins KUBECONFIG=/dev/null so a
laptop controller never writes into whatever cluster the shell points at.

Assisted-by: Claude
Local mode now dispatches openshell packs, so the refusal this test
asserts comes from a pod deployment with no deploy profile.

Assisted-by: Claude
Playbook launches, drafts and local runs reach into issues::engine for
resolve_bin, ITEM_ENV, fetch_object, repo_clone_url and the draft-PR
push. Those are lane-neutral; issues::engine keeps only scope and
grounded rank, so it can be gated with the rest of autoresearch.

Assisted-by: Claude
Playbooks are the controller's core; the machine-initiated scored lane
(GitHub discovery and triage, ranking, scope proposal and its approval
gate, builds, turns, scored loop runs) is now compiled in by the
`autoresearch` cargo feature and switched on by CONTROLLER_AUTORESEARCH.
Both default off. Boot refuses the knob on a build without the feature.

With the lane off its routes are not served (they live in their own
OpenAPI doc and router fragment, and their authz bindings in their own
table), its discovery polls and startup steps do not run, and reconcile
moves only playbook launches. /api/approvals stays core because it
also serves the pending pack imports; its scope and kept-PR sections
come back empty. /api/version reports the lane so the UI hides its
pages and redirects their routes to the playbooks.

genai and gcp_auth become optional, used only by the ranker. The
autopilot flag on ApiState is optional and its routes answer 503 when
no daemon loaded one. The tokio test-util dev-dependency is declared
directly, since it used to arrive only through feature unification.

CI clippies and tests the controller in both sets, sqlx prepare checks
with all features, and the published image and release binaries are
built with the feature.
@github-actions

Copy link
Copy Markdown

Docs preview for this PR is built and attached as the docs-preview artifact.

Download docs-preview, unzip, and open index.html in a browser.

Rebuilt for eec0ec4.

@wseaton
wseaton merged commit d1c37ff into main Sep 23, 2026
5 checks passed
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