Skip to content

bot: Move from opencode 1.19 (opencode-ai) to opencode 2 (@opencode/cli) - #1

Closed
jmarrero-bot wants to merge 2 commits into
mainfrom
bot/opencode2
Closed

jmarrero-bot wants to merge 2 commits into
mainfrom
bot/opencode2

Conversation

@jmarrero-bot

@jmarrero-bot jmarrero-bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Moves botd, bot-opencode and the coordinator's opencode backend from opencode-ai@1.19.0 to @opencode/cli@2.1.1: the system config in v2's format (policies, permissions, agents), the ported check-opencode-config.py, and opencode-copilot, since opencode 2 keeps logins in its database and no longer reads OPENCODE_AUTH_CONTENT. The first commit empties the login tables in the coordinator's archived opencode database, which is where v2 keeps the token.

The issue's first question: yes. In v2 the system config (/etc/opencode) loads after every file a session can write, agents' rules are joined as lists in load order (v1 merged them by key, which let a crafted file outrank the builder's rules), and the system config's policies are evaluated after everyone else's. Only an OpenCode Console organization's policies come later, which needs a Console login. I moved the provider pin there and added policies against plugins, MCP servers and the question tool. The check below shows planted config of every kind losing, also without the loop's clean. I kept the per-start clean anyway: v2 takes a login only into a new database, which the archive gives it, and the clean also drops planted instructions and prompts. Details and source references are in the gist.

Containment. All 9725 builder deny patterns are the same as in 1.19, now after shell * allow, with subagent * deny. The question tool, the provider pin and the clean all still apply. Plugins and MCP servers are now refused too. Subagents still can't start their own: 1.19 never applied its subagent_depth: 2. I checked that nothing got weaker three ways: against the opencode 2.1.1 source, with the check and a control run, and end to end through opencode's own permission evaluation.

Testing

On devspace jmarrero-devspace-39536677514 (16 cores, RHEL 10), at 41d1329. Scripts and full logs: https://gist.github.com/jmarrero-bot/bfcd3328fae650406964de2fbcb77566

  • Unit tests: python3 -m unittest in bot/ passed: 128 tests at the head, 123 at the first commit, with test_render.py against jmarrero-forge/homegit 09dceb6. New tests cover the v2 config, the archive scrub (including a WAL left by a killed process, and the token's bytes gone from the file) and opencode-copilot's login formats and refusals.
  • Images: bot/agent.Containerfile (opencode v2.1.1) and bot/coordinator/ built with podman.
  • check-opencode-config.py in the coordinator image exited 0 (30 cases). It plants a project and a global opencode.json, ~/.opencode, an agent file, a crafted file that repeats the managed rules and adds an allow, a plugin and an MCP server, with project config off and on, with and without the loop's clean, and also into a running server. Every case kept the builder refusing git push, the question tool refused for every agent, only github-copilot listed, no plugin or MCP server started, and the login imported. A control with the policies removed from the system config failed as it should: the free provider was listed, the plugin and the MCP server started, and planted rules allowed the question tool. The builder still refused git push there, because its rules come last.
  • End to end, with a fake OpenAI-compatible model driving opencode run, opencode refused the builder's git push, bash -c 'gh pr merge 1' and cd /tmp && git commit, and env git push with planted allow rules and policies. It allowed the builder's git status and the coordinator's own git push. The question tool wasn't offered, also with planted allows; without the policy it was. A general worker can't start a subagent.
  • Failover smoke test: the real loop, the image's backends.toml (only Claude's command and opencode's probe swapped), a fake Claude session and the real opencode 2 TUI on the fake model. At 96% Claude handed off and its worker was stopped. The TUI started with --standalone --auto --prompt, read the rendered AGENTS.md, and ran bot-coordinator-loop started itself, and the takeover note reached it. After a kill -9 of the loop and a restart, opencode started again. The archived database had 0 credentials and kept its session, and the token appeared nowhere in the archive. After SIGTERM, no process carrying the generation was left. PASS.
  • botd's command and bot-opencode (two runs, then --models) produced the model's answer on stdout. bot-opencode left no database behind, and the login came back unchanged after auth export and --login-from-export.

Not tested: a real Copilot login, model and quota probe, and the interactive device login in copilot-login.sh, which need the bot machine. The format conversion is covered by the unit tests and the round trip above. 2.1.1 came out on 2026-10-20.

The 1.19 pin was to stay through the failover live run (jmarrero-forge/tracker#11, still open), so this should merge after it, unless the live run is done on this branch.

Should workers start their own reviewer, as jmarrero#18's design intended? That needs subagent_depth: 2 plus subagent allowed for general. I left both as 1.19 effectively had them.

Fixes jmarrero-forge/tracker#14

Generated-by: https://github.com/jmarrero-bot#llms


Review draft in jmarrero-forge, not upstream yet. This section is removed when the PR is opened upstream.

  • Upstream: jmarrero/atomic-setup, base main
  • Board item: PVTI_lADOFBIZ7c4BlZPrzhAeHcM
  • Fork CI: off; the devspace testing described above is this PR's CI, and upstream CI runs once it is opened there

To review:

  • Approve, or comment /promote on a line of its own, to open it upstream, ready for review. Either covers only the commits pushed so far.
  • If upstream requires DCO, approving also signs off: promote adds Signed-off-by: Joseph Marrero Corchado <jmarrero@redhat.com> to the commits lacking it (the bot's and yours; anyone else's only if you ask), with you as committer.
  • Add a /draft line (in the same comment or before) to open it upstream as a draft (/ready undoes that).
  • Close to drop it.
  • Edit the title and description freely: they become the upstream PR's. Review comments are squashed into the commits they concern, with a reply here.

Bot run: session 965dfd54, agents a0fd4f8e +1 · claude-opus-5-5 78.0M in (98% cached) / 236k out · ~$28.11 inference + ~$2.46 compute (est., list prices) · 1h 9m wall · 15.6 core-h, Actions run 39536677514

opencode 2 keeps its logins (and any OpenCode Console account) in its
SQLite database instead of auth.json, so archiving its data directory as
it is would put the Copilot token into every archive, which jmarrero#18 was
careful to avoid. A backend's new `scrub` names databases in an archived
directory and the tables to empty there; the rows are deleted with
secure_delete and the file vacuumed, and its write-ahead log is
checkpointed first, since a stopped session can leave writes there. A
database that can't be scrubbed is dropped from the archive rather than
kept.

Prep for moving the opencode backend to opencode 2.

Generated-by: AI
opencode 2 is a separate package with a new config format and a new
config merge, and it no longer reads OPENCODE_AUTH_CONTENT, which botd,
bot-opencode and the coordinator's opencode backend all relied on
(jmarrero-forge/tracker#14).

The system config (/etc/opencode) now loads after every file a session can
write, and opencode 2 joins agents' rules as lists in load order instead of
merging them by key, so the builder's shell rules come last whatever a
session's own config says. Its policies are a separate layer that can
only take things away, evaluated after every other file's: they hold the
Copilot provider pin and now also refuse plugins, MCP servers and the
question tool, so code a session plants in its config can't run either.
The builder's 9725 patterns are the same as before, only in the new format
(bash -> shell, task -> subagent), with the shell and its parser pinned.
subagent_depth goes back to its default: opencode 1 never applied the 2 it
was set to (it refused the task tool to every subagent), and opencode 2
would. check-opencode-config.py is ported to all of that: it plants
config of each kind (and a plugin and an MCP server) with project config
off and on, with and without the loop's clean, and into a running server,
which reloads config files as they change.

So the per-start clean is no longer what protects those rules. It stays,
because opencode 2 takes a login only into a new database: opencode-copilot
imports OPENCODE_AUTH_CONTENT with `opencode auth import` and refuses an
existing database, and the coordinator's archived data directory (its
logins scrubbed), botd's fresh job containers and a database of bot-opencode's
own per run give it one. The secret keeps opencode 1's auth.json format,
so the stored login and the usage probe work unchanged; copilot-login.sh
converts a new login into it.

Every run uses --standalone, a private server tied to the client, because
by default opencode 2 attaches to a shared background service that would
outlive the session and its generation. The TUI has no --model any more,
so the backend passes the models in OPENCODE_CONFIG_CONTENT, rather than
as {env:...} in the system config, where an unset variable would make the
model invalid.

Generated-by: AI

@jmarrero-bot jmarrero-bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opencode review: changes needed

Reviewed head 41d1329b232e26a721676058f2bffced93869cdc against 3fa0959.

  1. P2 — Interrupted archiving retains the Copilot credential. bot/coordinator/coordinator-loop.py:825-828 moves the live database into the archive before scrubbing it. Killing the loop between these steps, or an error during a cross-filesystem copy, leaves an unsanitized database/partial copy there. Subsequent starts never revisit it; it survives until ordinary pruning. Scrub before publishing the archive and recover interrupted staging on restart, or explicitly sanitize all existing/partial archives before proceeding. Add interruption and failed-copy tests that check token bytes in the entire archive. The current killed-process/WAL test covers an interrupted opencode process, not an interrupted cleanup.

  2. P2 — The new scrub follows a symlinked data directory. bot/coordinator/coordinator-loop.py:826-828,870-875 can move ~/.local/share/opencode as a symlink, then open <archived-link>/opencode.db. Checking only the final component with islink(path) misses the parent link: DELETE/VACUUM and sidecar unlinking operate on its target. This violates clean()'s existing no-symlink contract and can damage another database without malicious intent. Require a real archived directory before descending, and test a symlinked source with its target and sidecars unchanged.

Policy verdict: bot/coordinator/opencode-config.py:113-146,159-166 has the right ordering for file-based overrides. I checked upstream tag v2.1.1 (ba546ec): config.ts splits managed policies to the front and managed settings to the end; config/plugin/agent.ts appends agent rules in that order; managed-policy.ts reverses policy documents; permission.ts rejects configured denies before saved allows/autoaccept. Project/global config, agent files, ~/.opencode and OPENCODE_CONFIG_CONTENT therefore cannot append an allow after the system builder denies through that mechanism. Console organization policies are the stated exception. This remains a same-user mistake guard: session permissions follow agent permissions, and provider endpoints/formatter commands remain writable. The existing README acknowledges that boundary; do not treat the provider-ID pin as an endpoint or credential-exfiltration boundary.

Credential follow-up: bot/opencode-copilot:90-97 correctly uses stdin rather than argv and redacts import errors. Successful DB/WAL scrubbing is covered by the evidence. However, it retains OPENCODE_AUTH_CONTENT in both the import child and the final opencode environment; shell children inherit it. An accidental env dump can enter logs/transcripts, which the three-table scrub does not erase. This exposure predates the migration, so it is not a new blocking finding, but v2 no longer needs the variable: remove it from the child environments after parsing, keeping the loop/probe's parent copy. A malicious same-UID process can still read the live database.

Pin/deployment: bot/agent.Containerfile:26-35 uses an exact version and disables automatic updates, which is appropriate. The tag is dated 2026-10-20; age alone is not a defect, but I would not replace the production coordinator on this evidence alone. Require an operator-run real-Copilot canary (login/import, actual model request and quota probe) and the pending live failover in tracker jmarrero#11, on this exact image/head, before rollout. The fake model does not exercise those paths.

Verification: read the full diff/commit messages, tagged upstream source, and the linked gist's scripts/logs (128 unit tests, both image builds, 30 config cases, negative control, tool-level denials and fake-model failover). Those are worker results, not independently rerun tests. I reran git diff --check, bash -n on both changed shell scripts, and bot-git check; all passed. No build/runtime tests were rerun. The two archive failure cases above are absent from the evidence.

Review guide: prioritize archive publication/scrubbing, then login import and policy generation; the small setup/model-list/comment-only edits are safe to skim. Structured guide attached below for the review app.

@jmarrero-bot jmarrero-bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review guide for head 41d1329b23: where to look closely and what is safe to skim. It is advice from the bot's reviewer and doesn't replace reading the diff; the review app walks it.

Moves botd, bot-opencode and the coordinator's opencode backend from opencode-ai 1.19 to @opencode/cli 2.1.1: the system config in v2's format (policies, ordered permissions, agents), a ported check-opencode-config.py, and opencode-copilot, which imports the Copilot login into a fresh database because v2 no longer reads OPENCODE_AUTH_CONTENT. The first commit empties the login tables of the archived opencode database. The reviewer checked the containment claims against the v2.1.1 source (config.ts load order puts the system config's settings last and its policies first in the list, so they are evaluated last; permission.ts evaluate is last-match-wins; agent rules are appended per document in load order) and re-ran the check and an end-to-end builder deny on a devspace. The risks: the pin is a day-old release with 2.1.2 already out, and 2.1.2 fixes a 60-minute inactivity eviction that can interrupt a long-running coordinator. Planted formatter commands still run at once through v2's config hot reload, which bypasses the builder's shell rules, and a one-line formatter: false in the system config would close that. The archive scrub also needs a careful read.

Hotspots

  1. look-closely · logic — bot/agent.Containerfile:26 (41d1329): 2.1.1 is a day old and 2.1.2 is already out. 2.1.2 fixes location eviction after 60 minutes without durable activity, which interrupts running sessions (a long tool call or background job). The precedence code is the same in 2.0.32 through 2.1.2, so 2.1.2 costs only a re-run of the check.
  2. risky · security — bot/coordinator/opencode-config.py:123-145 (41d1329): No formatter: false here. The reviewer confirmed on 2.1.1 that a formatter planted in ~/.config/opencode runs its command when the builder writes a .txt, bypassing the shell rules (v2 hot-reloads it too). With formatter: false in this file it didn't run. 1.19 needed a SIGUSR2 for the same, so the risk was already there, but the fix is one line.
  3. note · security — bot/coordinator/opencode-config.py:113-118 (41d1329): Verified against v2.1.1: system agent rules are appended after every session-written document's, and evaluate() is findLast. A deny pattern is only reached after the system's own shell * allow, so any shell rule a session plants earlier never decides.
  4. note · api — bot/coordinator/backends.toml:101 (41d1329): OPENCODE_CONFIG_CONTENT ranks below the system config's settings in v2 (config.ts load), and the session can't change the loop's environment or the read-only bot-harness config, so this doesn't weaken anything. An unset BOT_OPENCODE_REVIEW_MODEL now keeps the whole opencode failover from starting, not just second-opinion.
  5. look-closely · security — bot/coordinator/backends.toml:85-86 (41d1329): keep still carries an opencode 1 auth.json (a token file) from start to start, although v2 never reads it. Dropping keep, or deleting the file, is cleaner than the README's 'delete it by hand once'.
  6. look-closely · error-handling — bot/coordinator/coordinator-loop.py:824-831 (40127ef): The database is moved into the archive before it is scrubbed. If scrub_database raises an OSError other than FileNotFoundError, the start fails but the unscrubbed database stays in the archive. Unlikely, but scrubbing before the move, or removing the archived database on any exception, would make that fail closed.
  7. note · security — bot/coordinator/coordinator-loop.py:864-895 (40127ef): The scrub covers all of v2.1.1's token-bearing tables (credential, account, control_account). Copilot uses the GitHub OAuth token directly, so no other cached token exists. secure_delete plus a checkpoint then VACUUM is sound, and the unit test checks the token's bytes are gone.
  8. note · security — bot/opencode-copilot:86-97 (41d1329): The login goes to auth import on stdin, not argv, and errors are redacted. OPENCODE_AUTH_CONTENT is still inherited by opencode and every tool process, as in 1.19. v2 no longer needs it, so it could be dropped before exec, though the token also sits in the readable live DB.
  9. note · test-gap — bot/coordinator/check-opencode-config.py:89-96 (41d1329): The check models opencode's decision in Python from /api/agent rather than asking opencode to run anything. That is fine as a regression check, but only the gist's e2e (not in the repo) exercises opencode's own evaluation. It also doesn't plant a formatter.
  10. note · security — bot/coordinator/README.md:226-232 (41d1329): This documents that planted formatter commands and a provider address take effect at once. The commit message's claim that code a session plants 'can't run either' therefore holds only for plugins and MCP servers.

Safe to skim

  • bot/coordinator/backend-opencode.md: Prose: task to subagent tool renames for the coordinator skill
  • bot/coordinator/operator-notes.md: One-line doc update
  • bot/SETUP.md: One-line doc note
  • bot/coordinator/bot-coordinator.container: Comment only
  • bot/test_opencode_copilot.py: Data-driven unit tests of the login format conversion

@jmarrero-bot

Copy link
Copy Markdown
Author

Closed unmerged in favor of jmarrero-forge/tracker#35 (Sync with cgwalters harness). The branch stays for reference. Review fixes are on the bot's local branch wip/opencode2-review-fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Move the bot from opencode 1.19 (opencode-ai) to opencode 2 (@opencode/cli)

1 participant