Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
178 changes: 178 additions & 0 deletions docs/ADRs/0005-unidirectional-control-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
title: "5. Unidirectional control flow through the execution stack"
status: Proposed
relates_to:
- agent-architecture
- agent-infrastructure
- security-threat-model
topics:
- architecture
- security
- portability
---

# 5. Unidirectional control flow through the execution stack

Date: 2026-03-27

## Status

Proposed

## Context

The [architecture document](../architecture.md) defines five components that
form the execution stack — the vertical path from event to agent action:

1. **Agent Dispatch and Coordination Layer** — translates events into agent tasks
2. **Agent Infrastructure** — compute and orchestration that runs agents
3. **Agent Sandbox** — isolation boundary (network, filesystem)
4. **Agent Harness** — configuration and context layer (skills, prompts, tools)
5. **Agent Runtime** — the LLM in execution

Other components (Policy Store, Intent Source, Identity Provider, Observability,
Agent Registry) exist alongside the stack but are not part of its vertical
control flow.

Today, the architecture document names these components and their
responsibilities but does not state the structural relationship between them.
Without an explicit rule, it is ambiguous whether a lower layer may influence a
higher one — whether an agent runtime can modify its own harness, or a harness
can reconfigure its sandbox.

This matters for four reasons:

**Security.** A compromised agent runtime must not be able to weaken its own
sandbox. A poisoned skill must not be able to expand network access. Each layer
constrains the layers below it; those constraints must be immutable from below.
This directly supports the threat model's top priority (external prompt
injection) by ensuring that an injected instruction cannot escalate the agent's
own capabilities.

**Portability.** Each layer can be swapped independently when control flows in
one direction. Replacing the infrastructure layer (GitHub Actions to Kubernetes)
requires re-implementing only that layer's interface to the layer below it.
Nothing in the sandbox, harness, or runtime changes. This is critical because
we intend to support multiple execution platforms.

**Testability.** Each layer can be tested in isolation by mocking the layer
above it. A harness test does not need real infrastructure; a sandbox test does
not need a real dispatch layer.

**Reasoning.** When debugging or auditing, control flow traces in one direction.
You never have to ask "did the agent change its own sandbox config?" or "did a
skill modify the dispatch layer?" The answer is always no.

## Options

### Option A: Unidirectional control flow (strict top-down)

Control flows strictly downward through the execution stack. No layer may
influence, configure, or depend on layers above it. A layer that needs something
not provided by the layer above must fail or escalate — it cannot
self-provision.

**Trade-offs:**
- Eliminates an entire class of security vulnerabilities (privilege escalation
from within the stack).
- Simple to reason about, audit, and test.
- Agents that need additional capabilities must fail and surface the gap, which
is the correct behavior in a zero-trust system.
- Slightly less flexible: an agent cannot dynamically request additional tools
or network access mid-execution.

### Option B: Bidirectional control flow (allow upward requests)

Lower layers may request changes from higher layers through a controlled
protocol — for example, the agent runtime could request an additional tool from
the harness, or the harness could request expanded network access from the
sandbox.

**Trade-offs:**
- More flexible: agents can adapt to unanticipated needs at runtime.
- Introduces a request/approval protocol between layers, adding complexity.
- Every upward channel is an attack surface. A compromised runtime could use the
request mechanism to escalate its own capabilities.
- Violates zero-trust: the system must evaluate whether to grant requests from a
potentially compromised component.
- Makes reasoning harder: control flow becomes a graph, not a line.

**Why we reject this:** The security risk and complexity outweigh the
flexibility gain. In a zero-trust model, a layer that can request changes to its
own constraints is a layer that can potentially weaken its own constraints. The
correct response to insufficient capabilities is failure and escalation to a
human or a higher-level process — not self-provisioning.

## Decision

Control flows strictly downward through the execution stack:

```
Agent Dispatch → Agent Infrastructure → Agent Sandbox → Agent Harness → Agent Runtime
```

No layer may influence, configure, or depend on layers above it:

- The **agent runtime** cannot modify the harness (its own system prompt,
skills, tool definitions).
- The **agent harness** cannot modify the sandbox (network policy, filesystem
restrictions).
- The **agent sandbox** cannot modify the infrastructure (compute resources,
scheduling).
- The **agent infrastructure** cannot modify the agent dispatch and coordination
layer (what events cause agent invocations).

### Control flow vs. data flow

The unidirectional rule applies to **control flow** — configuration, behavior,
and constraints. It does not prohibit upward **data flow**:

- **Prohibited (upward control flow):** A lower layer modifying the
configuration, behavior, or constraints of a higher layer. The agent runtime
cannot add tools to its own harness. The sandbox cannot expand its own network
policy. The harness cannot reconfigure infrastructure scheduling.
- **Permitted (upward data flow):** Telemetry, logs, traces, and failure signals
flowing from any layer to Observability. Exit codes and error messages
indicating failure. Forge comments explaining what an agent could not do.

This distinction matters because Observability inherently collects data from
every layer in the stack — that is its job. The rule prohibits a layer from
*changing* layers above it, not from *emitting signals* that layers above
observe. A runtime that writes a structured log entry is emitting data upward; a
runtime that modifies its own system prompt is exerting control upward. Only the
latter is prohibited.

Each component interface is a one-way contract: the layer above provides
configuration, the layer below consumes it.

Cross-cutting concerns (Observability, Identity Provider, Policy Store) sit
alongside the stack and feed into layers from the side. They too follow the
unidirectional principle: an agent runtime cannot modify its own policy,
identity, or observability configuration.

## Consequences

- **Each layer boundary is a security boundary.** Compromise of a lower layer
cannot propagate upward. This is the stack's primary security property.
- **Agent runtimes that need capabilities not provided by their harness or
sandbox must fail or escalate to humans.** They cannot self-provision, and
they cannot request the missing capability from the harness — that would be
upward control flow. Instead, the runtime fails and the failure is surfaced
to humans through observability and the forge (e.g., posting a comment
explaining what it could not do and why). The human decides whether to
reconfigure the harness or sandbox for next time. This forces capability
gaps to surface during harness design and testing rather than at runtime
through ad-hoc self-provisioning. (See
[dual-interpretation escalation](../problems/code-review.md#dual-interpretation-escalation)
for the pattern of structured escalation to humans, and
[agent-architecture.md](../problems/agent-architecture.md#how-deadlocks-are-resolved)
for the principle that persistent disagreement escalates to humans.)
- **Swapping any layer requires only re-implementing that layer's interface to
the layer below it.** Moving from GitHub Actions to Kubernetes changes the
infrastructure layer; the sandbox, harness, and runtime are unaffected.
- **Cross-cutting concerns follow the same principle.** The Policy Store feeds
policy into the harness and sandbox from the side, but the runtime cannot
write back to the Policy Store. Observability collects signals from every
layer but no layer can modify its own observability configuration.
- **The architecture document gains an overarching structural principle** that
the individual component descriptions can reference.
163 changes: 163 additions & 0 deletions docs/ADRs/0006-forge-abstraction-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
title: "6. Forge abstraction layer"
status: Proposed
relates_to:
- agent-architecture
- agent-infrastructure
topics:
- portability
- architecture
- tooling
---

# 6. Forge abstraction layer

Date: 2026-03-27

## Status

Proposed

## Context

Fullsend currently targets GitHub-hosted organizations but intends to support
GitLab and eventually Forgejo. Many components interact with forge-specific
APIs: creating issues, opening pull/merge requests, applying labels, posting
status checks, and reading CODEOWNERS. Branch protection configuration varies
significantly across forges and is out of scope for the initial abstraction —
it remains an unsolved portability problem.

These interactions happen in two distinct contexts:

1. **Agent runtime (LLM-driven).** The agent decides to open a PR, comment on
an issue, or check labels. LLM-based agents are naturally good at detecting
which forge they're on and using its native CLI (`gh`, `glab`, etc.).
Forcing them through an abstraction adds friction without clear benefit —
the agent adapts.

2. **Deterministic code paths (scripted).** Two specific places run
deterministic, non-LLM code that must work across forges:
- The **agent runtime wrapper** — the script that runs inside the sandbox,
configures the harness, and launches the agent runtime. It reads issue
metadata, posts status updates, and fetches configuration. This code
must work identically regardless of forge.
- **Skill scripts** — scripts embedded in `scripts/` directories within
skills that agents invoke as tools. These are shipped by fullsend and
must be portable.

The forge abstraction belongs in the deterministic code, not in the agent's
mouth.

## Options

### Option 1: Abstraction everywhere

A CLI tool that all forge interactions go through, including agent-initiated
ones. Agent prompts say `fullsend pr create` instead of `gh pr create`.

**Pros:**
- Uniform interface everywhere. Easy to audit forge interactions.

**Cons:**
- Fights the LLM's natural behavior. Agents are good at using native CLIs.
- Requires teaching every agent a non-standard CLI instead of leveraging
existing training data for `gh`, `glab`, etc.
- The abstraction is only valuable in deterministic code paths where we control
the source. In agent-generated commands, the LLM adapts naturally.

### Option 2: Abstraction in deterministic code only

A shared library/module used by the agent runtime wrapper and skill scripts.
Agents themselves use whatever forge CLI is available.

**Pros:**
- Forge portability where it matters (our code), natural behavior where it
doesn't (agent-generated commands).
- Fewer moving parts — no CLI binary to distribute, just a library used by
code we already ship.
- Agents benefit from their training data on `gh`, `glab`, etc.

**Cons:**
- Agents may use forge-specific features that don't exist on other forges. This
is acceptable — agent prompts can be tuned per-forge if needed, and the
harness can provide forge-appropriate context.

### Option 3: No abstraction — accept GitHub coupling

Use `gh` and GitHub APIs everywhere, including deterministic code.

**Pros:**
- Simplest now.

**Cons:**
- Porting the runtime wrapper and skill scripts to GitLab requires rewriting
every forge interaction in those code paths.

## Decision

Forge-specific interactions are abstracted in the two deterministic code paths
that fullsend controls: the **agent runtime wrapper** and **skill scripts**.
Agents themselves are free to use native forge CLIs.

### Where the abstraction lives

A shared library (working name: `forgekit`) provides functions for the forge
operations that deterministic code needs:

- Issue operations: read metadata, apply labels, post comments
- PR/MR operations: create, update status, post review comments
- Status checks: post pass/fail results
- Code ownership: query CODEOWNERS / equivalent
- Repository metadata: default branch, permissions, clone URLs

The agent runtime wrapper and skill scripts import this library. The library
detects the forge type from the repo's remote URL or from configuration in the
`.fullsend` repo, and dispatches to the appropriate backend.

### What agents do

Agents use whatever forge CLI is available in the sandbox (`gh`, `glab`, etc.).
The harness provides forge-appropriate context so agents know which system
they're on, but agents are not forced through an abstraction layer. LLMs are
naturally effective at using native CLIs based on their training data.

### Key design points

- **Labels are fullsend vocabulary.** Labels used as control signals (e.g.,
"agent-ready", "not-reproducible") are part of the fullsend vocabulary.
The library maps them to the appropriate forge mechanism. Agents may also
apply these labels using native CLIs — the label names are the contract,
not the mechanism.
- **CODEOWNERS parsing is wrapped.** Different forges have different syntax
for code ownership. The library abstracts this behind a uniform query
interface for use by the runtime wrapper and review logic.
- **Skill scripts use the library, not forge CLIs.** Any `scripts/` shipped
with fullsend skills call `forgekit` functions, making skills portable
without rewriting.

## Consequences

- **Deterministic code is forge-portable.** The runtime wrapper and skill
scripts work across GitHub, GitLab, and Forgejo without modification.
- **Agent prompts are forge-aware, not forge-abstracted.** Agent definitions
may include forge-specific context (e.g., "you are working on a GitHub
repo, use `gh` for forge operations"), but this is a harness concern, not
an architectural constraint.
- **New forge backends require implementing the library adapter.** Adding
GitLab or Forgejo support means implementing `forgekit` backends and
ensuring the right forge CLI is available in the sandbox.
- **The library is a fullsend deliverable** that must be versioned and tested,
but it is simpler than a standalone CLI since it only needs to support the
operations used by deterministic code paths.
- **The agent dispatch and coordination layer uses this library.** It runs
deterministic code that interacts with forge APIs for event processing and
work assignment — it goes through `forgekit`, not forge APIs directly.
- **The Agent Identity Provider uses this library for credential issuance.**
`forgekit` is responsible for making agent identity credentials available to
the agent runtime (e.g., generating scoped tokens from a GitHub App or
equivalent). The sandbox is responsible for making those scoped tokens
available to the layers it controls (harness and runtime).
- **Branch protection remains an unsolved portability problem.** Branch
protection rules vary significantly across forges in both semantics and
configuration mechanisms. The initial `forgekit` abstraction does not attempt
to unify branch protection management.
Loading