dev
← All ADRs

Agent Configuration Layering

DRAFTEngine2026-10-13

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_providers removed (V1 field).
  • Provider restriction via experimental.policies if needed.
  • Provider settings use v2 shape (settings, env field).
  • permissions uses 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 /connect or 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, or plugins must 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 to AUGR_OPENCODE_* is a separate mechanical change.
  • Future: aggregate-config.sh could 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.

  • agent-authentication-architecture.md -- credential management, connection methods, secrets compliance.
  • secrets-management.md -- per-class credential rules.
  • enterprise-network-proxy.md -- network isolation model.