---
description: The components of the fullsend agent execution stack and how they fit together — the current architectural truth, kept in sync with accepted ADRs.
---

# Architecture

What are the components of the agent execution stack?

> **This is a living document.** It must always reflect the current state of
> architectural decisions. When an ADR is accepted (or superseded), this
> document is updated to match. ADRs are point-in-time records that may receive minor annotations
> but are not substantially rewritten; this document is where the *current* truth lives.
> A reader should be able to understand the system's architecture from this
> document alone, without tracing a chain of ADRs.

This document names the parts of the system without deciding how they work. It establishes shared vocabulary that the [problem documents](problems/) can reference when discussing design choices. Each component gets a responsibility statement and open questions — implementation decisions live in the problem docs and will crystallize into [ADRs](ADRs/) as they mature.

This is not exhaustive. Not every problem doc maps to a component here, and not every component here has a corresponding problem doc yet.

## Execution Stack

Five components form the vertical execution path from event to agent action:

1. **Agent Dispatch and Coordination Layer** — translates events into agent tasks
2. **Agent Infrastructure** — provisions and runs agent workloads
3. **Agent Sandbox** — enforces isolation (network, filesystem)
4. **Agent Harness** — assembles configuration and context (skills, prompts, tools)
5. **Agent Runtime** — the LLM in execution

Control flows strictly downward through this stack. No layer may influence, configure, or depend on layers above it. This is the execution stack's primary structural invariant. (See [ADR 0016](ADRs/0016-unidirectional-control-flow.md).)

The remaining components described in this document (Policy Store, Intent Source, Identity Provider, Observability, Agent Registry) are cross-cutting concerns that feed into the stack from the side. They are not part of the vertical control flow, but they follow the same principle: no component within the stack can modify the cross-cutting systems that constrain it.

## Agent Infrastructure

The compute and orchestration layer that runs agent workloads. Responsible for provisioning, scheduling, scaling, and lifecycle management of agent execution environments.

This is the "where do agents physically run" question — whether that's a managed platform, internal Kubernetes, CI runners repurposed for agent work, or something purpose-built.

Forge-native infrastructure platform choice and configuration live in each
target repository's **`.fullsend/`** directory. Per-repo installation is the
sole supported installation model
([ADR 0033](ADRs/0033-per-repo-installation-mode.md)); the dedicated org-level
`<org>/.fullsend` config repo is deprecated
([ADR 0044](ADRs/0044-deprecate-per-org-installation-mode.md)).

**Decided:**

- Tenant configuration for a possible centrally managed service reuses the
  existing `repos.yaml` v1 format directly through `repos.Manifest`. Whether
  to offer the service, its source of truth, and its delivery mechanism remain
  open ([ADR 0123](ADRs/0123-tenant-configuration-from-repos-yaml.md)).
- Forge abstraction: all forge operations go through the `forge.Client` interface, keeping the rest of the codebase forge-agnostic ([ADR 0005](ADRs/0005-forge-abstraction-layer.md)).
- Conversation surface: agents participate in GitHub Discussions and later other chat systems through a narrow `conversation.Client` (parallel to `tracker.Client` for issue content), not by extending `forge.Client` ([ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md)). A **conversation** is the container (Discussion / Slack channel) with exactly one category and optional M:M labels; a **thread** is the top-level message plus replies that share its `parent_id` (`parent_id == id` on the root message).
- Event-source routing for status notifications: the notification destination for run-status comments and reactions is dynamically determined by event provenance — a Jira-triggered run posts status to Jira, a GitHub-triggered run posts to GitHub — rather than being hardwired to the code-output forge. Status notifications route through `tracker.Client`; reactions are an optional `tracker.Reactor` capability (Jira Cloud supports comment reactions but not issue reactions, so `Reactor` is not implemented for Jira currently) ([ADR 0093](ADRs/0093-tracker-routed-status-notifications.md)).
- Installation model: ordered layer stack (install forward, uninstall reverse, analyze for status reporting) with idempotent operations. Current stack: config-repo → workflows → vendor-binary → secrets → inference → dispatch → enrollment ([ADR 0006](ADRs/0006-ordered-layer-model.md)).
- Cross-repo dispatch: enrolled repos call `.fullsend` via `workflow_call`; a dispatch workflow mints OIDC tokens exchanged at a central token mint (GCP Cloud Function or Cloudflare Worker) for scoped GitHub App installation tokens per agent role. App PEM secrets are stored in Secret Manager (GCF mint), Worker secrets (CF mint), or the local filesystem (standalone mint), not the config repo ([ADR 0008](ADRs/0008-workflow-dispatch-for-cross-repo-dispatch.md)).
- Shim workflow security: `pull_request_target` prevents PR authors from modifying the shim workflow. No long-lived secrets flow through the shim — OIDC tokens are issued by the GitHub runtime and scoped to the workflow run ([ADR 0009](ADRs/0009-pull-request-target-in-shim-workflows.md)).
- Repo maintenance: a workflow in `.fullsend` (`.github/workflows/repo-maintenance.yml`) reconciles enrollment shims in target repos when `config.yaml` changes or on manual dispatch. The CLI's `EnrollmentLayer.Install()` dispatches this workflow via `workflow_dispatch` and monitors it for completion, then reports any enrollment PRs created in target repos.
- Installer scaffold: the `WorkflowsLayer` deploys content from an embedded scaffold (`internal/scaffold/`), keeping deployable files as real files under version control rather than Go string constants.
- Reusable workflows: agent workflows in `.fullsend` are thin callers (~40-70 lines) that delegate infrastructure logic to upstream reusable workflows (`fullsend-ai/fullsend/.github/workflows/reusable-*.yml`) via `workflow_call`. Infrastructure patches ship once upstream and propagate to all orgs without re-install ([ADR 0031](ADRs/0031-reusable-workflows-for-action-installed-distribution.md)). **`--vendor`** ([ADR 0047](ADRs/0047-vendored-installs-with-vendor-flag.md)) commits workflows and agent content at install time; layered installs (default) fetch upstream at runtime.
- Event-driven stage dispatch: eliminate `workflow_dispatch` + `gh workflow run` fan-out from `dispatch.yml` in favor of synchronous `workflow_call` so the dispatched run stays linked to the caller ([ADR 0041](ADRs/0041-synchronous-workflow-call-event-dispatch.md)).
- Multi-repo management: a `fullsend repos` subcommand group with a declarative `repos.yaml` manifest for managing per-repo installations at scale — install, convergence (provision, sync, upgrade), status, and uninstall across repos and orgs ([ADR 0057](ADRs/0057-repos-management.md), [ADR 0074](ADRs/0074-repos-command-consolidation.md)).
- Declarative managed repository configuration: `defaults.config` opts every repository into a canonical managed `.fullsend/config.yaml`, while a per-repository `config` block opts in only that repository; repositories with neither declaration remain unmanaged. Every managed write compares the candidate and current effective configuration through the runtime accessor chain and rejects an implicit relaxation ([ADR 0122](ADRs/0122-declarative-repo-configuration.md)).
- Dispatch version-skew resolution: per-repo `reusable-dispatch.yml` inlines stage workflow jobs directly, eliminating `@v0` references to `reusable-{stage}.yml` ([ADR 0062](ADRs/0062-dispatch-version-skew.md)).
- Ready-made configuration presets: `fullsend github setup --config <path-or-url>` commits the preset unchanged as `.fullsend/config.base.yaml` and writes any explicitly-passed persistent setup flags (`--runtime`, `--agents`, `--mint-url`, `--inference-*`) into `.fullsend/config.yaml`, overriding matching preset values; a stub overlay is written only when no persistent flags are passed. `fullsend repos install` converges the same preset through `defaults.config_base` / per-repo `config_base` in `repos.yaml` (optional `sha256`, `none` to disable inheritance), replacing `.fullsend/config.base.yaml` wholesale without clobbering `.fullsend/config.yaml`; when a repository is opted into ADR 0122, install separately converges that file from the manifest. Shared-infrastructure presets will reduce per-adopter enrollment (target state): mint via `job_workflow_ref` trust per [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); inference authorization model undecided ([ADR 0069](ADRs/0069-ready-made-configuration-presets.md)); enrollment remains required until follow-on ADRs land.
- GitLab event dispatch: hybrid native webhook fast-path (GitLab "use a webhook" pipeline trigger pinned to the protected default branch) plus the cron-poller as reconciliation backstop ([ADR 0125](ADRs/0125-gitlab-hybrid-webhook-poller-dispatch.md)); the fast-path is ship-gated and not yet enabled, so the cron-poller remains the implemented dispatch path. Native `merge_request_event` dispatch stays removed ([#7322](https://github.com/fullsend-ai/fullsend/issues/7322)); protected CI/CD variables are unavailable on unprotected MR refs. No external webhook-bridge infrastructure. The bot PAT is Developer-level — the current poller credential per [ADR 0067](ADRs/0067-gitlab-cron-polling-event-dispatch.md) — and stored as a protected CI/CD variable; the fast-path's variable-restriction control may carry a higher-privilege credential cost that is unresolved and tracked in ADR 0125 Status/Caveats and [#7769](https://github.com/fullsend-ai/fullsend/issues/7769). Poller state lives on dedicated HMAC-signed branches. Per-repo only ([ADR 0067](ADRs/0067-gitlab-cron-polling-event-dispatch.md)).

**Open questions:**

- Do we adopt a 3rd party platform, use existing internal infrastructure, or build our own? The tenant manifest format is decided, while the source of truth, service, infrastructure, and poller/dispatch/runner boundary remain open ([ADR 0123](ADRs/0123-tenant-configuration-from-repos-yaml.md); see [agent-infrastructure.md](problems/agent-infrastructure.md)).
- Can different agent types (short-lived review vs. long-running code) run on different infrastructure?
- Who in the org owns and operates this, and how does it relate to existing platform or CI ownership?
- Should model and MCP (or other tool-protocol) traffic from agent runtimes go through a **shared gateway** for authentication, spend limits, allowlists, and telemetry? (See [landscape.md](landscape.md#agent-gateway).)

## Agent Sandbox

The isolation boundary around a running agent. Responsible for filesystem access control and network regulation — ensuring an agent can only reach what it's authorized to reach and cannot affect other agents or systems outside its boundary.

The sandbox is a security primitive. Its job is containment: if an agent is compromised or misbehaves, the blast radius is limited to what the sandbox permits.

Ecosystem projects reuse the word *sandbox* for different workload shapes. For example, [Kubernetes SIG Agent Sandbox](https://github.com/kubernetes-sigs/agent-sandbox) targets **stateful, singleton** agent runtimes (long-lived sessions), whereas many fullsend-style workflows emphasize **short-lived, task-scoped** runs with tight isolation and observability. How those patterns compare is discussed in [agent-infrastructure.md](problems/agent-infrastructure.md#kubernetes-sig-agent-sandbox).

Sandbox defaults (network policy, filesystem restrictions) are configured in
each target repository's **`.fullsend/`** directory
([ADR 0033](ADRs/0033-per-repo-installation-mode.md)).

**Open questions:**

- What is the right isolation level — process, container, microVM, or separate cluster? (See [agent-infrastructure.md](problems/agent-infrastructure.md) and [security-threat-model.md](problems/security-threat-model.md).)
- ~~How granular is network regulation? Allowlist of endpoints, or coarser controls?~~ Decided in [ADR 0065](ADRs/0065-provider-backed-policy-composition.md): network access is granted through provider profiles with per-endpoint allowlists.
- Does the sandbox provide a pre-built environment (tools, language runtimes, repo clones), or does the agent set up its own workspace within the sandbox?
- ~~Is the sandbox the same for all agent roles, or does each role get a differently-scoped sandbox?~~ Decided in [ADR 0020](ADRs/0020-composable-single-responsibility-agents-with-individual-sandboxes.md): each agent gets its own sandbox with policies designed for its responsibility.

**Decided:**

- Provider-backed policy composition: network access is granted through provider profiles declared in harness files. Policy files define only non-composable sandbox restrictions (filesystem, landlock, process). A single `base.yaml` replaces per-agent policy files in the scaffold. Inline `network_policies` continue to work but providers are the recommended approach ([ADR 0065](ADRs/0065-provider-backed-policy-composition.md)).

## Agent Harness

The configuration and context layer that prepares an agent for its task. Responsible for providing skills, system prompts, codebase context, tool definitions, and behavioral instructions to the agent runtime.

The harness is what makes a generic LLM into a specific agent with a specific role. It assembles what the agent needs to know and what it's allowed to do before the agent starts working.

The harness draws its configuration from each target repository's
**`.fullsend/`** directory — skills, workflow definitions, and agent behavioral
instructions are assembled from the layered config (fullsend defaults, then
repo baseline and overrides)
([ADR 0033](ADRs/0033-per-repo-installation-mode.md)).

**Decided:**

- Output schema enforcement: a harness post-script validates every agent's
  output against a declared JSON schema on the host. Non-compliant output
  triggers a retry (capped); exhaustion is a hard failure — no unvalidated
  output is emitted
  ([ADR 0022](ADRs/0022-harness-level-output-schema-enforcement.md)).
  An iteration killed at `timeout_minutes` is not retried: the run ends with a
  distinct timeout error unless an iteration's output already validated, and
  the budget reaches the sandbox as `FULLSEND_TIMEOUT_MINUTES` plus a
  per-iteration `FULLSEND_ITERATION_DEADLINE`
  ([ADR 0105](ADRs/0105-timed-out-iteration-ends-the-run.md)).
- Forge-portable harness schema: `role` and `slug` move into the harness
  YAML (eliminating the config.yaml `agents:` block dependency), and a
  `forge:` section separates platform-specific config from platform-neutral
  fields (see [Harness Field Reference](contributing/harness-fields.md)
  for the current field classifications and merge rules). Forge blocks
  inherit from top-level defaults and override only deltas
  ([ADR 0045](ADRs/0045-forge-portable-harness-schema.md), superseded by
  [ADR 0088](ADRs/0088-cel-guarded-overlays.md)). `validation_loop` fields
  merge independently during `base:` composition and forge/overlay
  resolution: child/forge non-zero values win, omitted fields inherit.
- Unified env var delivery: a single `env:` key with `runner` and `sandbox`
  sub-maps replaces `runner_env` and manual `.env` files. The runner generates
  the sandbox `.env` file from `env.sandbox` at bootstrap. `runner_env` is
  deprecated ([ADR 0055](ADRs/0055-unified-env-var-delivery.md), amending
  [ADR 0024](ADRs/0024-harness-definitions.md)).
- Agent configuration env vars: behavioral knobs use `{AGENT}_{SETTING_NAME}`
  naming (e.g., `REVIEW_SEVERITY_THRESHOLD`), delivered via `env.runner` and
  `env.sandbox` in the harness YAML. Each agent documents its config vars in
  `docs/agents/<agent>.md`
  ([ADR 0049](ADRs/0049-agent-configuration-env-var-convention.md)).
- Config surface boundary: a knob that applies to one agent is an
  `{AGENT}_`-prefixed harness env var (never a `config.yaml` field); a
  knob that applies across agents or governs dispatch/policy is a
  `config.yaml` field (never also an env var)
  ([ADR 0080](ADRs/0080-config-yaml-vs-agent-env-var-scope.md)).
- CI workflow `env:` scope: the workflow `env:` block is reserved for
  infrastructure plumbing (credentials, project IDs, regions) and values
  computable only at CI runtime; agent behavior defaults are set via
  harness `env.runner`/`env.sandbox` and overridden through `base:`
  composition, never the workflow file
  ([ADR 0081](ADRs/0081-reserve-workflow-env-for-infra-plumbing.md)).
- Agent-driven branch targeting: the code agent writes its chosen target
  branch to structured output. The post-script validates the choice against
  an allowlist and falls back to the repo's auto-detected default branch.
  Branch-targeting logic lives in the portable post-script, not in workflow
  YAML ([ADR 0053](ADRs/0053-agent-driven-branch-targeting.md)).
- Harness trigger expressions: each harness may declare an optional CEL
  `trigger` boolean evaluated against a required forge-neutral normalized
  entity and an optional prompting `NormalizedEvent`. `fullsend dispatch`
  resolves each event's entity before matching; scheduled discovery evaluates
  resolved entities with no prompting event. Harnesses without entity sources
  remain event-triggered only and may rely on the event being present
  ([ADR 0061](ADRs/0061-harness-cel-dispatch.md), partially superseded by
  [ADR 0098](ADRs/0098-entity-first-harness-evaluation.md)).
- Portable provider and profile resolution: provider and profile definitions
  can be URL-referenced (sha256-pinned) or specified as local file paths in
  the harness, enabling portable base harnesses that carry their own
  provider/profile dependencies. URL-resolved providers are validated against
  `allowed_remote_resources` and merged with local definitions at resolution
  time ([ADR 0075](ADRs/0075-local-path-profiles-providers.md)).
- Run-stage-scoped privilege levels: a `privilege_levels` field in harness config
  maps run-stages (`pre_script`, `runtime`, `post_script`) to named mint
  privilege levels. A `default` key covers unspecified run-stages. When omitted,
  the harness defaults to `write` for all run-stages, preserving backward
  compatibility ([ADR 0073](ADRs/0073-named-mint-privilege-levels.md)).
- Pre-script skip signalling: the harness `pre_script` runs exactly once,
  inside `fullsend run`; a pre-script stops the run before sandbox creation by
  writing `skipped=true` to the CLI-provided `FULLSEND_PRESCRIPT_OUTPUT` file
  or by exiting with code 78 (neutral skip)
  (contract: [`docs/normative/prescript-output/v1`](normative/prescript-output/v1/README.md)),
  replacing the inline workflow pre-checks and their scaffold script copies
  ([ADR 0072](ADRs/0072-pre-script-output-protocol.md)).
- CEL-guarded overlays: an `overlays:` list of CEL-guarded config
  overlays generalizes the `forge:` block, letting harness authors
  condition scripts, skills, env vars, and other fields on any event
  property (source system, event type, etc.) rather than only the forge
  platform. `forge:` is deprecated but remains functional
  ([ADR 0088](ADRs/0088-cel-guarded-overlays.md)).

**Open questions:**

- Does the harness live inside the sandbox (configuring the agent from within its isolation boundary) or outside it (preparing the environment before the agent starts)? (Security hooks are injected as a runner-owned `hooks.json` loaded via `--settings`; see [ADR 0027](ADRs/0027-allowed-and-disallowed-tools-for-agents.md). General harness placement remains open.)
- How is codebase context assembled? (See [codebase-context.md](problems/codebase-context.md).)
- How do we version and test harness configurations? (See [testing-agents.md](problems/testing-agents.md).) (Functional tests now test the full pipeline including harness-assembled configuration — [ADR 0052](ADRs/0052-functional-tests-for-agent-pipelines.md). Harness versioning remains open.)

## Agent Runtime

The agent itself in execution — the LLM, its tool-use loop, and the interface to the model provider. Responsible for performing the assigned task within the boundaries set by the sandbox and the configuration provided by the harness.

This is the thing that actually reasons and acts. Everything else in this document exists to support, constrain, or coordinate it.

The runner talks to every runtime through one contract, so the harness, sandbox, hook scripts and credentials are shared; only the in-sandbox config directory and the way hooks are wired differ per runtime:

```mermaid
flowchart TB
  subgraph RUNNER["fullsend run — runner host"]
    direction LR
    CFG[".fullsend/config.yaml\nruntime: claude | pi | codex | dummy | dummy-playback"]
    RT["runtime.Runtime\nBootstrap · Run (+ TranscriptHandler)"]
    HOOKS["security.HookPlan\nruntime-neutral scripts (ADR 0090)"]
    CFG --> RT
  end
  subgraph SANDBOX["OpenShell sandbox — same image, policy and egress"]
    direction LR
    CC["Claude Code\nclaude -p --agent\nhooks via --settings"]
    PI["pi\npi --print --mode json\nhooks via fullsend-hooks.js"]
    CX["codex\ncodex exec --json\nhooks via hooks.json + adapter"]
    DM["dummy\nscripted ops\n(behaviour tests)"]
    DP["dummy-playback\nplaylist replay\n(behaviour tests)"]
  end
  RT -->|"/sandbox/claude-config"| CC
  RT -->|"/sandbox/pi-config"| PI
  RT -->|"/sandbox/codex-config"| CX
  RT --> DM
  RT --> DP
  HOOKS -.-> CC
  HOOKS -.-> PI
  HOOKS -.-> CX
  VX["Vertex AI — *.googleapis.com\nWIF: OIDC token → STS"]
  OA["OpenAI — api.openai.com\nWIF: OIDC token → run-scoped provider"]
  CC --> VX
  PI -->|"same credential path"| VX
  CX -->|"POST /v1/responses only"| OA
  classDef opt fill:#e3e9fb,stroke:#2d5be3,color:#1b2230;
  classDef def fill:#eceee8,stroke:#a9afa4,color:#1b2230;
  class PI,CX opt;
  class CC,DM,DP def;
```

**Decided (implementation):**

- The `fullsend run` runner delegates in-sandbox agent execution to a `runtime.Runtime` interface; production orgs default to Claude Code, with [pi](https://github.com/earendil-works/pi) available as an opt-in second runtime (`runtime: pi`, Claude-on-Vertex through the same WIF credential path) and [codex](https://github.com/openai/codex) as a third (`runtime: codex`, OpenAI-only through a custom model provider whose bearer token comes from a runner-seeded file, with the sandbox tool hooks behind a translating adapter — [ADR 0099](ADRs/0099-codex-agent-runtime.md) and [ADR 0100](ADRs/0100-codex-sandbox-hooks.md)). Runtime selection is configured per repo with `runtime:` in `.fullsend/config.yaml` (per-agent `runtime`/`model`/`effort`/`subagents` on the agent's `agents:` entry sit above it and below the `--runtime`/`--model`/`--effort` flags and `FULLSEND_*` variables, [ADR 0091](ADRs/0091-per-agent-runtime-model-effort.md)) and resolved via `runtime.ResolveForAgent()`. Test-only runtimes — **dummy** (scripted operations) and **dummy-playback** (playlist-based replay of canned results) — execute in the real OpenShell sandbox for behaviour tests without inference. Bootstrap uses a portable `BootstrapInput` interface with optional extensions such as `SandboxHooksBootstrap` for the runtime-neutral sandbox tool hooks ([ADR 0090](ADRs/0090-runtime-neutral-sandbox-hooks-contract.md)); runtimes declare further capabilities through small optional interfaces (`DebugLogNamer`, `ContextBridger`) rather than `Name()` checks in the runner. Transcript and debug artifact handling use a separate `TranscriptHandler` interface. See [runtimes.md](runtimes.md) for the per-runtime security feature matrix required when adding a new backend.
- Plugins are runtime-scoped harness resources: a harness declares `plugins:` as one list of directories in its own repository (same trust and fetch path as skills), and each entry's format decides which runtime loads it — a `plugin.json` bundle is Claude Code's, a directory pi's `-e` loader resolves is uploaded and loaded after a tree-hash preflight computed from the host copy. Each runtime names and skips the entries in the other format, so the list survives a runtime switch. Because `--no-extensions` plus explicit `-e` closes the set of code that can register tools, no per-tool declaration is needed ([ADR 0094](ADRs/0094-pi-extensions-are-harness-resources.md)).

### Behaviour testing

End-to-end **behaviour tests** use the shared framework in `pkg/behaviourtest/` (with live-test infrastructure in `internal/e2etest/`); the in-repo runner and Gherkin features live under `e2e/behaviour/`. They validate deterministic platform code — dispatch routing, harness loading, sandbox policy, SCM mutations — with the LLM layer removed via the dummy and dummy-playback runtimes. Tests exercise real GitHub (and GitLab) SCM and GitHub Actions CI through pluggable drivers; Gherkin scenarios stay install-mode agnostic while runner env vars select backends. This coverage is **orthogonal** to LLM and instruction testing in [testing-agents.md](problems/testing-agents.md). See [ADR 0066](ADRs/0066-behaviour-tests-with-gherkin-and-drivers.md). The dummy runtime handles single-agent sandbox verification; the dummy-playback runtime replays canned results from an ordered playlist to enable multi-agent sequential scenarios (triage → code → review → fix) without inference ([ADR 0116](ADRs/0116-dummy-playback-runtime.md)).

**Open questions:**

- Is the runtime a single model call, a loop (plan-act-observe), or something more structured?
- How does the runtime interact with the sandbox boundaries — does it know what it can't do, or does it just hit walls? (For tool access: both — prose instructions inform the runtime, and `permissions.deny` hard-blocks execution; see [ADR 0027](ADRs/0027-allowed-and-disallowed-tools-for-agents.md). Broader sandbox interaction remains open.)
- How do we swap model providers or versions without changing the rest of the stack?
- What is the interface between the harness and the runtime? (A system prompt? A configuration file? An API contract?) (Decided: the runner-side contract is `runtime.Runtime` + `BootstrapInput`, the runtime-neutral sandbox tool-hook contract in [ADR 0090](ADRs/0090-runtime-neutral-sandbox-hooks-contract.md), and optional capability interfaces. Each runtime translates the harness prompt into its own instruction slot — pi's `APPEND_SYSTEM.md`, codex's `developer_instructions` ([ADR 0099](ADRs/0099-codex-agent-runtime.md)) — rather than a shared format.)

## Agent Identity Provider

The system that gives agents credentials to act on external services. Responsible for issuing, scoping, rotating, and revoking the identities agents use to interact with GitHub, container registries, and other APIs.

Identity is not the same as trust. An agent's identity lets it authenticate to external services; the trust model is defined by repository permissions and CODEOWNERS, not by which credentials the agent holds. (See [agent-architecture.md](problems/agent-architecture.md) — "trust derives from repository permissions, not agent identity.")

**Decided:**

- Credential delivery model: four tiers — (1) prefetch + post-process for agents with enumerable inputs (zero credential access), (2) OpenShell providers + L7 egress policies for static token auth (credentials never enter sandbox), (3) host-side REST server for operations providers cannot handle — long-running operations, sandbox capability gaps, credentials in request bodies, response transformation, and multi-step atomic operations (see [ADR 0046](ADRs/0046-host-side-api-server-design.md)), (4) host files + L7 policies for complex auth requiring in-sandbox credential files. L7 policies enforce both method + path and binary-level restrictions. Providers are preferred over REST servers when viable ([ADR 0017](ADRs/0017-credential-isolation-for-sandboxed-agents.md), extended by [ADR 0025](ADRs/0025-provider-credential-delivery-for-sandboxed-agents.md)). OpenAI inference on the pi runtime is the first tier-2 use: the runner exchanges the job's GitHub OIDC token for a short-lived OpenAI access token and hands it to a run-scoped OpenShell provider, so no OpenAI credential enters the sandbox ([ADR 0092](ADRs/0092-openai-wif-credential-delivery.md)); the codex runtime reuses that provider and follows its refreshes by re-reading a runner-seeded token file through codex's `auth.command`, since its process environment cannot carry a placeholder that survives a refresh ([ADR 0099](ADRs/0099-codex-agent-runtime.md)).
- GitHub Packages for sandboxed installs: the job's Actions workflow token is preserved as `GH_WORKFLOW_TOKEN`, readable only by provider credential expansion, and a repo-level provider binds it to `npm.pkg.github.com` (read-only, enforced) and the tarball CDN (which serves pre-signed URLs); forge identity stays the minted App token, which cannot read public packages owned by another org ([ADR 0114](ADRs/0114-github-packages-via-host-bound-workflow-token-provider.md)).
- Host-side API server design: Credential delivery tier 3 servers follow a uniform process contract (`--port`, `--token`, `--bind-address`, `/healthz`, `/tools.json`, `SIGTERM`). Network access is controlled via composable provider profiles — atomic capability profiles composed per-harness. Per-run UUID bearer tokens are delivered through OpenShell provider placeholders. File transfer uses `openshell sandbox upload/download` ([ADR 0046](ADRs/0046-host-side-api-server-design.md)).
- Per-role GitHub Apps with manifest-based creation. Each agent role gets its own app with scoped permissions. PEMs stored in Secret Manager as `fullsend-{role}-app-pem` — one secret per role, shared across orgs on a mint. `ROLE_APP_IDS` uses the same shared-per-role model (`coder` → app ID). Org isolation is enforced via `ALLOWED_ORGS`, WIF conditions, and installation verification ([ADR 0007](ADRs/0007-per-role-github-apps.md), [ADR 0033](ADRs/0033-per-repo-installation-mode.md)). Public multi-tenant mint (`ALLOWED_ORGS=*`) with upstream-only workflow provenance is defined in [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); upstream-only provenance limits which workflows can call the mint, complementing [ADR 0029](ADRs/0029-central-token-mint-secretless-fullsend.md) multi-tenant blast-radius concerns.
- Cross-org mint authorization: workflows may request tokens for a different org via optional `target_org` when the target org installs the role App and sets `FULLSEND_FOREIGN_<role>_REPOS` ([ADR 0060](ADRs/0060-cross-org-mint-authorization-via-org-variables.md)). Repo-level `FULLSEND_FOREIGN_<role>_REPOS` variables enable per-repo foreign grants (scoped to the specific target repo) and intra-org cross-repo access for per-repo callers, with disjoint authorization boundaries from org-level grants — repo-level for repo-scoped requests, org-level for installation-wide requests ([ADR 0083](ADRs/0083-repo-level-foreign-allow-list.md)).
- Mint `repos` scope: foreign mints with `repos: ["*"]` require an org-level FOREIGN grant; foreign mints with specific repos require per-repo FOREIGN grants on each requested repo (org-level grants are not consulted for repo-scoped requests). Per-repo callers (repo in `PER_REPO_WIF_REPOS`) must list exactly the requesting repository unless authorized by repo-level FOREIGN grants for other repos. Per-org callers (org in `ALLOWED_ORGS`, repo not in `PER_REPO_WIF_REPOS`) get org-mode shapes: `.fullsend` callers may use any non-empty validated list; other callers may use `[.fullsend]` or `{self,.fullsend}`. Same-org installation-wide tokens are denied ([ADR 0077](ADRs/0077-mint-repos-scope-hardening.md), simplified in [ADR 0078](ADRs/0078-simplified-mint-authorization-policy.md)).
- Workflow-host allow-list: `WORKFLOW_HOST_REPOS` controls which repos may host workflows calling the mint for per-repo and public-mode callers (default: `fullsend-ai/fullsend`). Per-org callers hard-wire to `{org}/.fullsend` and upstream. Public mode is not special-cased — it uses the same per-repo validation path with `WORKFLOW_HOST_REPOS` and the basename allowlist. This separates caller enrollment from workflow-host trust ([ADR 0082](ADRs/0082-workflow-host-allow-list.md)).
- Standalone mint deployment: `cmd/mint/` provides a self-contained HTTP server that uses direct JWKS verification and filesystem PEM storage instead of GCP infrastructure. It shares the `internal/mintcore/` library with the GCF mint and adds support for custom role permissions and a fallback proxy to an upstream mint. Custom role permissions live in mintcore (not `cmd/mint/`) so that `HasRole`, `RolePermissionsForLevel`, and `CreateInstallationToken` return a unified view without callers needing to distinguish built-in from custom roles. Both the standalone and GCF mints call `ParseCustomRolePermissions` + `RegisterCustomRoleLevels` when `CUSTOM_ROLE_PERMISSIONS` is set. See the [standalone mint guide](guides/infrastructure/standalone-mint.md). For mintcore internals (platform accessors, load-site construction, WASM constraints), see the [mintcore contributor guide](contributing/mintcore.md).
- Hosted public community mint: steady-state deployment on Cloudflare Workers (JWKS + WAF + single ops console), with interim GCP Cloud Function acceptable until the Worker port is production-ready. Trust policy (`ALLOWED_ORGS=*`, upstream-only workflow provenance) is in [ADR 0059](ADRs/0059-public-mint-mode-with-wildcard-allowlists.md); deployment, edge security, monitoring, and phasing are in [ADR 0068](ADRs/0068-public-community-mint-architecture.md). Enrollment is installing the shared Apps—no per-org mint env registration ([#1145](https://github.com/fullsend-ai/fullsend/issues/1145)).
- Named privilege levels: each role defines named levels as keys — `read` and `write` are mandatory, extra named levels are allowed on custom roles. The mint looks up the requested level on the role and returns the stored permission map, or fails if the level is missing. Built-in roles statically define both `read` (all values `"read"`) and `write` (the canonical ceiling) in the permission table. The mint API accepts an optional `level` field (default `write` — temporary compatibility default; a future release will change to `read`). Flat-format `CUSTOM_ROLE_PERMISSIONS` entries are stored as both `read` and `write` (same permissions for either level). Multi-level format uses a `levels` key for distinct per-level maps; extra named levels beyond read/write are permitted. The harness `privilege_levels` flag maps run-stages to levels; omitting it defaults to `write`, preserving backward compatibility for existing harness configurations ([ADR 0073](ADRs/0073-named-mint-privilege-levels.md)).
- Adding a built-in agent role: a request must pass six tests — no path
  without a role (the user's own workflow with the job token, or an existing
  role), the agent's output needs the scope (no write access on control-plane
  scopes such as `actions` or `workflows`), the App's name shows on the issue
  or pull request where it acts, one purpose with named endpoints, every write
  group justified in combination with the others, and a need beyond one
  agent. Roles with two or more write groups get the closest review. A need
  specific to one agent goes to a custom role on its author's standalone mint
  ([ADR 0124](ADRs/0124-criteria-for-adding-a-built-in-agent-role.md)).

One concrete implementation option is [`oidcx`](https://github.com/oxidecomputer/oidcx): a service that accepts OIDC identity tokens and exchanges them for short-lived access tokens. It can mint tokens scoped to selected GitHub repositories and permissions, or to selected Oxide silos and permissions, and it also ships with a GitHub Action wrapper. In a Fullsend deployment, this can be used by the sandbox entrypoint to narrow a broad GitHub App identity down to only the specific permissions an agent needs for the current run.

**Open questions:**

- ~~What identity model fits best — separate bot accounts per agent role, a single bot account with role metadata, GitHub App installations, or something else?~~ Decided in [ADR 0007](ADRs/0007-per-role-github-apps.md).
- How are credentials rotated and revoked, and who has authority to do that?
- Does the identity provider integrate with existing secrets management, or is it a new system?
- How will per-role identity work on GitLab and Forgejo, which lack GitHub's app manifest flow? GitLab uses per-role Developer-level bot PATs (Poller/Analyst/Coder) stored as protected CI/CD variables; poller state lives on dedicated HMAC-signed branches rather than Maintainer-only CI/CD variables — see [ADR 0067](ADRs/0067-gitlab-cron-polling-event-dispatch.md). The registered-role credential contract (built-in Poller/Analyst/Coder plus administrator-registered custom roles) is specified in [gitlab-role-credentials.md](contributing/gitlab-role-credentials.md); job routing (#7499, and #7782 for runtime authentication) has landed for `fullsend poll`, `fullsend run`, and `fullsend post-review`, selecting the registered role credential unconditionally and failing closed if that secret is missing — there is no shared `FULLSEND_FORGE_TOKEN` fallback.
- Which agent roles need Discussions (or other chat) write scopes, and how do those scopes map onto named mint privilege levels? Conversation participation requires least-privilege identity deltas per [ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md).

## Agent Dispatch and Coordination Layer

The mechanism that assigns work to agents and prevents conflicts. Responsible for translating triggers (GitHub events, schedules, manual requests) into agent tasks and ensuring two agents don't work the same problem simultaneously.

The existing design principle is that [the repo is the coordinator](problems/agent-architecture.md#interaction-model-the-repo-as-coordinator) — branch protection, CODEOWNERS, status checks, and GitHub events provide coordination without a central orchestrator. The agent dispatch and coordination layer may be nothing more than the glue that connects GitHub webhooks to agent infrastructure. Or it may need to be more.

**Decided:**

- Event-driven stage dispatch runs synchronously via `workflow_call` to preserve run correlation in the GitHub Actions UI (see [ADR 0041](ADRs/0041-synchronous-workflow-call-event-dispatch.md)).
- Routing moves from workflow bash to harness CEL `trigger` expressions
  evaluated by `fullsend dispatch` with pluggable input/output drivers
  operating on a `NormalizedEvent` struct
  ([ADR 0061](ADRs/0061-harness-cel-dispatch.md), partially superseded by
  [ADR 0098](ADRs/0098-entity-first-harness-evaluation.md)).
- Automatic runs are serialized per harness and event subject. Later events do
  not cancel an active run. Each event still passes through authorization and
  CEL routing; the execution platform coalesces matching events into one latest
  pending run, and each run reconciles the subject's current state. Authority
  over other comments and content discovered during reconciliation remains a
  separate decision
  ([ADR 0106](ADRs/0106-serialize-agent-runs-and-coalesce-subsequent-events.md)).
- Dispatch runs no follow-up stage after an agent. Every run publishes the
  `fullsend-<agent>` artifact, and users who want to act on a result chain
  their own workflow on the shim's `workflow_run` event
  ([guide](guides/user/chaining-follow-up-workflows.md),
  [ADR 0124](ADRs/0124-criteria-for-adding-a-built-in-agent-role.md)).
- Per-repo **polling** complements webhook dispatch: `fullsend poll` uses poll
  input drivers to discover work from remote systems (Jira first), coordinates
  via source-native write-then-verify locks, and feeds the same dispatch pipeline
  as webhooks ([ADR 0063](ADRs/0063-polling-based-work-discovery.md)). Initial
  scope is per-repo mode only.
- Harness routing uses one CEL predicate over a required normalized entity and
  an optional prompting event. Webhooks provide low-latency candidates;
  `fullsend poll` and its input drivers enumerate and resolve scheduled
  candidates without reconstructing a complete event stream. Entity activity
  retains actor and authorization provenance, while harness-defined evidence —
  existing entity activity or explicit receipts — distinguishes handled work
  ([ADR 0098](ADRs/0098-entity-first-harness-evaluation.md), partially
  superseding [ADR 0063](ADRs/0063-polling-based-work-discovery.md)).
- GitLab dispatch uses a native webhook fast-path for low-latency candidates and the cron-poller as the authoritative reconciliation backstop ([ADR 0125](ADRs/0125-gitlab-hybrid-webhook-poller-dispatch.md)). Native `merge_request_event` dispatch stays removed ([#7322](https://github.com/fullsend-ai/fullsend/issues/7322)). No external webhook bridge (see [ADR 0067](ADRs/0067-gitlab-cron-polling-event-dispatch.md) for the poller, credentials, and in-job gate).
- Conversation participation: GitHub Discussions (and future chat systems) enter
  dispatch as resolved entities with `entity.kind: conversation`; when a
  prompting event is available, it expresses threading on
  `transition.comment.id` / `parent_id` (`parent_id` always names the thread
  root). They reuse CEL harness triggers and ADR 0054 authorization, and write
  back through host/post-script or host-side API servers via
  `conversation.Client` — not a separate always-on chat bot and not an extension
  of `forge.Client`
  ([ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md)).
- Event-backed dispatch authorization: event-triggered paths authorize the prompting
  actor before dispatch. This includes schedule/manual dispatch represented as
  a `NormalizedEvent`, whose actor is the configured service identity. GitHub
  paths check the acting user's collaborator permission via the repository API
  (`write` or above for mutation commands; `triage` or above for observation
  stages); a repo that enables the `owners_file` authorization provider also
  grants these roles to its Prow `OWNERS` approvers and reviewers. Non-GitHub event paths map source-system roles to dispatch
  authorization roles (`read`, `write`, `admin`) using source-native role
  resolution, with no cross-system identity verification
  ([Authorization Contract v1](normative/authorization/v1/);
  [ADR 0054](ADRs/0054-require-authorization-on-all-agent-dispatch-paths.md)).
- Poll entity-discovery authorization: `fullsend poll` has no prompting event
  actor; verified, non-user-assertable Fullsend invocation provenance authorizes
  entity enumeration and evaluation, and callers without it are denied. Before
  each candidate harness's CEL predicate, the platform fail-closed enumerates a
  closed superset of action-indicating entity elements, resolves each actor's
  current permission, and removes elements below that harness's observation or
  mutation threshold. Later input-selection and injection filtering are defense
  in depth for prompt construction. Entity-first execution remains disabled
  until its versioned normalized-entity contract exists. Every run uses the
  harness's configured identity
  ([ADR 0098](ADRs/0098-entity-first-harness-evaluation.md)).

**Open questions:**

- What normative entity-history, query-planning, and handled-state contract
  can support entity-first harness evaluation without unbounded provider reads
  ([ADR 0098](ADRs/0098-entity-first-harness-evaluation.md))?
- Is GitHub's event system sufficient for forge-native duplicate protection, or
  do we need additional coordination beyond label/state conventions and agent
  idempotency? (Jira polling per ADR 0063 uses entity-property locks and runner
  lock refresh; ADR 0098 does not resolve this question.)
- How does work assignment interact with the backlog/priority agent described in [agent-architecture.md](problems/agent-architecture.md)?
- How should explicit cancellation, retry, and reassignment interact with the
  automatic event-coalescing policy in
  [ADR 0106](ADRs/0106-serialize-agent-runs-and-coalesce-subsequent-events.md)?
- Does the coordinator need state (a queue, a lock, a claim system), or can it be stateless and event-driven?
- When should a conversation or thread be linked to a work item (e.g. Discussion
  → issue) so a conversation-native agent can hand off to `/fs-code` without
  violating entity-context separation ([ADR 0076](ADRs/0076-slash-command-entity-context-separation.md),
  [ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md))?
- How should concurrent agent runs that touch the same conversation thread be
  coordinated ([ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md))?

## Policy Store

Where agent behavioral rules live. Responsible for holding autonomy levels, review requirements, allowed operations, and escalation rules — the configuration that governs what agents may do.

Policy is distinct from the harness (which configures *how* an agent works) and from intent (which defines *what* work is authorized). Policy defines the *boundaries* of agent behavior — what an agent is allowed to do regardless of what it's asked to do.

Each target repository's **`.fullsend/`** directory holds policy
configuration — guardrails, autonomy levels, and escalation rules governed by
the repo's CODEOWNERS and review process
([ADR 0033](ADRs/0033-per-repo-installation-mode.md)).

**Open questions:**

- How is policy versioned, and how do we ensure agents run under the correct policy version?
- Who can change policy, and what approval process governs policy changes? (See [governance.md](problems/governance.md).)
- How does policy interact with the autonomy spectrum — is the auto-merge vs. escalate decision a policy setting? (See [autonomy-spectrum.md](problems/autonomy-spectrum.md).)

## Intent Source

The system that provides authorized intent for agent work. Responsible for representing what changes are wanted, who authorized them, and at what intent authorization tier of approval.

Intent answers the question "should this change exist?" before anyone asks "is this change correct?" Without authorized intent, an agent has no basis for deciding what to work on or whether its output matches what was asked for.

Each target repository's **`.fullsend/`** directory holds the pointer to the
intent source (for example, `intent_repo: your-org/features`), so tooling
discovers where intent lives without hardcoding
([ADR 0033](ADRs/0033-per-repo-installation-mode.md)).

**Open questions:**

- What is the right representation — forge issues, a dedicated intent repo, RFCs, or tiered combinations? (See [intent-representation.md](problems/intent-representation.md).)
- How do agents verify that intent is authentic and hasn't been tampered with?
- How do different intent authorization tiers (standing rules, tactical issues, strategic features) map to different authorization requirements?
- How does intent interact with the "try it" phase — agents building exploratory drafts before authorization? (See [intent-representation.md](problems/intent-representation.md).)

## Observability

The logging, tracing, and audit layer for agent actions. Responsible for making every agent action attributable, traceable, and reviewable — both for debugging failures and for security auditability.

Observability is a cross-cutting concern that touches every other component. Each component produces signals; this component is responsible for collecting, storing, and making them useful.

**Decided:**

- JSONL reasoning trace exposure: raw JSONL conversation transcripts are extracted from sandboxes and stored with owner-scoped access. Credential scanning acts as an invariant check on [ADR 0017](ADRs/0017-credential-isolation-for-sandboxed-agents.md)'s isolation model. Agents handling data from protected sources beyond the target repo can opt in to JSONL suppression via configuration ([ADR 0021](ADRs/0021-jsonl-reasoning-trace-exposure.md)).
- Event-driven stage dispatch remains traceable end-to-end in the GitHub Actions UI by using synchronous `workflow_call` dispatch (see [ADR 0041](ADRs/0041-synchronous-workflow-call-event-dispatch.md)).
- Scheduled entity-discovery runs are attributable to the verified Fullsend
  invocation identity, target repository, effective policy, harness revision,
  and resolved entity; retained action-indicating elements preserve their actor
  provenance. State-only predicates trace to the configured service identity
  and versioned policy/harness configuration
  ([ADR 0098](ADRs/0098-entity-first-harness-evaluation.md)).
- Distributed tracing: framework-native OpenTelemetry instrumentation with zero-configuration baseline. Every run produces `run-telemetry.jsonl` locally; optional live OTLP export to any compatible backend. W3C trace context propagation links multi-agent pipelines into unified traces. OTEL GenAI semantic conventions enable LLM-aware backends ([ADR 0050](ADRs/0050-distributed-tracing-instrumentation.md)).
- Eval measurements: the concept of scoring traces ([fail-open](glossary.md#fail-open)). [OTEL primary facts](glossary.md#otel-primary-facts) stay on the run trace (`run-telemetry.jsonl`); [OTEL derived products](glossary.md#otel-derived-products) are the scores (`eval-measurements.jsonl`) ([ADR 0087](ADRs/0087-eval-measurements-online-trace-scoring.md)). See [Eval Measurements](guides/infrastructure/eval-measurements.md). When `OTEL_EXPORTER_OTLP_*` is set, scores also export as `gen_ai.evaluation.result` span events on the same TraceID (same OTLP path as agent traces; fail-open).

- Tool-call span topology: every id-bearing tool call the runtime reports — Claude Code today; pi and codex emit no call ids — becomes an `execute_tool` child span of its iteration's `agent` span, up to 1,024 per iteration. The spans carry semantic-convention metadata only, timed at runner receipt; tool content stays on the `agent` span's message record ([ADR 0108](ADRs/0108-tool-call-span-topology.md)).

**Open questions:**

- What signals matter most — cost, latency, token usage, action logs, decision traces, or something else?
- ~~How do we balance detailed tracing (useful for debugging) with the volume of data agents will produce?~~ Decided in [ADR 0050](ADRs/0050-distributed-tracing-instrumentation.md): instrument all lifecycle steps comprehensively; volume is managed by backends not by suppressing data at the source.
- ~~How do we score wild agent traces for trends without a second export stack?~~ Decided in [ADR 0087](ADRs/0087-eval-measurements-online-trace-scoring.md): eval measurements write local JSONL beside telemetry when at least one new score row is produced (including `label: skip`); portable remote export uses the same OTLP config as traces (`gen_ai.evaluation.result` events). The JSONL is absent (not empty) when telemetry/manifest is missing, no traces match, or every candidate is already in the ledger.
- What is the retention and access model for agent logs? Who can see what? (JSONL trace access model decided in [ADR 0021](ADRs/0021-jsonl-reasoning-trace-exposure.md); retention policy and broader log access remain open.)
- How does observability interact with the security requirement that "every
  action is logged, attributable, and reviewable"? Scheduled entity-discovery
  attribution is decided in
  [ADR 0098](ADRs/0098-entity-first-harness-evaluation.md); broader audit-log
  requirements remain open. (See
  [security-threat-model.md](problems/security-threat-model.md).)
- Is there a real-time monitoring requirement (agent is stuck, agent is behaving anomalously), or is observability primarily forensic?

## Agent Registry

The catalog of available agent roles and their configurations. Responsible for defining what agent types exist, what capabilities each has, and how they are instantiated.

The registry is the bridge between the abstract roles defined in [agent-architecture.md](problems/agent-architecture.md) (correctness sub-agent, intent & coherence sub-agent, security sub-agent, etc.) and the concrete runtime configurations that the harness uses to set up each agent.

Fullsend provides a base set of agent definitions. Each target repository's
**`.fullsend/`** directory extends this with repo-specific agents, following
the inheritance model: fullsend defaults, then repo baseline (`config.base.yaml`
/ harness `base:` references), then repo overrides
([ADR 0033](ADRs/0033-per-repo-installation-mode.md),
[ADR 0058](ADRs/0058-agent-registration.md)).

**Decided:**

- Config-level agent registration: an `agents` list in both `OrgConfig` and `PerRepoConfig` declares agent harness sources as pinned URLs or local paths, replacing compiled-in agent discovery ([ADR 0058](ADRs/0058-agent-registration.md)).
- Runtime resolution: `fullsend run <name>` resolves agents in two tiers: (1) config entries from `OrgConfig.Agents` (highest priority), (2) runtime fallback to the `fullsend-ai/agents` repository for known first-party agents not in config. The agents-repo fallback is a transitional mechanism for the agent extraction; it will be removed once all users have migrated to config-driven registration (ADR 0058 Phase 5).
- Config lookup: config entries are looked up directly via `findConfigAgentEntry`; the agents-repo fallback operates independently when the agent is not found in config. Builds on [ADR 0045](ADRs/0045-forge-portable-harness-schema.md) harness identity model.
- CLI management: `fullsend agent add|list|set|update|remove` manages config entries and auto-pins URLs to a commit SHA with an integrity hash.
- Agent generation: `fullsend agent new <name>` writes a complete custom agent — harness, agent definition, result schema, post-script, and the policy, providers and profiles a per-repo install does not vendor — validates it with the loader dispatch uses, and registers it through the `agent add` path above. A `trigger:` is mandatory, because a trigger-less harness registers and validates and is then silently never dispatched; `--role` is a closed table of the roles the hosted mint serves, so an unservable role fails locally rather than as a `403` at first dispatch ([ADR 0102](ADRs/0102-generate-custom-agents-from-the-cli.md)).

**Open questions:**

- How are new agent roles added, tested, and promoted to production? (See [testing-agents.md](problems/testing-agents.md).) (Functional tests provide a framework for testing agent roles against controlled fixtures — [ADR 0052](ADRs/0052-functional-tests-for-agent-pipelines.md). Admission criteria for a new built-in role are in [ADR 0124](ADRs/0124-criteria-for-adding-a-built-in-agent-role.md). Promotion workflow remains open.)
- Does the registry include version information, so we can roll back to a previous agent configuration?
- How does the registry relate to the policy store — does policy reference registry entries, or are they independent?

## Reference workflow components (ADR 0002)

The [Initial Fullsend Design](ADRs/0002-initial-fullsend-design.md) describes a concrete GitHub-centric issue→merge workflow. Its **building blocks** are named below so this document and the ADR stay aligned. Descriptions are brief; the ADR is normative for behavior.

### 1. Webhook + dispatch service

Normalizes GitHub events (issue/PR/label/comment/check/merge), deduplicates flapping events, and dispatches work to agent runtimes.
ADR 0002: [Building block 1](ADRs/0002-initial-fullsend-design.md#1-webhook--dispatch-service).

### 2. Slash-command parser + ACL

Parses `/fs-triage`, `/fs-code`, `/fs-review`, and related commands and enforces who is allowed to invoke each. Commands are restricted to the entity context where their agent's inputs exist — `/fs-code` dispatches only from issues (no associated PR), `/fs-fix` and `/fs-review` only from PRs ([ADR 0076](ADRs/0076-slash-command-entity-context-separation.md)). Conversation surfaces (GitHub Discussions and future chat systems) are a separate entity context: conversation-native agents may listen on conversations/threads there, but code-mutating slash commands do not ([ADR 0086](ADRs/0086-conversation-surface-for-agent-participation.md)).
ADR 0002: [Building block 2](ADRs/0002-initial-fullsend-design.md#2-slash-command-parser--acl).

### 3. Label state machine guard

Validates legal label transitions and enforces mutual exclusion and run-start reset semantics (triage start clears **`duplicate`** and downstream labels; **`blocked`** is cleared by the post-script when a non-blocked outcome is reached; PR/review strips per ADR).
ADR 0002: [Building block 3](ADRs/0002-initial-fullsend-design.md#3-label-state-machine-guard).

### 4. triage agent runtime

Runs triage from issue `title`/`body` + GitHub-native attachments only; each run starts with **`duplicate`** and other reset labels cleared; duplicate detection, prerequisite detection (cross-repo), readiness, reproducibility, test handoff; can close as duplicate again if still a match, label **`blocked`** when progress depends on another open issue or PR, or create upstream prerequisite issues when no tracking issue exists (controlled by `create_issues.allow_targets` config).
ADR 0002: [Building block 4](ADRs/0002-initial-fullsend-design.md#4-triage-agent-runtime).

### 5. Duplicate / similarity search

Provides candidate duplicate retrieval and confidence scoring for triage duplicate decisions.
ADR 0002: [Building block 5](ADRs/0002-initial-fullsend-design.md#5-duplicate--similarity-search).

### 6. Repro sandbox template

Isolated environment used by triage for reproducibility checks.
ADR 0002: [Building block 6](ADRs/0002-initial-fullsend-design.md#6-repro-sandbox-template).

### 7. Test artifact formatter

Formats triage test artifacts in repo-native conventions for PR handoff.
ADR 0002: [Building block 7](ADRs/0002-initial-fullsend-design.md#7-test-artifact-formatter).

### 8. code agent runtime

Implements changes, runs local/CI-equivalent tests, handles check failures, and opens or updates a PR. Review dispatch is triggered automatically by `pull_request_target` events.
ADR 0002: [Building block 8](ADRs/0002-initial-fullsend-design.md#8-implementation-agent-runtime).

### 9. PR sandbox / CI mirror

Execution environment for **Code** and test loops, aligned to contributor/CI toolchains.
ADR 0002: [Building block 9](ADRs/0002-initial-fullsend-design.md#9-pr-sandbox--ci-mirror).

### 10. Check failure triage

Fetches and classifies failing check logs to guide **code agent** remediation loops.
ADR 0002: [Building block 10](ADRs/0002-initial-fullsend-design.md#10-check-failure-triage).

### 11. review agent runtime

Runs N parallel **review agent** invocations and produces structured review verdicts/comments.
ADR 0002: [Building block 11](ADRs/0002-initial-fullsend-design.md#11-review-agent-runtime).

**Decided:**

- PR-level risk assessment scoring: pre-pass sub-agent computes a composite 1–5
  risk score from metadata, git history, and linked-issue signals
  ([ADR 0089](ADRs/0089-pr-risk-assessment-scoring.md)).

### 12. Coordinator merge algorithm

Aggregates review verdicts and applies labels:

- unanimous approve-merge → `ready-for-merge` (for the **current** PR head at the end of that round only)
- unanimous rework → triggers [fix agent](agents/fix.md)
- split/conflicting (including conflicting security severities) → `requires-manual-review`
- each **review run start** (including push-triggered re-review) clears **`ready-for-merge`** together with **`ready-for-review`** so merge approval is never stale after new commits
ADR 0002: [Building block 12](ADRs/0002-initial-fullsend-design.md#12-coordinator-merge-algorithm).

### 13. Observability

Traceability layer across issue, **Triage**, **Code**, **Review**, checks, and merge for incident response and correlation across automation runs.
ADR 0002: [Building block 13](ADRs/0002-initial-fullsend-design.md#13-observability).

### 14. retro agent runtime

Retrospective analyst — examines completed or in-progress agent workflows, identifies improvement opportunities, and files proposals as GitHub issues. Runs automatically on PR close (merged or rejected) and on-demand via `/fs-retro` command. Analyzes the full workflow graph (triage, code, review, fix agent interactions and human interventions) and posts a summary comment on the originating PR/issue linking to all filed proposals.

## Configuration layering

Fullsend uses a three-tier configuration inheritance model for all configuration: agent definitions, skills, plugins, policies, harness definitions, and guardrails. Each configuration tier can extend or override the one below it. Guardrails can only be tightened, never weakened.

```


  ┌──────────────────────────────────────────────────────────────────┐
  │  fullsend-ai/fullsend                    (upstream open source)  │
  │                                                                  │
  │  Framework defaults:                                             │
  │    base agents, skills, plugins, policies                         │
  │    fullsend CLI (fullsend run, fullsend install, ...)            │
  │    scaffold templates, security scanners                         │
  │                                                                  │
  │  Owned by: fullsend project maintainers                          │
  ├──────────────────────────────────────────────────────────────────┤
  │  <org>/.fullsend                              (dedicated repo)   │
  │                                                                  │
  │  Org-wide configuration:                                         │
  │    agents/            org agent definitions (.md)                │
  │    skills/            org skills (shared across repos)           │
  │    policies/          sandbox network/filesystem policies        │
  │    harness/           per-agent harness configs (.yaml)          │
  │    guardrails.yaml    org-wide guardrails (can only be tightened)│
  │    config.yaml        intent repo, runtime, infrastructure       │
  │                                                                  │
  │  Owned by: org platform team (CODEOWNERS, human-only)            │
  ├──────────────────────────────────────────────────────────────────┤
  │  <org>/<repo>                               (directory in repo)  │
  │                                                                  │
  │  Repo-specific overrides:                                        │
  │    AGENTS.md          per-repo agent instructions                │
  │    skills/            repo-specific skills (domain knowledge)    │
  │    .fullsend/config   overrides -  adjust timeouts, prompts      │
  │                                                                  │
  │  Owned by: repo maintainers (CODEOWNERS)                         │
  └──────────────────────────────────────────────────────────────────┘

  Inheritance:  fullsend defaults  <  org .fullsend config  <  per-repo overrides
                (base)                (extend/override)        (extend/tighten)
```

In per-repo installation the middle tier is replaced by files inside the
target repo: `.fullsend/config.base.yaml` (vendor preset or baseline) and
`.fullsend/config.yaml` (repo overlay), with code defaults below both. The
org-tier box above describes the historical per-org model, now deprecated
([ADR 0044](ADRs/0044-deprecate-per-org-installation-mode.md),
[ADR 0069](ADRs/0069-ready-made-configuration-presets.md)).

Skills flow downward through this stack. A repo-level skill might encode domain knowledge ("this repo uses a custom ORM — here's how queries work"). An org-level skill might encode org conventions ("all services use structured logging via zerolog"). Upstream fullsend provides foundational skills (code implementation, triage coordination, testing conventions).

AGENTS.md files follow the same layering. A repo's `.fullsend/AGENTS.md` gives agents repo-specific instructions (build commands, test patterns, architectural constraints). The org's `.fullsend/agents/` directory provides role-specific agent definitions that apply across all enrolled repos.

See [ADR 0003](ADRs/0003-org-config-repo-convention.md) for the config repo convention and [ADR 0024](ADRs/0024-harness-definitions.md) for harness definitions.

**Decided:**

- Agent configuration: upstream defaults (agents, skills, plugins, schemas,
  harness, policies, scripts) are resolved at runtime from
  `fullsend-ai/agents`, or from vendored files when `--vendor` was used at
  install (detected via `.defaults/action.yml` — see
  [ADR 0047](ADRs/0047-vendored-installs-with-vendor-flag.md)).
  Customization uses `base:` harness composition, URL resource
  references, and config-based agent registration
  ([ADR 0045](ADRs/0045-forge-portable-harness-schema.md),
  [ADR 0064](ADRs/0064-deprecate-customized-directory-overlay.md)).

## Multi-org deployment model

In the self-managed per-repository deployment model, each installation
operates independently, with no shared control plane between installations.
If Fullsend offers a centrally managed service, each tenant's repository
configuration will use the existing `repos.yaml` v1 manifest. The source of
truth and how service components consume it, along with the service's runtime
and deployment model, remain open ([ADR 0123](ADRs/0123-tenant-configuration-from-repos-yaml.md)).

```
  Org A                                      Org B
  ┌─────────────────────────────┐            ┌─────────────────────────────┐
  │ repo-a1                     │            │ repo-b1                     │
  │ ├── .fullsend/config.yaml    │            │ ├── .fullsend/config.yaml    │
  │ ├── .fullsend/agents/        │            │ ├── .fullsend/agents/        │
  │ └── .fullsend/skills/        │            │ └── .fullsend/skills/        │
  │                             │            │                             │
  │ repo-a2                     │            │ repo-b2                     │
  │ └── .fullsend/              │            │ └── .fullsend/              │
  └─────────────────────────────┘            └─────────────────────────────┘
                    │                                        │
                    └──────── independent installations ────┘
                                       │
                            ┌──────────┴───────────┐
                            │  fullsend-ai/fullsend│
                            │  CLI, base agents,   │
                            │  skills, scaffold    │
                            │  Releases are pulled │
                            │  independently       │
                            └──────────────────────┘
```

Each self-managed org is a fully independent instance. They choose when to
upgrade. They configure their own agents, skills, plugins, and policies. They
use their own model providers and API keys. The only shared element is the
upstream fullsend project they all pull from.

## Downstream/upstream federation

Independent orgs can optionally collaborate across the forge boundary. A downstream org — a vendor, contributor, or consumer — runs its own fullsend instance for internal work. An agent in that downstream instance can push feature proposals upstream to a project that has its own full SDLC.

```
  ┌─── Upstream Project ───────────────────────────────────────────┐
  │                                                                │
  │       Refinement ──► Prioritization ──► Execution              │
  │      ╱                                           ╲             │
  │  Discovery                                        Verification │
  │      ╲                                           ╱             │
  │       Feedback ◄─────── Monitor ◄──────── Release              │
  │          ▲                                   │                 │
  └──────────│───────────────────────────────────│─────────────────┘
             │                                   └─────────┐
             │      upstreaming agent                      │
             │     proposes enhancement                    │ release
             └────────────────────────────────┐            │
                                              │            │
  ┌─── Downstream Org (vendor/consumer) ──────│────────────│───────┐
  │                                           │            │       │
  │       Refinement ──► Prioritization ──► Execution      │       │
  │      ╱                                                 ▼       │
  │  Discovery                                        Verification │
  │      ╲                                           ╱             │
  │       Feedback ◄──── Monitor ◄──────── Delivery                │
  │                                                                │
  └────────────────────────────────────────────────────────────────┘
```

Both orgs run the full [SDLC loop](vision.md#the-agentic-sdlc). The two cross-org handoff points are:

1. **Downstream Prioritization → Upstreaming agent → Upstream Refinement.** When the downstream org's SDLC prioritizes work that belongs upstream, the handoff at Prioritization → Execution goes to an *upstreaming agent* instead of a coding agent. This agent drafts proposals (issues or PRs) and ferries them into the upstream project's Refinement or Prioritization process via the forge.

2. **Upstream Delivery → Downstream Verification.** When the upstream project delivers a release, the downstream org consumes it. The new release enters the downstream SDLC at Verification — the downstream validates against its own integration tests, compatibility requirements, and deployment constraints.

The forge (GitHub) is the interface between the two orgs. The upstream project doesn't need to know or care that the proposal was generated by an agent in a downstream fullsend instance — it evaluates contributions through its own SDLC the same way it evaluates any human or agent contribution.

This connects to the [downstream/upstream problem doc](problems/downstream-upstream.md), which explores how competing sources of strategic intent get reconciled when multiple downstream contributors propose features into the same upstream project.

## Runtime execution flow

The diagrams below show the runtime path from event to completed agent task. The installer, admin CLI, and enrollment machinery are not shown — only what happens when an agent actually runs.

The architecture is a set of concentric layers, each wrapping the next:

```
Dispatcher → Agent Runner → Sandbox → Agent Runtime → LLM
```

Each outer layer configures and constrains the layer inside it. No inner layer can modify an outer layer. Credentials exist only in the outermost layers and never cross the sandbox boundary inward.

### Abstract model

This diagram is platform-agnostic. It uses a nested-box layout to show the concentric wrapping structure: each layer wraps the one inside it, and control flows inward (setup), then outward (teardown and delivery). No specific SCM, CI system, sandbox runtime, or LLM is named.

```
event ──► DISPATCHER
          Filters event, selects agent role, dispatches run
                │
                ▼
          ╔═══════════════════════════════════════════════════════╗
          ║ AGENT RUNNER                                          ║
          ║                                                       ║
          ║ Loads harness definition for agent role:              ║
          ║   agent prompt, sandbox image, network policy,        ║
          ║   skills, pre/post scripts, validation config,        ║
          ║   output schema, host files, env vars                 ║
          ║                                                       ║
          ║ Runs pre-script on host:                              ║
          ║   validate inputs, prefetch data                      ║
          ║   may request skip, exiting before sandbox creation   ║
          ║                                                       ║
          ║ ┌───────────────────────────────────────────────────┐ ║
          ║ │ SANDBOX (ephemeral, per-run)                      │ ║
          ║ │                                                   │ ║
          ║ │ Created with image + network policy.              │ ║
          ║ │ Bootstrapped with agent def, skills, repo code,   │ ║
          ║ │ env vars, host files, security hooks.             │ ║
          ║ │ No credentials present inside this boundary.      │ ║
          ║ │                                                   │ ║
          ║ │ Pre-agent security scan (context injection).      │ ║
          ║ │                                                   │ ║
          ║ │ ┌───────────────────────────────────────────────┐ │ ║
          ║ │ │ AGENT RUNTIME                                 │ │ ║
          ║ │ │                                               │ │ ║
          ║ │ │ LLM tool-use loop:                            │ │ ║
          ║ │ │   read code, edit files, run tests, iterate   │ │ ║
          ║ │ │                                               │ │ ║
          ║ │ │ Boundaries enforced by enclosing sandbox:     │ │ ║
          ║ │ │   network policy, security hooks,             │ │ ║
          ║ │ │   no credentials, filesystem restrictions     │ │ ║
          ║ │ │                                               │ │ ║
          ║ │ │ Produces: modified repo, output artifacts     │ │ ║
          ║ │ └───────────────────────────────────────────────┘ │ ║
          ║ │                                                   │ ║
          ║ └───────────────────────────────────────────────────┘ ║
          ║                                                       ║
          ║ Extracts from destroyed sandbox:                      ║
          ║   output files, reasoning transcripts, modified repo  ║
          ║                                                       ║
          ║ Post-agent security scan (redact secrets from output) ║
          ║                                                       ║
          ║ Validation loop (if configured):                      ║
          ║   schema check on host                                ║
          ║   ├─ pass: continue                                   ║
          ║   ├─ fail + agent killed at timeout: HARD FAILURE     ║
          ║   │   (no retry; "agent timed out" error, unless an   ║
          ║   │   earlier or this iteration's output validated)   ║
          ║   ├─ fail + retries remain: re-run agent w/ feedback  ║
          ║   └─ fail + retries exhausted: HARD FAILURE           ║
          ║     (no unvalidated output emitted)                   ║
          ║                                                       ║
          ║ Runs post-script on host (outside sandbox):           ║
          ║   push code, create PR, post comments, apply labels   ║
          ║                                                       ║
          ╚═══════════════════════════════════════════════════════╝
                │
                ▼
          Results applied to external system
```

**Key invariants visible in this layout:**

- **Credentials never cross the sandbox boundary.** They exist in the agent runner layer; the sandbox and everything inside it operate without them.
- **Control flows inward (setup) then outward (teardown).** The harness configures the sandbox; the sandbox constrains the runtime. No inner layer can modify an outer layer.
- **Validation gates output.** When configured, no unvalidated output crosses from runner to external system. Exhausted retries are a hard failure, not a fallback.
- **The sandbox is ephemeral.** Created per-run, destroyed after extraction. No state carries between runs.

### MVP embodiment: GitHub + GitHub Actions + OpenShell + Claude Code

The same wrapping structure, with each layer mapped to its concrete technology.

```
GitHub event ──► SHIM WORKFLOW (fullsend.yml in enrolled repo)
                 Evaluates dispatch conditions (event type, labels, /slash commands).
                 Calls workflow_call to .fullsend repo (dispatch.yml).
                       │
                       ▼
                 ╔═══════════════════════════════════════════════════════════════╗
                 ║ DISPATCH WORKFLOW (.fullsend repo, dispatch.yml)              ║
                 ║                                                               ║
                 ║ Mints OIDC token → Cloud Function (token mint) → scoped       ║
                 ║ GitHub App installation token per agent role.                 ║
                 ║ Dispatches per-role agent workflows (code.yml, triage.yml).   ║
                 ╚═══════════════════════════════════════════════════════════════╝
                       │
                       ▼
                 ╔═══════════════════════════════════════════════════════════════╗
                 ║ AGENT WORKFLOW (.fullsend repo, e.g. code.yml)                ║
                 ║                                                               ║
                 ║ Validates source repo is enrolled in config.yaml.             ║
                 ║ Uses scoped GitHub App tokens:                                ║
                 ║   read-only token → enters sandbox (clone, read issues)       ║
                 ║   read-write token → stays on runner (push, create PR)        ║
                 ║ Checks out .fullsend repo + target repo.                      ║
                 ║                                                               ║
                 ║ ┌───────────────────────────────────────────────────────────┐ ║
                 ║ │ FULLSEND CLI (fullsend run code)                          │ ║
                 ║ │                                                           │ ║
                 ║ │ Loads harness/code.yaml:                                  │ ║
                 ║ │   agent: agents/code.md                                   │ ║
                 ║ │   image: ghcr.io/fullsend-ai/fullsend-code:latest         │ ║
                 ║ │   policy: policies/base.yaml                              │ ║
                 ║ │   providers: [vertex-ai, github, package-registries]      │ ║
                 ║ │   skills: [skills/code-implementation]                    │ ║
                 ║ │   pre_script: scripts/pre-code.sh                         │ ║
                 ║ │   post_script: scripts/post-code.sh                       │ ║
                 ║ │                                                           │ ║
                 ║ │ Pre-script: validates ISSUE_NUMBER, REPO_FULL_NAME,       │ ║
                 ║ │ URL consistency.                                          │ ║
                 ║ │                                                           │ ║
                 ║ │ ┌───────────────────────────────────────────────────────┐ │ ║
                 ║ │ │ OPENSHELL SANDBOX                                     │ │ ║
                 ║ │ │                                                       │ │ ║
                 ║ │ │ Created with --from image, --policy base.yaml.        │ │ ║
                 ║ │ │ Bootstrapped via openshell upload/exec:               │ │ ║
                 ║ │ │   agent def    → /sandbox/claude-config/agents/       │ │ ║
                 ║ │ │   skills       → /sandbox/claude-config/skills/       │ │ ║
                 ║ │ │   .env, host files (GCP creds), security hooks        │ │ ║
                 ║ │ │   target repo  → /sandbox/workspace/target-repo/      │ │ ║
                 ║ │ │                                                       │ │ ║
                 ║ │ │ Network policy enforced (L7, per-binary):             │ │ ║
                 ║ │ │   Vertex AI     → claude, node only                   │ │ ║
                 ║ │ │   GitHub API    → gh, git only                        │ │ ║
                 ║ │ │   Pkg registries → npm, pip, go                       │ │ ║
                 ║ │ │                                                       │ │ ║
                 ║ │ │ Pre-agent scan: fullsend scan context                 │ │ ║
                 ║ │ │ (injection detection on CLAUDE.md, AGENTS.md, etc.)   │ │ ║
                 ║ │ │                                                       │ │ ║
                 ║ │ │ ┌───────────────────────────────────────────────────┐ │ │ ║
                 ║ │ │ │ CLAUDE CODE (claude --agent code)                 │ │ │ ║
                 ║ │ │ │                                                   │ │ │ ║
                 ║ │ │ │ Tool-use loop:                                    │ │ │ ║
                 ║ │ │ │   read files, edit code, run tests, iterate       │ │ │ ║
                 ║ │ │ │                                                   │ │ │ ║
                 ║ │ │ │ Model: Opus (via Vertex AI)                       │ │ │ ║
                 ║ │ │ │ Security hooks active: Tirith, SSRF, secret scan  │ │ │ ║
                 ║ │ │ │ No credentials in environment.                    │ │ │ ║
                 ║ │ │ │                                                   │ │ │ ║
                 ║ │ │ │ Produces: modified repo, output artifacts         │ │ │ ║
                 ║ │ │ └───────────────────────────────────────────────────┘ │ │ ║
                 ║ │ │                                                       │ │ ║
                 ║ │ └───────────────────────────────────────────────────────┘ │ ║
                 ║ │                                                           │ ║
                 ║ │ Extracts from destroyed sandbox:                          │ ║
                 ║ │   /sandbox/workspace/output/, JSONL transcripts,          │ ║
                 ║ │   SafeDownload repo (sanitize symlinks, strip hooks)      │ ║
                 ║ │                                                           │ ║
                 ║ │ Post-agent secret scan (redact from extracted output).    │ ║
                 ║ │                                                           │ ║
                 ║ │ Post-script (scripts/post-code.sh, with PUSH_TOKEN):      │ ║
                 ║ │   1. Verify feature branch (not main/master)              │ ║
                 ║ │   2. Protected-path check                                 │ ║
                 ║ │   3. gitleaks secret scan                                 │ ║
                 ║ │   4. pre-commit hooks                                     │ ║
                 ║ │   5. git push --force-with-lease                          │ ║
                 ║ │   6. Create/update PR with ready-for-review label         │ ║
                 ║ │                                                           │ ║
                 ║ └───────────────────────────────────────────────────────────┘ ║
                 ║                                                               ║
                 ║ Upload artifacts (fullsend-code)                              ║
                 ╚═══════════════════════════════════════════════════════════════╝
                       │
                       ▼
                 Branch pushed, PR created with ready-for-review label
```

**Layer mapping (abstract → MVP):**

| Abstract layer | MVP technology | ADR |
|---|---|---|
| Dispatcher | Shim workflow (`fullsend.yml`) in enrolled repo → `workflow_call` to `.fullsend/dispatch.yml` → OIDC mint → per-role agent workflows (thin callers → upstream reusable workflows) | [ADR 0008](ADRs/0008-workflow-dispatch-for-cross-repo-dispatch.md), [ADR 0031](ADRs/0031-reusable-workflows-for-action-installed-distribution.md) |
| Agent runner | GitHub Actions job → `fullsend run` CLI (via `fullsend-ai/fullsend@<version>` composite action) | |
| Harness store | YAML files in `.fullsend/harness/` (e.g. `code.yaml`, `triage.yaml`) | |
| Sandbox | OpenShell with per-agent L7 network policies (endpoint + binary restrictions) | |
| Agent runtime | Claude Code (`claude --agent --dangerously-skip-permissions`); pi (`pi --print --mode json`) and Codex (`codex exec --json`) as opt-in runtimes | [runtimes.md](runtimes.md) |
| Sandbox image | `ghcr.io/fullsend-ai/fullsend-code:latest` (pre-built with tools, runtimes, security scanners) | |
| Credential isolation | Read-only GitHub App token inside sandbox; write token only in post-script | [ADR 0017](ADRs/0017-credential-isolation-for-sandboxed-agents.md) |
| Validation | Host-side schema validation script with retry loop | [ADR 0022](ADRs/0022-harness-level-output-schema-enforcement.md) |
| Post-script | `post-code.sh` (in `fullsend-ai/agents`): protected-path check, gitleaks scan, pre-commit, push, PR creation | |
| Observability | JSONL transcript extraction, security findings, trace ID correlation | [ADR 0021](ADRs/0021-jsonl-reasoning-trace-exposure.md) |

### Two runtimes inside the same sandbox

The OpenShell box above is drawn for Claude Code. With `runtime: pi` the outer layers are identical — same dispatch, same sandbox creation, same policy, same scans, same extraction — and only the innermost box changes. The diagram shows the two side by side; the amber step is pi's integrity check on its hook adapter, which has no Claude Code equivalent because Claude loads hooks from a runner-owned `--settings` file.

```mermaid
flowchart TB
  subgraph SB["OpenShell sandbox (per run — only a short-lived OIDC token + WIF config enter, ADR 0017/0025)"]
    direction LR
    subgraph CL["runtime: claude"]
      direction TB
      C1["/sandbox/claude-config\nagents/ · skills/ · hooks/ · hooks.json"]
      C2["claude -p --agent code\n--settings hooks.json\n--dangerously-skip-permissions"]
      C1 --> C2
    end
    subgraph PL["runtime: pi"]
      direction TB
      P1["/sandbox/pi-config\nAPPEND_SYSTEM.md · settings.json · skills/\nhooks/ · fullsend-hooks.js · fullsend-edit-repair.js · fullsend-manifest.json"]
      P0{"shell guard, before .env:\nadapter present and SHA-256 = embedded copy?\nmanifest present?"}
      P2["pi --print --mode json --no-approve\n--no-extensions [-e anthropic-vertex, on Vertex] -e fullsend-hooks.js\n[-e fullsend-edit-repair.js, with edit] --tools … --model anthropic-vertex/… #lt;/dev/null"]
      PX["exit 97 — never runs unhooked\n(Run refuses earlier, exit -1, if the manifest has no hook plan)"]
      P1 --> P0
      P0 -- yes --> P2
      P0 -- no --> PX
    end
  end
  OUT["extracted: output/ · transcripts/ · debug log\nhost-written: metrics.json (runtime: …)"]
  C2 --> OUT
  P2 --> OUT
  classDef guard fill:#fbf0d6,stroke:#d98e04,color:#1b2230;
  classDef bad fill:#f8e1de,stroke:#c0392b,color:#1b2230;
  classDef opt fill:#e3e9fb,stroke:#2d5be3,color:#1b2230;
  class P0 guard;
  class PX bad;
  class P1,P2 opt;
```

See [runtimes.md](runtimes.md) for the control-by-control security matrix, the config-key mapping and how to select a runtime per repo.

## Repository layout (design workspace vs. web delivery)

The repository combines design documents, Go CLI code, and a small **public web** surface. **Decided:** Browser-oriented static source lives under **`web/`** (the landing page is `web/public/index.html` at `/`, the interactive document graph is `web/public/graph.html` at `/graph.html`, and the custom 404 page is `web/public/404.html`). The user-facing documentation site is built from **`docs/`** by VitePress (`npm run docs:build`) and served under `/docs/`. Cloudflare Wrangler configuration and deploy-time static assets live under **`cloudflare_site/`** (single `wrangler.toml` with `not_found_handling = "404-page"`; CI stages **`_bundle/`** on the deploy runner and copies only **`public/`** and **`worker/`** from the artifact into that tree so **`wrangler.toml` is never taken from the PR-built zip**). See [ADR 0019](ADRs/0019-web-source-and-cloudflare-site-layout.md).

The **admin installation SPA** that formerly lived under `web/admin/` was removed on
2026-08-20, together with the OAuth BFF it required in the site Worker. The site Worker
(`cloudflare_site/worker/`) is now a static-asset passthrough that needs no vars or secrets.
Installation is driven entirely by the `fullsend` CLI (`fullsend github setup`,
`fullsend repos`). This is unrelated to the **public mint** Worker, which is a separate
deployment provisioned from `internal/dispatch/cf/` and served at `mint.fullsend.sh`
([ADR 0068](ADRs/0068-public-community-mint-architecture.md)).
