Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6a3da63
feat(harness): implement CEL-guarded overlays (ADR 0088)
ralphbean Aug 17, 2026
8155464
fix(harness): add URL-base resolution for overlay entries and docs fixes
fullsend-ai-coder[bot] Aug 17, 2026
258df3a
refactor(harness): adopt first-match-wins overlays and expanded CEL env
ralphbean Aug 17, 2026
ee955e5
fix: address review feedback on PR #6285
fullsend-ai-coder[bot] Aug 18, 2026
7cc342f
fix(harness): wire event and config into production overlay callers
fullsend-ai-coder[bot] Aug 18, 2026
cb8efcf
fix(harness): wire Config in all callers and enable overlays without …
ralphbean Aug 19, 2026
d8472e9
fix(harness): treat CEL eval errors as non-matching in ResolveOverlays
fullsend-ai-coder[bot] Aug 19, 2026
a082658
refactor(harness): extract shared BuildConfigMap to eliminate code du…
fullsend-ai-coder[bot] Aug 20, 2026
031e6d0
fix(harness): thread config.forge into runtime.forge precedence chain
ralphbean Aug 21, 2026
af838e5
fix(lock): add overlays[N].* cases to resolveFromLock
ralphbean Aug 21, 2026
7c579b3
fix(harness): make forge deprecation warning reachable in CI
ralphbean Aug 21, 2026
b4f001a
fix(harness): remove config CEL variable whitelist
ralphbean Aug 21, 2026
836724f
docs(overlays): document has() guard and empty-event semantics
ralphbean Aug 21, 2026
379c3da
refactor(harness): clarify naming and error message conventions in ov…
ralphbean Aug 21, 2026
126ee0c
fix(harness): address review feedback — validate config.forge, update…
fullsend-ai-coder[bot] Aug 21, 2026
0c20de3
fix(cli): correct runAgent call signatures in tests
ralphbean Aug 24, 2026
63ae43a
fix(cli): add missing runOverrideFlags arg to runAgent call
ralphbean Aug 24, 2026
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
6 changes: 6 additions & 0 deletions docs/ADRs/0045-forge-portable-harness-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ Superseded by [ADR-0088](0088-cel-guarded-overlays.md) (CEL-guarded overlays)
> acceptance. For the current authoritative version, see
> [Harness Field Reference](../contributing/harness-fields.md).

> **Note:** The `forge:` section introduced by this ADR is deprecated in favor
> of CEL-guarded `overlays:` — see [ADR 0088](0088-cel-guarded-overlays.md).
Comment thread
ralphbean marked this conversation as resolved.
> The rest of this ADR (role, slug, base composition, merge rules) remains
> current.
Comment thread
ralphbean marked this conversation as resolved.

## Context

ADR 0024 established the harness YAML as the self-contained execution unit
Expand Down Expand Up @@ -766,3 +771,4 @@ forge-specific artifact. The harness and agent definition are portable.
- [Issue #322](https://github.com/fullsend-ai/fullsend/issues/322): Platform-specific component identification
- [Issue #1986](https://github.com/fullsend-ai/fullsend/issues/1986): Default agents should use the same delivery mechanism as custom agents
- [ADR 0058](0058-agent-registration.md): Agent registration — re-adds `agents` config key with URL/path semantics (supersedes the role/name/slug schema removed in Phase 4)
- [ADR 0088](0088-cel-guarded-overlays.md): CEL-guarded overlays — deprecates the `forge:` section in favor of `overlays:` with CEL `when` expressions
6 changes: 4 additions & 2 deletions docs/ADRs/0055-unified-env-var-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ env:
environment, same as `runner_env` and `expand: true` host_files today.

The `env:` field can appear at the top level and inside `forge.<platform>`
blocks, replacing `runner_env` at both levels
([ADR 0045](0045-forge-portable-harness-schema.md)).
blocks (or `overlays:` entries), replacing `runner_env` at both levels
([ADR 0045](0045-forge-portable-harness-schema.md)). `forge:` is deprecated
in favor of CEL-guarded `overlays:` — see
[ADR 0088](0088-cel-guarded-overlays.md).

Go struct:

Expand Down
7 changes: 5 additions & 2 deletions docs/ADRs/0088-cel-guarded-overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ warning when `forge:` is present, recommending migration to

### Resolution pipeline

> **Update (2026-08-21):** The empty-event semantics described below have
> evolved since this ADR was written. See
> [Harness Field Reference](../contributing/harness-fields.md) for the
> current behavior (nil event → empty map substitution).

`LoadWithOpts` and `LoadWithBase` gain `Event normevent.Event` and
`Config map[string]any` fields in their options structs. The pipeline
becomes:
Expand All @@ -139,8 +144,6 @@ ResolveForge(platform) → ResolveOverlays(event, config) → Validate
against the CEL environment (see below). The first entry whose
`when` returns true is merged; remaining entries are skipped. Like
`ResolveForge`, it nils out the field after resolution (consumed).
When `Event` is nil, `ResolveOverlays` is a no-op (no entries
match), paralleling `ResolveForge` when `ForgePlatform` is empty.

### CEL environment

Expand Down
22 changes: 21 additions & 1 deletion docs/contributing/harness-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,30 @@ and update the others as needed.
| Function | File | Purpose |
|----------|------|---------|
| `mergeBaseIntoChild` | `internal/harness/compose.go` | Merges base harness fields into child during `base:` composition |
| `mergeForgeConfig` | `internal/harness/forge.go` | Applies `forge.<platform>` overrides onto top-level harness fields |
| `mergeForgeConfig` | `internal/harness/forge.go` | Applies `forge.<platform>` or overlay overrides onto top-level harness fields |
| `mergeForgeConfigInto` | `internal/harness/compose.go` | Merges base `ForgeConfig` fields into child `ForgeConfig` during `base:` composition |
| `mergeSkills` | `internal/harness/compose.go` | Deduplicates skills by basename (base + child); merges file-level override maps when both define the same basename (child keys win) |
| `mergeHostFiles` | `internal/harness/compose.go` | Deduplicates host files by dest path (base + child) |
| `mergeForgeBlocks` | `internal/harness/compose.go` | Merges `forge:` maps key-by-key across base and child |

> **Note — overlay precedence exception.** When `overlays` are concatenated
> during base composition, base entries are placed first. Because overlay
> resolution uses first-match-wins semantics (`ResolveOverlays` stops at the
> first matching `when` expression), base overlay entries take precedence over
> child overlay entries with the same condition. This is an intentional
> exception to the child-overrides-base convention used by scalar and map
> merges, and matches the concatenation behavior for `plugins`, `providers`,
> and `api_servers` lists.

### Validation and resolution side

| Function | File | Purpose |
|----------|------|---------|
| `validateForge` | `internal/harness/forge.go` | Validates `forge:` block keys and `ForgeConfig` field values |
| `validateOverlays` | `internal/harness/forge.go` | Validates `overlays:` entries — CEL `when` expressions and `ForgeConfig` field values; enforces mutual exclusion with `forge:` |
| `ResolveForge` | `internal/harness/forge.go` | Merges the selected forge platform's config into the harness and nils the forge map |
| `ResolveOverlays` | `internal/harness/forge.go` | Evaluates overlay `when` expressions against event/runtime/config CEL environment; merges the first matching entry (first-match-wins) and nils the overlays list. When event is nil (CLI flows without event context), an empty map is substituted so overlays conditioned on `runtime.forge` or `config` can still match. Use `has(event.source)` to guard event field access in `when` expressions. |

### How they correspond

The merge functions define which fields participate in harness
Expand Down Expand Up @@ -99,5 +117,7 @@ matching `_test.go` file.
current overlay mechanism
- [ADR-0064](../ADRs/0064-deprecate-customized-directory-overlay.md):
Deprecate customized directory overlay
- [ADR-0088](../ADRs/0088-cel-guarded-overlays.md): CEL-guarded overlays —
generalizes forge-specific config with CEL expressions
- Issue #5579: Harness field integration pipeline (complementary
checklist covering the broader field addition workflow)
33 changes: 17 additions & 16 deletions docs/contributing/harness-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ field type follows specific merge semantics. The same rules apply during
| `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` *(planned)* | Concatenated (base + child); first-match-wins at resolution (ADR-0088, not yet implemented) | Absent (nil) = inherit |
| `overlays` | Concatenated (base + child); first-match-wins at resolution (ADR-0088) | Absent (nil) = inherit |

## `ForgeConfig` struct

Expand Down Expand Up @@ -115,28 +115,29 @@ The current forge resolution pipeline is:
Unmarshal → validateForge → ResolveForge(platform) → Validate
```

## Overlay resolution (planned — ADR-0088)
## Overlay resolution (ADR-0088)

> **Note:** This section describes planned behavior from
> [ADR-0088](../ADRs/0088-cel-guarded-overlays.md). The overlay feature
> has not been implemented yet. The current implementation uses `forge:`
> blocks only.
`overlays:` is the successor to deprecated `forge:` blocks. Each overlay
entry has a `when:` CEL expression and the same override fields as
`ForgeConfig`. The first entry whose `when` evaluates to true is merged;
remaining entries are skipped (first-match-wins).

`overlays:` is the planned successor to `forge:` blocks. Each overlay
entry will have a `when:` CEL expression and the same override fields as
`ForgeConfig`. The first entry whose `when` evaluates to true will be
merged; remaining entries will be skipped.

### Planned resolution pipeline
### Resolution pipeline

```
Unmarshal → validateForge → validateOverlays →
ResolveForge(platform) → ResolveOverlays(event, config) → Validate
ResolveForge(platform) → ResolveOverlays(event, forgePlatform, config) → Validate
```

### Planned CEL environment
When `event` is nil (CLI flows without event context like `fullsend run`
or `fullsend lock`), `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 will be evaluated with:
Overlay `when` expressions are evaluated with:

| Variable | Type | Source |
|---|---|---|
Expand All @@ -147,7 +148,7 @@ Overlay `when` expressions will be evaluated with:
### Mutual exclusion

`forge:` and `overlays:` must not coexist in the same harness (post-merge).
`forge:` is deprecated; new harnesses should use `overlays:` once implemented.
`forge:` is deprecated; new harnesses should use `overlays:` instead.

## Related

Expand Down
91 changes: 58 additions & 33 deletions docs/guides/user/bring-your-own-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,25 +170,25 @@ validation_loop:

timeout_minutes: 10

forge:
github:
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
env:
runner:
GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL}
GH_TOKEN: ${GH_TOKEN}
sandbox:
GITHUB_ISSUE_URL: "${GITHUB_ISSUE_URL}"
GH_TOKEN: "${GH_TOKEN}"
overlays:
- when: 'runtime.forge == "github"'
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
env:
runner:
GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL}
GH_TOKEN: ${GH_TOKEN}
sandbox:
GITHUB_ISSUE_URL: "${GITHUB_ISSUE_URL}"
GH_TOKEN: "${GH_TOKEN}"
```

Key patterns to note:

- **`policy: policies/triage.yaml`** is a per-agent policy that includes filesystem, landlock, process, and network rules (via inline `network_policies`). This agent predates the provider-based pattern — new agents can use `providers:` instead (see [Minimum viable agent](#minimum-viable-agent)).
- **`host_files`** copy credentials from the trusted runner into the sandbox. `expand: true` resolves `${VAR}` references before copying.
- **`validation_loop.schema`** references the JSON schema file directly — the validation script checks agent output against it.
- **`forge.github`** scopes scripts, skills, providers, openshell, host_files, and env vars to GitHub. When running on GitLab, a `forge.gitlab` block would take effect instead.
- **`overlays`** uses CEL `when` expressions to conditionally apply scripts, skills, providers, openshell, host_files, and env vars. Resolution is first-match-wins: the first entry whose `when` evaluates to true is merged; remaining entries are skipped. The CEL environment exposes `event` (the triggering event), `runtime.forge` (the effective forge platform), and `config` (per-repo config from config.yaml). When running without an event context (e.g., `fullsend run` or `fullsend lock`), `event` is an empty map — use `has(event.source)` to guard event field access: `has(event.source) && event.source.system == "jira"` instead of just `event.source.system == "jira"` to avoid "no such key" errors.
- **`common/env/gcp-vertex.env`** is referenced by relative path because both files live in the same repo. If your agent lives in a different repo, reference it by URL (see [Remote references](#referencing-resources-local-vs-remote)) or copy it locally.

## Harness field reference
Expand Down Expand Up @@ -275,23 +275,30 @@ api_servers: # Host-side REST proxies exposed to sandbox
env: # Env vars for the server process
API_KEY: "${API_KEY}"

# ── Forge-specific overrides ──────────────────────────────────
forge:
github:
pre_script: scripts/pre-gh.sh
post_script: scripts/post-gh.sh
skills: [skills/github-specific] # Concatenated with top-level
providers: [providers/github.yaml] # Concatenated with top-level
openshell:
profiles: [profiles/github.yaml] # Concatenated with top-level
host_files: # Forge-specific host files
- src: env/github.env
dest: /run/secrets/forge.env
env:
runner:
GH_TOKEN: "${GH_TOKEN}"
gitlab:
pre_script: scripts/pre-gl.sh
# ── Conditional overrides (CEL-guarded, first-match-wins) ────
overlays:
- when: 'event.source.system == "jira" && runtime.forge == "github"'
pre_script: scripts/pre-jira-on-gh.sh
skills: [skills/jira-read] # Merged with top-level
env:
runner:
GH_TOKEN: "${GH_TOKEN}"
JIRA_TOKEN: "${JIRA_TOKEN}"
- when: 'runtime.forge == "github"'
pre_script: scripts/pre-gh.sh
post_script: scripts/post-gh.sh
skills: [skills/github-specific] # Merged with top-level
providers: [providers/github.yaml] # Concatenated with top-level
openshell:
profiles: [profiles/github.yaml] # Concatenated with top-level
host_files: # Overlay-specific host files
- src: env/github.env
dest: /run/secrets/forge.env
env:
runner:
GH_TOKEN: "${GH_TOKEN}"
- when: 'event.source.system == "jira"'
pre_script: scripts/pre-jira.sh

# ── Security ──────────────────────────────────────────────────
security:
Expand All @@ -308,18 +315,34 @@ security:

### Deprecated fields

> **Deprecated:** `forge` is deprecated. Use `overlays` with CEL `when`
> expressions instead (see [ADR 0088](../../ADRs/0088-cel-guarded-overlays.md)).
> The `forge` field still works but emits a deprecation warning at lint time.
> Migration: each forge key becomes an overlay entry — e.g. `forge: github:`
> becomes `overlays: - when: 'runtime.forge == "github"'`. Note the conditioning
> axis: `runtime.forge` reflects the effective forge platform (from `--forge`
> flag, `config.forge`, or CI env vars), while `event.source.system` identifies
Comment thread
ralphbean marked this conversation as resolved.
> the event origin. These diverge for cross-system events (e.g. a JIRA issue
> triggering work on GitHub). `forge` and `overlays` cannot coexist in the
> same harness.

> **Deprecated:** `runner_env` is deprecated. Use `env.runner`
> instead. The `runner_env` field still works but emits a deprecation warning
> at runtime. Migration: move `runner_env:` entries under `env: runner:` and
> delete the `runner_env:` block.

### Field merge rules (for `base` and `forge`)
### Field merge rules (for `base` and `overlays`)

Overlays use first-match-wins: exactly one overlay (or none) applies to any
given event. When an agent needs config from multiple concerns (e.g.
JIRA-specific scripts *and* GitHub-specific runner env), create a combined
entry. More-specific entries go first; broader fallbacks go last.

| Field type | Behavior |
|-----------|----------|
| Scalars (`model`, `pre_script`, `policy`, `image`, etc.) | Child wins if non-empty |
| `skills` | Merged with deduplication by basename (child overrides base) |
| `providers`, `openshell.profiles` | Concatenated (base + child); also applies per-forge |
| `providers`, `openshell.profiles` | Concatenated (base + child); also applies per matched overlay |
| `plugins`, `api_servers` | Concatenated (base + child) |
| `host_files` | Concatenated; child overrides by `dest` |
| `env`, `runner_env` (deprecated) | Merged; child keys win |
Expand Down Expand Up @@ -394,7 +417,9 @@ skills:
timeout_minutes: 15
```

Base chains support up to 5 levels (`MaxBaseDepth` in `internal/harness/compose.go`). Circular references are detected and rejected. Resolution order: base chain → child overrides → forge selection. See [field merge rules](#field-merge-rules-for-base-and-forge) for how each field type combines.
Base chains support up to 5 levels (`MaxBaseDepth` in `internal/harness/compose.go`). Circular references are detected and rejected. Resolution order: base chain → child overrides → overlay resolution. See [field merge rules](#field-merge-rules-for-base-and-overlays) for how each field type combines.

> **Overlay precedence with `base:`:** Overlays are concatenated base-first, child-appended — the same ordering as `plugins`, `providers`, and `api_servers`. Because `ResolveOverlays` uses first-match-wins, a base overlay whose `when` matches will take precedence over a child overlay with the same condition. This is consistent with the trusted-base model (base URLs require an org-level allowlist).

> **Note:** `allowed_remote_resources`, `allow_runtime_fetch`, and `max_runtime_fetches` are NOT inherited from base harnesses — the child must declare its own. This prevents a base harness from injecting arbitrary URL prefixes or enabling runtime fetching in the child.

Expand Down Expand Up @@ -462,7 +487,7 @@ env:

### What you can configure

Any harness field can be overridden. The [field merge rules](#field-merge-rules-for-base-and-forge) determine how your overrides combine with the base:
Any harness field can be overridden. The [field merge rules](#field-merge-rules-for-base-and-overlays) determine how your overrides combine with the base:

- **Change model, timeout, image, scripts** — scalars replace the base value.
- **Add skills** — your entries are merged with the base's by basename; same-named skills override the base entry. **Add plugins or host_files** — your entries are concatenated with the base's.
Expand Down
Loading
Loading