Agent Configuration Layering
Context
autograph runs AI agent containers (dev-agent and metalab-agent) that share a single Docker image and entrypoint. They use OpenCode v2 as their agent runtime. The current configuration mechanism has structural problems.
Current mechanism
The entrypoint renders an OpenCode configuration at each container startup:
validate-env.sh requires a provider ID and token, then render-config.sh
runs envsubst over a package-owned template, substituting four variables
(provider ID, auth token, base URL, region). The rendered file
(.opencode.local.json) is the only configuration layer OpenCode sees.
Problems
P1 -- Asymmetric template mechanism. Each agent package (pkg.agent, pkg.metalab) maintains its own near-identical template (~99% overlap). The mount strategy differs between the two containers. There is no shared base and no standard override mechanism.
P2 -- No layered configuration. The rendered config is the only layer. There is no mechanism for project-level overrides (MCP servers, model selection, custom agents) or role-specific overrides without editing a generated file that is overwritten on restart.
P3 -- Provider required at startup. The entrypoint refuses to start
without a provider ID and (usually) a token. This blocks provider-free
startup (useful for local models, deferred auth, or interactive /connect).
P4 -- V1 configuration fields. The templates use enabled_providers
and other V1-era fields. OpenCode v2 uses experimental.policies for
provider restrictions and has different configuration shapes.
P5 -- No standard for package-contributed config. Skills and prompts are aggregated generically by the entrypoint, but there is no equivalent for configuration fragments. A package that needs custom MCP servers or agents has no standard contribution path.
OpenCode v2 native layering
OpenCode v2.0.27 (the pinned version) provides a native multi-layer configuration system that makes custom merging unnecessary.
Directory-walk. OpenCode searches from the current working directory to
the filesystem root. At each level it looks for opencode.json(c) (direct)
and .opencode/opencode.json(c). Files are deep-merged from the farthest
ancestor to the closest. At each level, .opencode/ configs override all
direct configs. Global user config (~/.config/opencode/opencode.json(c))
has the lowest precedence.
Merge behavior. Scalars from higher-precedence layers win.
Non-overlapping keys from lower layers are preserved. Objects deep-merge
(keys from both layers coexist; matching keys use the higher layer's
value). Arrays are replaced in full (standard JSON merge -- the higher
layer's array replaces the lower layer's array entirely, no concatenation).
This means a project layer that defines watcher.ignore must restate the
complete list, not just its additions. Same constraint applies to
permissions, skills, plugins, and any other array-typed field.
Backward compat. OPENCODE_CONFIG and OPENCODE_CONFIG_DIR still
function but are not documented for v2. The directory-walk is the
recommended approach.
Constraints
- The agent Docker image is shared between dev-agent and metalab. Entrypoint changes require a staged rebuild (safe-contribution-workflow ADR).
- Deterministic regeneration on restart is a feature: the entrypoint rendering the base config from env vars ensures a clean, known state.
- Authentication is handled separately (see
agent-authentication-architecture.md). This ADR covers only the non-credential configuration layers.
Options considered
Option A -- Keep single-layer envsubst, fix incrementally
Keep the current rendering mechanism. Fix problems one by one (remove V1 fields, de-duplicate templates, make provider optional).
- Advantage: minimal change surface.
- Disadvantage: does not address P2 (no layering) or P5 (no package config contribution). Every new requirement (MCP, model selection, project override) requires another ad-hoc modification to the entrypoint or templates.
Option B -- Native layered configuration with symmetric packages
Use OpenCode v2's directory-walk for multi-layer config. Each agent package provides a base template rendered by the entrypoint (non-sensitive values only). Higher-precedence layers are plain JSON files managed by the operator or contributed by packages.
- Advantage: addresses all five problems. Uses OpenCode's own merge, eliminating custom logic. Templates are symmetric. Projects and packages contribute config at the appropriate layer.
- Disadvantage: requires entrypoint changes (image rebuild). Medium effort.
Option C -- Tool-agnostic configuration contract with adapter
Define an autograph-native config schema. The entrypoint resolves this contract and generates OpenCode-specific config through an adapter.
- Advantage: decouples autograph from OpenCode config format.
- Disadvantage: over-engineering if OpenCode remains the sole runtime. High effort for no immediate value.
Decision
Option B -- Native layered configuration with symmetric packages.
Option A is insufficient (leaves structural problems). Option C is YAGNI. Option B addresses all problems with proportionate effort and preserves "seams" toward C if a second runtime is ever needed.
Decision 1: Layering model
Four layers, from lowest to highest precedence:
| Layer | Source | Managed by | Contains |
|---|---|---|---|
| Base (package) | opencode.json.template rendered by entrypoint |
Entrypoint (envsubst + jq cleanup) | Non-sensitive env-dependent values (provider ID, region, base URL), permissions, watcher, skills, plugins, agents, instructions |
| Project | opencode.json(c) in workspace |
Operator (committed) | MCP servers, custom agents, model selection, project-specific overrides |
| Override | .opencode/opencode.json(c) in workspace |
Operator (committed or runtime) | Role-specific or instance-specific overrides |
| Auth | Native env vars or /connect |
Separate (see auth ADR) | Provider credentials -- never in any config file |
OpenCode v2's directory-walk discovers the project and override layers
automatically. The base layer is placed via OPENCODE_CONFIG (backward
compat with v2).
The companion diagrams (agent-configuration-layering.drawio) are working
drafts: the "What Goes Where" table maps config concerns to layers and
sources; the "Agent Config Lifecycle" sketches the assembly pipeline from
build to runtime. Both are under study and will be finalized alongside
the implementation tickets (T02/T04).
Decision 2: Base template rendering (entrypoint)
The entrypoint continues to use envsubst on the package template. The
substituted variables include the auth token (universal apiKey mechanism):
AUGR_ADE_OPENCODE_PROVIDER_ID-- active provider (optional)AUGR_ADE_OPENCODE_AUTH_TOKEN-- provider credential (optional)AUGR_ADE_OPENCODE_REGION-- provider region (optional)AUGR_ADE_OPENCODE_BASE_URL-- custom endpoint (optional)
All four variables are optional. When provider ID is not set, the jq
cleanup removes the empty provider block entirely (provider-free startup).
When set, the rendered config contains a provider block with apiKey.
Decision 3: Symmetric agent package contract
Each agent package provides configuration through a standard interface:
| Artifact | Path in package | Purpose |
|---|---|---|
| Config template | agent/opencode.json.template |
Package defaults (permissions, watcher, agents, instructions) |
| Skills | agent/skills/ |
Package-contributed skills (aggregated by entrypoint) |
| Prompts | agent/prompts/ |
Agent system prompts |
| Plugins | agent/plugins/ |
Package-contributed plugins (aggregated by entrypoint) |
| Instructions | agent/AGENTS.md |
Agent operating manual |
This is a first version of the agent package interface contract.
Differences between dev-agent and metalab are expressed as different
AGENTS.md content, different agent definitions, and different compose
volume mounts -- not as different configuration mechanisms.
Decision 4: V2 configuration alignment
enabled_providersremoved (V1 field).- Provider restriction via
experimental.policiesif needed. - Provider settings use v2 shape (
settings,envfield). permissionsuses v2 array format.
Decision 5: Container filesystem and compiled config
The container filesystem separates three concerns:
| Mount | Purpose | Access |
|---|---|---|
/augr/workspace/ |
Agent working space (L1 or L2) | rw or ro |
/augr/hostspace/ |
Observation context (L2 or L1) | ro |
/augr/agent/ |
Compiled agent config | ro (agent), written by entrypoint |
/augr/agent/ is the compiled output of the build/entrypoint pipeline.
It is not authored by the agent -- it is generated from package sources
(and, in the future, project sources). On the host, it lives in
env/<ENV>/services/<agent>/agent/. The entrypoint writes the rendered
config, aggregated skills, and plugins there. OpenCode reads from it
(OPENCODE_CONFIG points here).
Note: the /augr/ prefix is the target layout. Current implementation
may still use /workspace/ and /hostspace/ until the rename is done.
How files flow from package source to compiled config:
Package sources (host, versioned):
engine/deps/modules/autograph.pkg.agent/
agent/
opencode.json.template # config template (envsubst placeholders)
skills/*/SKILL.md # skills (aggregated)
prompts/work.md # system prompt
plugins/phoenix-sync/ # plugins (aggregated)
AGENTS.md # agent operating manual
engine/deps/modules/autograph.pkg.metalab/
agent/ # same structure, different content
opencode.json.template
skills/*/SKILL.md
prompts/work.md
AGENTS.md
Compiled config (host, generated):
env/<ENV>/services/<agent>/
agent/ # mounted at /augr/agent (compiled output)
.opencode.local.json # rendered by entrypoint (envsubst + jq)
skills/ # aggregated from packages (+ project, future)
plugins/ # aggregated from packages
opencode.json.template # ro overlay from package source
prompts/ # ro overlay from package source
data/ # mounted at ~/.local/share/opencode
Compose volume mounts (same pattern for both agents):
Both agents follow the same mount pattern: the compiled config dir with read-only package overlays on top. The only difference is WHICH package provides the overlays and WHERE the workspace/hostspace mounts point.
volumes:
# Compiled agent config -- entrypoint writes here, agent reads
- env/<ENV>/services/<agent>/agent:/augr/agent:rw
# Package overlays (ro, on top of compiled dir)
- pkg.<role>/agent/opencode.json.template:/augr/agent/opencode.json.template:ro
- pkg.<role>/agent/prompts:/augr/agent/prompts:ro
- pkg.<role>/agent/AGENTS.md:/augr/workspace/AGENTS.md:ro
# OpenCode data (persistent credentials, sessions)
- env/<ENV>/services/<agent>/data:~/.local/share/opencode:rw
The template is always sourced from the owning package. There is no project-level template override in this epic.
Entrypoint processing (container startup):
1. validate-env.sh
Reads: AUGR_ADE_OPENCODE_PROVIDER_ID (optional), AUTH_TOKEN (optional)
2. aggregate-skills.sh
Reads: /augr/hostspace/engine/deps/modules/*/agent/skills/
Writes: /augr/agent/skills/* (symlinks)
3. aggregate-plugins.sh
Reads: /augr/hostspace/engine/deps/modules/*/agent/plugins/
Writes: /augr/agent/plugins/* (copies)
4. render-config.sh
Reads: /augr/agent/opencode.json.template (ro overlay from package)
+ env vars (PROVIDER_ID, AUTH_TOKEN, REGION, BASE_URL)
Writes: /augr/agent/.opencode.local.json
What OpenCode sees (runtime, after entrypoint):
/augr/agent/.opencode.local.json <- base layer (rendered from package template)
OPENCODE_CONFIG points here
Contains: permissions, watcher, agents, skills, plugins, instructions
Contains: provider block with apiKey IF token was set
/augr/workspace/opencode.json <- project layer (if operator created it)
Discovered by directory-walk
Contains: MCP servers, model selection, custom agents, overrides
~/.local/share/opencode/opencode.db <- auth (separate, not a config layer)
/connect credentials, persistent across restarts
Decision 6: Project-level agent customization (target)
Each project can contribute agent customization through a directory at the
project root that follows the same interface as package agent/ dirs:
{project-root}/
agent/ # project-level agent customization
skills/ # project-specific skills
prompts/ # project-specific prompts
opencode.json # project-level config overrides
AGENTS.md # agent operating manual (project-owned)
This directory is mounted read-only in both agents. It is treated as an
additional source during the build/aggregation step, alongside package
sources. Skills and plugins from the project agent/ dir are aggregated
into the compiled config alongside those from packages.
AGENTS.md is sourced from the project agent/ dir, not from the
package. The agent operating manual describes the project context,
conventions, and working instructions -- this is inherently project-specific.
Packages may provide a default AGENTS.md in their agent/ dir as a
starting point (seed template), but the project's version takes precedence
once it exists.
The project agent/ dir does NOT contain a template -- the template
remains the exclusive responsibility of the owning package. The project
contributes overrides, additions, and customizations, not a redefinition
of the base.
Changes to the project agent/ dir take effect after augr:build or
augr:up, not live. This is consistent with the compiled-config model:
sources are assembled into the compiled dir, which is what the container
reads.
This decision is not implemented in E03. E03 enforces package-only
templates with package-owned AGENTS.md. Implementation is tracked in
epic draft Exx-agent-project-customization.
Consequences
Positive
- Native layering. Project and role overrides are plain JSON files merged automatically by OpenCode. No custom merge logic.
- Symmetric packages. Dev-agent and metalab follow the same config contract. Template drift is eliminated.
- Provider-free startup. Containers start without a mandatory
provider/token pair. Operators can defer auth to
/connector env vars. - Extensibility. The layering model supports config contributions from packages and project-level customization naturally.
Negative / Risks
- Entrypoint change requires image rebuild. One-time cost for this migration, mitigated by staged validation.
- Backward compat with
OPENCODE_CONFIG. If v2 deprecates this env var, the rendering target must move to a directory-walk location. - V1-to-V2 field migration. Template field names and shapes change. Env vars and compose configs remain compatible.
- Array fields are replaced, not merged. OpenCode's deep-merge replaces
arrays in full. A project layer that overrides
permissions,watcher.ignore,skills, orpluginsmust restate the complete list (not just additions). This limits the ergonomics of per-project overrides for array-typed settings. Mitigation: the base template covers sensible defaults; most projects will not need to override array fields. When they do, the full list must be restated -- documented as a known constraint.
Implementation notes
- The
AUGR_ADE_OPENCODE_*variable namespace is preserved for now. A rename toAUGR_OPENCODE_*is a separate mechanical change. - Future:
aggregate-config.shcould aggregate config fragments from packages (MCP servers, providers, agents), similar to skills aggregation.
Open questions
Agent image ownership. The Docker image is defined in pkg.agent but shared by both agents. It may belong in engine (same question as Docker network ownership). To be evaluated separately.
Generic config aggregation. A mechanism for packages to contribute config fragments (MCP, providers, agents) would complete the package contribution model. Deferred to a future iteration.
Related
agent-authentication-architecture.md-- credential management, connection methods, secrets compliance.secrets-management.md-- per-class credential rules.enterprise-network-proxy.md-- network isolation model.