Harness Field Reference
This is a living document. It is the authoritative reference for harness field classifications, merge rules, and the
ForgeConfigstruct. Update this document whenever you add a new field toHarnessorForgeConfig, move a field between classification tiers, or change merge semantics.The architectural decisions behind these rules are recorded in ADR-0045 (forge-portable schema) and ADR-0088 (CEL-guarded overlays). Those ADRs are point-in-time records; this document reflects the current state.
Field classification
Harness fields are classified into two tiers based on whether they can be overridden inside forge.<platform> blocks or overlays: entries.
Fields that can appear at both levels
These fields can appear at the harness top level (as defaults) and inside ForgeConfig (forge blocks or overlay entries):
| Field | Rationale |
|---|---|
pre_script | Scripts often call forge-specific CLIs (gh, glab) |
post_script | Push, PR/MR creation is forge-specific |
skills | Some skills wrap forge-specific APIs |
runner_env | Token names and event URLs differ per forge |
validation_loop | Validation scripts may call forge-specific tools |
policy | Sandbox policies may need forge-specific filesystem or process rules; network access is managed via providers (ADR-0065) but non-network policy sections can still differ per forge |
providers | Providers may need forge-specific entries (e.g., different API endpoints per platform); concatenated (top-level + forge) |
openshell | OpenShell profiles may need forge-specific configuration; profiles concatenated (top-level + forge) |
host_files | Host files may need forge-specific entries (e.g., different credential files per platform); deduplicated by dest path (child wins) |
env | Env config (runner and sandbox sub-maps) may need forge-specific entries (e.g., different token names per forge); sub-maps merged independently, forge/child keys win (ADR-0055) |
Fields that stay at top level only
These fields are platform-neutral and cannot be overridden per-forge or per-overlay:
| Field | Rationale |
|---|---|
agent | Agent definitions are forge-agnostic |
model | Model selection is independent of forge |
image | Container images are platform-neutral |
api_servers | REST proxies abstract forge details |
plugins | Plugin directories are forge-agnostic; each entry is a local path or a pinned URL and keeps its own env/pi options (ADR-0038, ADR-0094). Top level only — not a ForgeConfig field, so it is not settable under forge: or overlays: (a plugins: key there is ignored, not an error) |
agent_input | Agent prompt input is forge-agnostic |
timeout_minutes | Timeouts are operational, not forge-specific |
sandbox_timeout_seconds | Sandbox-level timeout, not forge-specific |
security | Security scanning is forge-agnostic |
allowed_remote_resources | URL allowlist for resource fetching (ADR 0038) |
description | Documentation, no runtime effect |
role | Agent identity is forge-agnostic |
slug | Kept top-level; per-forge slug differences handled via base composition |
base | Composition is a structural concern, not forge-specific |
doc | Documentation path, no runtime effect |
effort | Effort level is operational, not forge-specific |
readonly_repo | Repo access mode is forge-agnostic |
allow_runtime_fetch | Runtime fetch opt-in is forge-agnostic |
max_runtime_fetches | Fetch cap is operational, not forge-specific |
trigger | CEL trigger expression is evaluated against normalized events, not forge-specific (ADR-0061) |
privilege_levels | Mint privilege per run-stage is forge-agnostic (ADR-0073). Top level only — not a ForgeConfig field |
Merge and inheritance rules
When a forge block or overlay is merged into the harness top level, each field type follows specific merge semantics. The same rules apply during base: composition (base → child merging).
Two independent precedence axes govern field resolution (see #6798):
- Specificity (within a layer): Conditional forge/overlay values override same-layer top-level values.
- Derivation (across layers): Child-layer values override inherited base-layer values. Each base layer's forge and overlay blocks are resolved into top-level fields before merging into the child, so inherited conditional values cannot override the child's explicit settings.
| Field type | Merge behavior | Nil vs empty |
|---|---|---|
| Scalar fields | Forge/child value overrides top-level/base value | Absent = inherit from top level / base |
skills | Merged with deduplication by basename (forge/child overrides top-level/base) | Absent (nil) = inherit; skills: [] = empty list merged with base (base entries are returned) |
runner_env | Top-level/base map merged with forge/child map; forge/child keys win | Absent (nil) = inherit; runner_env: {} = no forge-specific keys (top-level env still inherited) |
privilege_levels | Top-level/base map merged with child map; child keys win (not in ForgeConfig, so no forge/overlay override) | Absent (nil) = inherit from base; omitted entirely at every layer defaults every run-stage to write |
validation_loop | Forge/child value replaces top-level/base value entirely | Absent (nil) = inherit from top level / base; explicit empty struct = intended to mean "no validation" (see ADR-0045 open questions) |
providers | Concatenated (top-level/base + forge/child) | Absent (nil) = inherit; providers: [] = no forge-specific additions (top-level providers still apply) |
openshell | profiles concatenated (top-level/base + forge/child) | Absent (nil) = inherit; empty profiles: [] = no forge-specific additions |
host_files | Concatenated (base + child); deduplicated by dest path (child wins) | Absent (nil) = inherit |
plugins | Concatenated (base + child) | Absent (nil) = inherit |
api_servers | Concatenated (base + child) | Absent (nil) = inherit |
env | Sub-maps (runner, sandbox) merged independently; forge/child keys win (ADR-0055) | Absent (nil) = inherit |
security | Child replaces base entirely (if non-nil) | Absent (nil) = inherit |
overlays | Concatenated (base + child); all matching entries merged at resolution with later precedence (ADR-0088) | Absent (nil) = inherit |
ForgeConfig struct
ForgeConfig is the shared field payload used by both legacy forge: platform blocks and current overlays: entries (via OverlayEntry's yaml:",inline" embedding). The type name is a legacy artifact from the original forge feature (ADR-0045); it was retained when ADR-0088 introduced overlays to avoid a rename-heavy migration. Both mechanisms use mergeForgeConfig to apply their fields onto harness top-level values.
// ForgeConfig holds platform-specific harness configuration.
// This is purely declarative YAML config — it selects which
// scripts, skills, host files, and env vars to use per platform. It is
// distinct from the forge.Client interface (internal/forge/),
// which is the runtime abstraction for forge API operations.
type ForgeConfig struct {
PreScript string `yaml:"pre_script,omitempty"`
PostScript string `yaml:"post_script,omitempty"`
Policy string `yaml:"policy,omitempty"`
Skills []SkillEntry `yaml:"skills,omitempty"`
Providers []string `yaml:"providers,omitempty"`
OpenShell *OpenShellConfig `yaml:"openshell,omitempty"`
HostFiles []HostFile `yaml:"host_files,omitempty"`
ValidationLoop *ValidationLoop `yaml:"validation_loop,omitempty"`
RunnerEnv map[string]string `yaml:"runner_env,omitempty"`
Env *EnvConfig `yaml:"env,omitempty"`
}Current resolution pipeline
The current forge resolution pipeline is:
Unmarshal → validateForge → ResolveForge(platform) → ValidateOverlay resolution (ADR-0088)
overlays: is the successor to deprecated forge: blocks. Each overlay entry has a when: CEL expression and the same override fields as ForgeConfig. All entries whose when evaluates to true are merged in order, with later matches taking precedence over earlier matches.
Resolution pipeline
Unmarshal → validateForge → validateOverlays →
ResolveForge(platform) → ResolveOverlays(event, forgePlatform, config) → ValidateWhen event is nil (CLI flows without event context, such as fullsend lock or fullsend run when no event can be recovered), ResolveOverlays substitutes an empty map so overlays conditioned on runtime.forge or config can still evaluate and match. Overlays that reference event fields should use has() to guard field access (e.g., has(event.source) && event.source.system == "jira").
CEL environment
Overlay when expressions are evaluated with:
| Variable | Type | Source |
|---|---|---|
event | normevent.Event | The triggering event — fields like source.system, entity.kind, transition.kind |
runtime.forge | string | Effective forge platform (precedence: CLI flag > config.forge > CI env vars) |
config | map[string]any | Full per-repo config from config.yaml |
Mutual exclusion
forge: and overlays: must not coexist in the same harness (post-merge). forge: is deprecated; new harnesses should use overlays: instead.
Related
- ADR-0045: Forge-portable harness schema — original architectural decision (Superseded by ADR-0088)
- ADR-0088: CEL-guarded overlays — current overlay mechanism
- Harness Composition: Merge function checklist (step 6 references this document)
- Issue #5579: Harness field integration pipeline (complementary checklist)
