Repository navigation
Description
Proposal: make OpenCode harness calls explicit
Summary
I’d like to propose updating the AgentField OpenCode provider to build a
per-run OPENCODE_CONFIG_CONTENT overlay from the existing harness(...)
options. Each opencode run would select a fixed agent with --agent, attach
the system prompt and model to that agent, use steps: 500, apply the supplied
reasoning variant, and run with explicit noninteractive permissions.
This would let OpenCode receive the same intent that the caller already
expressed through the harness API, without changing SWE-AF-specific workflows.
SWE-AF is one consumer of this behavior, but the change should stay generic and
live in the AgentField OpenCode provider.
Problem
The harness already accepts system_prompt, tools, model, variant, cwd,
max_turns, and permission_mode. The OpenCode provider currently uses only a
subset of those values when it builds a command similar to:
opencode run --format json --dir <CWD> -m <MODEL> <PROMPT>
The system prompt is folded into the positional prompt as ordinary text,
tools is not translated into OpenCode permissions, and
permission_mode does not establish a reliable noninteractive boundary. As a
result, a run can depend on ambient OpenCode defaults, use an unintended agent,
duplicate system instructions, or wait for a permission approval that nobody
can provide.
Proposed change
For each standalone OpenCode subprocess, the provider would generate an
OPENCODE_CONFIG_CONTENT overlay with one fixed generic agent name, such as
agentfield-harness. The deployment can continue to own the shared settings
for plugins, providers, model limits, and compaction. Meaning: if you customize your opencode deployment already, this will pickup your global mcp, plugins, etc... and only superceed what is declared.
The generated agent should have this shape:
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "agentfield-harness",
"agent": {
"agentfield-harness": {
"prompt": "<SYSTEM_PROMPT>",
"model": "<MODEL>",
"mode": "primary",
"steps": 500,
"reasoningEffort": "<VARIANT>",
"permission": {
"*": "allow",
"question": "deny",
"task": "deny"
}
}
}
}reasoningEffort would be omitted when no reasoning variant is supplied. The
AgentField variant and OpenCode reasoningEffort values represent one
logical reasoning-effort setting. When it is supplied, the provider would use
the same value for the agent's reasoningEffort and the OpenCode --variant
command option rather than treating them as independent settings.
At the process boundary, the command would look like this:
OPENCODE_CONFIG_CONTENT=<GENERATED_CONFIG> opencode run --format json --agent agentfield-harness --dir <CWD> -m <MODEL> [--variant <VARIANT>] <PROMPT>
The provider would set OPENCODE_CONFIG_CONTENT in the child process
environment. The SWE-AF caller would continue to pass normal harness options;
it would not need to construct this shell command.
The provider should pass only the task prompt positionally. It should not add a
SYSTEM INSTRUCTIONS: section because the system prompt is now attached to the
OpenCode agent.
Turn and permission semantics
One important distinction is that max_turns must not be mapped to OpenCode
steps. They are different budgets: steps is OpenCode's internal agent
iteration limit and should remain a large, fixed value of 500, while
max_turns remains an AgentField/SWE-AF loop setting.
Map recognized AgentField tools to OpenCode actions as follows:
Read -> read
Write -> edit
Edit -> edit
Glob -> glob
Grep -> grep
Bash -> bash
For the initial integration, I suggest an allow baseline rather than turning
every omitted tool into a deny rule. Explicitly deny only question and task,
so an autonomous run never waits for an approval that cannot be supplied. This
is a compatibility baseline, not yet strict per-role authorization.
That choice is driven by AgentField's current structured-output protocol:
schema-backed runs ask the agent to write .agentfield_output.json, and
OpenCode maps both AgentField Write and Edit to its edit permission. A
strict edit denial would therefore break structured output. Path-limited output
permissions or a different output transport can be considered separately.
Why I think this is useful
- The system prompt becomes an actual OpenCode agent prompt instead of being
repeated in the user-facing request. --agentanddefault_agentmake the selected agent deterministic.- The model remains available as an agent default while
-mexplicitly selects
the model for a particular run. - Reasoning effort is represented consistently in the agent configuration and by
the per-run variant option. - Headless runs can proceed autonomously instead of blocking on permission
prompts. - Per-run configuration lives in the subprocess environment, so concurrent
runs do not need to share and mutate a temporary config file.
Strategy and scope
I’d keep the first change deliberately narrow:
- Add the OpenCode config-overlay construction to the AgentField OpenCode
provider. - Preserve the existing model and directory options, adding
--agentand an
optional--variant. - Make
variantan explicit harness option on SDK surfaces where it is not
already typed, while preserving existing model-variant behavior. - Keep
max_turnsoutside the OpenCode configuration. - Add provider tests for the generated configuration, command construction,
prompt handling, permission defaults, fixedsteps: 500, and variant
handling. Maintain Python, Go, and TypeScript parity where those providers
expose the same harness contract.
I would leave opencode serve/--attach, a role/profile registry, per-role MCP
authorization, and a replacement structured-output transport out of the first
patch. Attached sessions load configuration on the server and would need a
separate per-session configuration design.
Acceptance criteria
- A standalone OpenCode run receives the generated
OPENCODE_CONFIG_CONTENT. - The selected agent, system prompt, model, and permissions are visible in the
effective OpenCode configuration. stepsis always500in this overlay;max_turnsis never serialized as
steps.- A supplied reasoning variant is represented as
reasoningEffortin the
agent configuration and as--varianton the command line. questionandtaskare denied, with no generatedaskpermissions.- The positional prompt contains the task only, not a second copy of the
system prompt. - Existing caller/deployment environment and provider behavior remain
compatible when no OpenCode-specific overrides are supplied.
Feedback requested
Before implementing this, I’d appreciate confirmation that
OPENCODE_CONFIG_CONTENT is the right extension point for per-run OpenCode
configuration, that the fixed agentfield-harness name is acceptable, and that
Python, Go, and TypeScript provider parity is expected for this change. If this
direction looks right, I plan to prepare it as a focused provider-and-tests PR.
note: gpt luna helped me cleanup the proposal and make it (hopefully) more presentable.
Activity
AbirAbbas commentedon Aug 25, 2026
Triage
Classification: feature ✓
Suggested labels: enhancement
Likely affected files:
Triage by github-buddy
AbirAbbas commentedon Aug 26, 2026
Thanks for writing this up properly and asking before building it out — and sorry you closed #961 before we got to it.
I checked the proposal against opencode 1.14.33 rather than guessing, and the mechanism holds up:
--agentis a real flag onrun, and every key in your overlay (default_agent,mode,steps,reasoningEffort,permission.question,permission.task) is accepted by opencode's strict validator (an unrecognized key hard-fails withConfiguration is invalid at OPENCODE_CONFIG_CONTENT, so passing means recognized). Your claim that a deployment keeps owning mcp/plugins is correct for file-based config — with a deliberately bogus key in$XDG_CONFIG_HOME/opencode/opencode.jsonandOPENCODE_CONFIG_CONTENTset, opencode still errored on the file, so it's still loaded and merged.So: yes to the extension point, yes to
agentfield-harnessas the fixed name, yes to denyingquestion/task, and yes to not double-shipping the system prompt.steps: 500is fine as a value — make it a named constant with an env override rather than a bare literal.Three things need to change from #961 before this can land:
The overlay must merge, not clobber. In feat(sdk/python): configure OpenCode harness per run #961
env["OPENCODE_CONFIG_CONTENT"] = ...is assigned unconditionally after the caller env is copied in; a caller-supplied value is destroyed. Comparesdk/go/harness/opencode.go:139andsdk/typescript/src/harness/providers/opencode.ts:81-82, where the same env var already carries the OpenRouter attribution overlay behind a "caller didn't set it" guard. Deep-merge into the caller/ambient value instead, so attribution and the harness agent can coexist.Silent system-prompt loss needs an answer.
opencode run --agent no-such-agentdoesn't fail — it prints a warning, falls back to the default agent, and exits 0. If the overlay ever fails to reach the child (env stripped by a wrapper or container) we'd run with no system prompt at all and still report success. SWE-AF and pr-af push long system prompts through this path, so I want an opt-out (env or a harness option) that restores the inline path for one release, so they can A/B and roll back without pinning an old SDK. Related: moving a large system prompt from stdin/argv into an env var has its own size limits — worth a test.Drop the tool→permission mapping for now. With
"*": "allow"present andHarnessConfig.toolsdefaulting to all six, the mapping only ever writesallowon top ofallow. Leave it out until there's a strict mode that actually denies. Same forpermission_mode: feat(sdk/python): configure OpenCode harness per run #961 still ignores it, and the docs should say so rather than imply it's wired.On parity: yes, py/go/ts should agree eventually, but not in one PR. Land Python first with the merge fix and the opt-out, smoke it against a real model with an SWE-AF-shaped prompt, then port — the Go/TS ports have to be co-designed with the attribution overlay.
Two notes on #961 specifically: the
variantaddition toHarnessConfig/HarnessRunner.runis good and provider-agnostic — split that out and it can go in on its own. And please drop theruff formatchurn inagent.py/types.py; CI doesn't gate formatting and it collides with #913 (Pi/OMP harnesses), which is conflicting right now and touches_runner.py,types.pyand the harness docs. Suggest we get #913 sorted, then you reopen on top of it. Separately, #947 is closing out your #813.ddbaron commentedon Aug 27, 2026
Thank you for the replies and direction. I’m working on the PR now.
During testing, I found a recursion issue. AgentField-launched OpenCode sessions must not invoke AgentField orchestration themselves. In other words, OpenCode instances launched by AgentField should not use af.
This appears to be happening because the AgentField skills are registered globally and therefore become available inside those OpenCode sessions.
I’ll address this in the PR. I think the best approach is to preserve allowing desirable global OpenCode configuration, such as MCP servers and plugins, while applying a custom configuration to AgentField-launched OpenCode instances that explicitly denies access to the agentfield* skills.
ddbaron commentedon Aug 31, 2026
The proposal is now split into two independently reviewable upstream pull requests:
The variant change is independently applicable to
main; the two pull requests are related but not codependent.AbirAbbas commentedon Aug 31, 2026
Good catch on the recursion, and your diagnosis is the right one: the trigger is the globally registered skills becoming visible inside the OpenCode session, so the config-level deny of the
agentfield*skills is the correct enforcement layer — please include it in the PR.What shipped on our side in v0.1.137 (#1003): every harness subprocess gets
AGENTFIELD_HARNESS_DEPTH(set and incremented by the Python, Go and TypeScript harness CLIs), and both agentfield skills tell the model not to dispatch when it's set. That's advisory — it stops a cooperating model, not a determined one — which is why your deny matters. I looked at also strippingAGENTFIELD_SERVER/ the API key from the child env and it wouldn't help this case: every consumer hard-defaults tolocalhost:8080when the variable is absent, so a nested session would still find the control plane.The three review conditions from before still stand: merge the
OPENCODE_CONFIG_CONTENToverlay into any caller/ambient value (don't clobber), keep an opt-out that restores the inline system-prompt path for one release, and skip the tool→permission map until there's a strict mode.@santoshkumarradha one policy call: deny nested harness orchestration by default (enforced, not advisory)? My take is not this cycle — keep it advisory, let this PR's skill deny be the enforcement for OpenCode, and if we do flip a default later, deny at depth ≥ 2 so a single level of tool use still works.
ddbaron commentedon Aug 31, 2026
@AbirAbbas thanks, I believe I've met the three review conditions via the two PRs #1023 and #1024, and I just rebased each PR on latest available main.
fix(sdk/go): keep the agentfield* skill denial last in the OpenCode o…
test(sdk/typescript): assert the camelCase harness option keys are ig…
AbirAbbas commentedon Sep 21, 2026
The Go and TypeScript halves are up in #1064, under the same three conditions #1023 landed on: the overlay deep-merges into any caller/ambient
OPENCODE_CONFIG_CONTENT(so a deployment's mcp/plugins/agents and the OpenRouter attribution overlay survive),AGENTFIELD_OPENCODE_INLINE_SYSTEM_PROMPTkeeps the inline path available for a release, andtools/permission_modestay untranslated.stepsbecame a named constant with anAGENTFIELD_OPENCODE_STEPSoverride in all three SDKs.Validated by dispatching a real reasoner through a live control plane and checking what the
opencodechild actually received —--agent agentfield-harness, the task as the only positional prompt, the caller's mcp block and extra agent still present in the merged config — then replaying that exact config against opencode 1.14.33: it passes the strict validator and resolves the agent, where the same command without the overlay falls back to the default agent.One thing worth flagging from the review: Go was getting the permission ordering right only by accident (
encoding/jsonsorts map keys), so a caller rule likeskill: {"agentfield-*": "allow"}would have serialized afteragentfield*: denyand won the last-match. That's fixed with explicit ordering. Also, the TypeScript provider was readingoptions.system_promptwhileHarnessRunnerpassessystemPrompt, so a system prompt set through the public TS API never reached opencode at all — it now accepts both.feat(sdk): port the explicit OpenCode harness config overlay to Go an…