Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions packages/extension/src/opencode_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ export function resolveJuliaProject(configValue: string): string {
}

/** Build the OPENCODE_CONFIG_CONTENT value: a config object that injects the
* amico AGENTS.md as a top-level `instructions` entry AND auto-allows the
* amico AGENTS.md as a top-level `instructions` entry, PINS the team model
* (AMICODE_MODEL — never the user's global model), and auto-allows the
* permissions the solve workflow needs. opencode MERGES this over the user's
* global config (model/provider preserved) for every session, independent of
* the session's working directory.
* global config (non-conflicting user keys preserved) for every session,
* independent of the session's working directory.
*
* Why the `permission` block: the agent reads the bundled template at an
* absolute path *outside* the session's working dir and writes scratch to
Expand Down Expand Up @@ -71,10 +72,21 @@ export function resolveJuliaProject(configValue: string): string {
* `webfetch` is intentionally NOT set — the solve flow never fetches a URL. */
const SCRATCH_DIR = "/tmp/amicode-work"; // matches AGENTS.md step 2/3

/** The ONE model every amicode session runs — pinned, never read from the
* user's global opencode config. Rationale: sessions were silently falling
* back to opencode's free anonymous model (`big-pickle`) on machines with an
* empty global config, which is exactly the "free-model phrasing variance"
* behind protocol breaks (invented Other options, JSON drift) — and two
* teammates debugging the same interview must be running the same model.
* Requires provider auth (`opencode auth login`); with none, sessions fail
* loudly instead of silently degrading. */
export const AMICODE_MODEL = "anthropic/claude-sonnet-5";

export function buildOpencodeConfigContent(agentsPath: string, templatePath: string, runsRoot: string): string {
const templatesDir = path.dirname(templatePath);
return JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: AMICODE_MODEL, // pinned team-wide — overrides any user global model (deep-merge, ours wins)
instructions: [agentsPath],
permission: {
bash: "allow",
Expand Down
14 changes: 8 additions & 6 deletions packages/extension/test/opencode_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, mkdirSync, writeFileSync, readFileSync } from
import { tmpdir, homedir } from 'node:os'
import { join } from 'node:path'
import { execFileSync } from 'node:child_process'
import { prepareOpencodeProject, resolveJuliaProject, buildOpencodeConfigContent } from '../src/opencode_config'
import { prepareOpencodeProject, resolveJuliaProject, buildOpencodeConfigContent, AMICODE_MODEL } from '../src/opencode_config'

function fakeExtRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'extroot-'))
Expand Down Expand Up @@ -87,11 +87,12 @@ describe('buildOpencodeConfigContent', () => {
// binary isn't present (e.g. minimal CI before `fetch:opencode`).
const OC_BIN = join(__dirname, '..', 'vendor', 'opencode', `${process.platform}-${process.arch}`, 'opencode')
describe.skipIf(!existsSync(OC_BIN))('opencode config injection + merge (1.17.3)', () => {
it('injects instructions/permission AND preserves the user global model + permission', () => {
it('injects instructions/permission, PINS the team model over the user global, preserves other user keys', () => {
const home = mkdtempSync(join(tmpdir(), 'ochome-'))
mkdirSync(join(home, '.config', 'opencode'), { recursive: true })
// A user global config with a distinctive model + permission key — both must
// survive the deep-merge under OPENCODE_CONFIG_CONTENT.
// A user global config with a CONFLICTING model + a distinctive permission
// key: the model must LOSE to the pin (one team, one model — no silent
// big-pickle fallback, no per-machine drift); the permission key must survive.
writeFileSync(join(home, '.config', 'opencode', 'opencode.json'),
JSON.stringify({ model: 'anthropic/claude-sonnet-4-6', permission: { doom_loop: 'deny' } }))
const agentsPath = join(home, 'AGENTS.md') // the exact file our `instructions` must point at
Expand All @@ -108,8 +109,9 @@ describe.skipIf(!existsSync(OC_BIN))('opencode config injection + merge (1.17.3)
// the runs-root grant survives the real deep-merge — the agent's post-solve
// FINISHED/result.toml/run.log read-backs must not "ask" on every run:
expect(cfg.permission.external_directory[join(home, '.amico', 'runs', 'default') + '/**']).toBe('allow')
// the user's global config SURVIVED the deep-merge:
expect(cfg.model).toBe('anthropic/claude-sonnet-4-6') // provider/model preserved (Q129 needs this)
// the team model pin BEATS the user's global model (verified on the real binary):
expect(cfg.model).toBe(AMICODE_MODEL)
// …while non-conflicting user keys still survive the deep-merge:
expect(cfg.permission.doom_loop).toBe('deny') // user permission key preserved (#22)
})
})
Expand Down
Loading