Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/adr/0001-agent-enclaves.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# ADR 0001: Agent enclave repository admission

## Status

Accepted for implementation planning.

## Context

Unified enclaves expose private repository work to bounded script and agent
executors through the single AWF-owned `awf-enclave` MCP backend. The original
mode uses compiler-enumerated `repos` entries as immutable seed material. Some
workflows need to choose a repository at invocation time, but caller-controlled
repository names must not change any security-sensitive bound.

## Decision

AWF supports two repository admission modes behind the same MCP backend:

- **Static seed-backed mode**: the compiler enumerates `repos` in workflow
frontmatter. AWF stages immutable repository seeds and each invocation selects
exactly one repository from that trusted catalog.
- **Dynamic GitHub-MCP-backed mode**: the compiler supplies a policy envelope
instead of a seed catalog. Each invocation supplies only a canonical
`owner/repo` selector, a bounded prompt or script, and a finite response
schema. AWF asks the compiler-owned GitHub MCP path to admit one matching
repository for that invocation.

The modes are compatible at the subsystem level but mutually exclusive for a
single enclave entry: an entry either declares static `repos` or a dynamic
repository policy, never both. Both modes use the same `enclave_run_script` and
`enclave_run_agent` tool shapes, the same finite output schemas, the same
ledger, and the same admission serialization lane. One enclave invocation
exposes one repository. Cross-repository aggregation happens only when the
primary agent makes multiple bounded enclave calls and combines their finite
results.

## Compiler-to-AWF policy envelope

In dynamic mode the compiler owns every trusted bound and passes AWF a closed
policy envelope. The invocation's repository selector may only choose within
that envelope. It cannot alter:

- repository sensitivity class;
- permitted tools or MCP servers;
- GitHub, model, or API credentials;
- model, runtime, image, profile, or network topology;
- CPU, memory, filesystem, process, timeout, or response-size limits;
- maximum admitted repository count, invocation count, or concurrency; or
- envelope expiry time.

The envelope includes at least:

- allowed owners or exact owner/repository patterns;
- sensitivity classification and disclosure bucket;
- permitted executor types and GitHub MCP tools;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contract should require a versioned, closed allowlist of repository-scopable read-only GitHub tools. Merely requiring tools to be policy-approved could permit write operations or tools whose arguments escape the admitted repository scope. Those operations should fail closed until their repository confinement can be enforced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1f65008: dynamic mode now requires github-repository-read-v1, a closed versioned allowlist of repository-scopable read-only tools, and fails closed for writes, unscoped search, repository discovery, or tools that cannot be mechanically confined.

- maximum admitted repositories for the workflow run;
- per-invocation CPU, memory, process, filesystem, network, timeout, prompt,
script, and response-schema limits;
- total invocation, byte, and time quotas;
- an absolute expiry not later than the workflow job lifetime; and
- audit labels that let AWF reconcile all dynamic state during shutdown.

AWF rejects any envelope field it does not understand and fails closed when the
compiler, mcpg, runtime registry, or executor cannot enforce a requested bound.
Admission, identity delegation, live-read setup, executor startup, revocation,
and cleanup failures do not fall back to static mode, a job-lifetime identity,
or a broader policy.

## Invocation identity and credential flow

The primary agent never receives GitHub credentials, repository seeds, dynamic
policy contents beyond the public tool schema, mcpg identity material, or a
direct transport to the enclave backend. The compiler creates invocation-scoped
mcpg identity delegation for dynamic admissions. Each delegated identity is
bound to:

- one workflow run and one AWF enclave backend;
- one canonical repository selector after policy admission;
- the policy-approved GitHub MCP tools only;
- the policy-approved sensitivity and finite response schema; and
- a short expiry that is no longer than the invocation timeout.

AWF stores the delegated identity in invocation-private state, mounts it
read-only into the single-use executor, and removes it before admitting another
repository. mcpg must reject replayed, expired, revoked, wrong-repository, and
wrong-tool identities. AWF requests revocation at normal completion, timeout,
executor failure, and shutdown; revocation is idempotent and failures are
recorded in audit without exposing the identity.

## Live-read semantics

Static mode prefers immutable staged repository seeds. Dynamic mode has no
frontmatter seed, so it uses live GitHub reads through the delegated MCP
identity. Admission resolves the repository's default branch and records the
admitted default-branch SHA before the executor sees repository contents. The
audit record distinguishes this live-read SHA from an immutable seed and marks
the result as point-in-time, not reproducible from workflow frontmatter alone.

The executor must not silently switch repositories or broaden a GitHub search.
Every GitHub MCP call is scoped to the single admitted repository and, where the
tool supports it, the admitted default-branch SHA or an equivalent immutable
reference. If an immutable read cannot be enforced for a tool, AWF still records
the admitted SHA and treats later data as live data for audit and disclosure
review.

## Non-disclosing errors

Dynamic admission must not become a repository existence oracle. Inaccessible,
nonexistent, expired, over-quota, malformed, and out-of-policy selectors all
return the same canonical admission-denied error to the primary agent. The error
does not include the requested owner, repository, policy reason, credential
state, HTTP status, or timing detail. Sensitive diagnostics are available only
in redacted audit artifacts for trusted operators.

## Quotas, serialization, and idempotency

AWF serializes admissions across static and dynamic invocations through one
lane, debits the shared finite repository ledger before execution, and never
queues unbounded work. Dynamic admission, identity creation, executor startup,
identity revocation, and cleanup are idempotent by `(run, enclave entry,
invocation id, canonical repository)`. A retried invocation receives the same
already-admitted repository and recorded default-branch SHA or the same
canonical denial after the envelope expires or quotas are exhausted.

Audit records include the mode, enclave entry, invocation id, canonical
selector hash, admitted repository hash, admitted default-branch SHA when
available, policy envelope id and expiry, delegated identity id hash, tool set,
quota debits, timing bucket, executor result state, revocation state, and
cleanup state. Records do not disclose private repository names to the primary
agent.

Shutdown closes admissions first, drains or cancels the single execution lane
within the configured grace period, revokes outstanding delegated identities,
reconciles labelled dynamic resources, writes audit records, and then removes
private state. Cleanup failures are fail-closed for future admissions and are
reported as redacted audit failures rather than retried indefinitely.

## Threat analysis

- **Repository-scope escape**: a canonical selector is admitted against the
compiler envelope, bound into one mcpg identity, and enforced on every GitHub
MCP call.
- **Search query scope escape**: dynamic identities allow only policy-approved
tools and repository-scoped queries; unscoped organization or global search is
rejected.
- **Confused deputy**: the primary agent cannot cause AWF, mcpg, or the
compiler to reuse a broader identity because the invocation identity is
created after admission and bound to one selector.
- **SSRF and network escape**: repository names are data, not URLs or network
policy. They cannot change runtime image, proxy, network peers, or egress
allowlists.
- **Identity replay and stale identities**: identities are single-invocation,
short-lived, revocable, and rejected after completion, timeout, shutdown, or
expiry.
- **Races**: the serialized admission lane and idempotency key prevent two
concurrent calls from exceeding quotas or binding one identity to another
repository.
- **Resource exhaustion**: compiler-owned quotas bound repositories,
invocations, bytes, processes, CPU, memory, runtime, schemas, prompt/script
sizes, and cleanup grace.
- **Existence disclosure**: all denial reasons collapse to one canonical error
and timing bucket, with details only in redacted audit.
- **Cleanup failures**: shutdown records unreconciled resources, revokes where
possible, removes private state only after audit, and fails closed on later
admissions until reconciliation succeeds.
- **Admission and setup failures**: policy lookup, identity delegation,
default-branch resolution, runtime-registry lookup, and executor-start errors
all fail closed with the canonical denial or a bounded executor failure; none
retries with broader credentials or a different mode.

## Consequences

Static workflows continue to work without dynamic GitHub access. Dynamic
workflows get late-bound repository selection without giving the caller control
over sensitivity, tools, credentials, runtime, model, image, network, or
resources. Compiler, mcpg, runtime-registry, executor, and integration work can
implement against this contract without changing the public enclave tool
surface.
116 changes: 116 additions & 0 deletions docs/enclaves-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ AWF stages immutable repository seeds on the host, starts one AWF-owned `enclave

The primary agent never receives a broker socket, wrapper binary, direct MCP server URL, capability, repository seed, ledger state, or alternate transport.

Repository admission has two modes, both served by this same MCP backend:

- **Static seed-backed mode** — the existing `repos` lists in enclave entries
form the trusted repository catalog. AWF stages immutable seeds for that
catalog before primary-agent work begins. Each invocation selects one catalog
entry and exposes only that repository to the single-use executor.
- **Dynamic GitHub-MCP-backed mode** — the compiler provides a closed policy
envelope instead of enumerating repository seeds in workflow frontmatter. Each
invocation provides only a canonical `owner/repo` selector, bounded
prompt/script payload, and finite response schema. AWF admits at most one
repository for that invocation through the compiler-owned GitHub MCP path and
records the admitted default-branch SHA.

Static and dynamic modes are compatible in one workflow run but mutually
exclusive within a single enclave entry: an entry declares either static `repos`
or a dynamic repository policy. Caller-controlled repository names are never
interpreted as policy. They cannot alter sensitivity, tools, credentials,
runtime, model, image, network, filesystem, resource limits, timeouts, or
quotas. Cross-repository aggregation occurs only in the primary agent by

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one-repository guarantee does not hold for existing static GitHub-enabled agent enclaves. Their job-lifetime mcpg identity covers the union of configured repositories, so an invocation assigned one seed can still read sibling repositories through GitHub MCP. Please scope this guarantee to dynamic mode or require per-invocation repository-scoped identities for static GitHub access too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1f65008: the one-repository guarantee is now scoped to dynamic admissions and static seed exposure, with an explicit caveat for existing static GitHub-enabled agent enclaves using a union-scoped job-lifetime mcpg identity.

combining multiple bounded enclave results.

## Tool contracts

The AWF-owned MCP server publishes only the enabled enclave tools:
Expand Down Expand Up @@ -182,6 +203,101 @@ can return to the primary agent. Shutdown drains admissions, removes labelled
enclaves, disconnects compiler-owned mcpg from the enclave network, and then
removes private state. AWF never stops or removes the shared gateway container.

## Dynamic repository enclaves

Dynamic repository enclaves extend GitHub access from a static seed catalog to
runtime admission while preserving compiler ownership of security-sensitive
bounds. The compiler-to-AWF envelope is closed and includes allowed owners or
exact repository patterns, sensitivity, permitted executor types and GitHub MCP
tools, maximum admitted repositories, per-invocation CPU/memory/process/
filesystem/network/time/prompt/script/response limits, total quotas, audit
labels, and an absolute expiry no later than the workflow job lifetime. AWF
rejects unknown envelope fields and fails closed if any requested bound cannot
be enforced by AWF, mcpg, the runtime registry, or the executor. Admission,
identity delegation, live-read setup, executor startup, revocation, and cleanup
failures do not fall back to static mode, the existing job-lifetime GitHub
identity, or a broader policy.

Dynamic invocation flow:

1. The primary agent calls `enclave_run_script` or `enclave_run_agent` with one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic mode cannot currently support enclave_run_script: script enclaves are explicitly no-network, and this mode provides no immutable seed. The script would have no path to repository contents. Please either restrict dynamic admission to agent entries or define a secure repository-access topology for dynamic script execution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1f65008: dynamic admission now rejects enclave_run_script and is limited to enclave_run_agent unless a future ADR defines a secure dynamic script repository-access topology.

canonical repository selector, bounded payload, and finite schema.
2. AWF serializes admission through the shared enclave lane, validates the
selector against the compiler envelope, and debits the shared repository
ledger before execution.
3. The compiler-owned GitHub MCP path creates or confirms an invocation-scoped

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not yet define an implementable trust contract. The compiler is not a runtime service, and mcpg currently receives fixed identities and policies at startup. Please identify the component responsible for delegation and specify the authenticated control channel, capability ownership, atomic create/confirm/revoke semantics, restart recovery, and required component version gates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1f65008: the docs now define mcpg’s dynamic-delegation controller, the private authenticated control channel, AWF-owned control capability, atomic create/confirm/revoke semantics, restart recovery, and required version gates.

delegated identity bound to that run, enclave entry, admitted repository,
approved tools, schema, and expiry.
4. AWF records the admitted repository hash and default-branch SHA, stores the
delegated identity only in invocation-private state, and mounts it read-only
into the single-use executor.
5. The executor may access only the admitted repository through the delegated
GitHub MCP identity. If a GitHub MCP tool supports immutable refs, AWF uses
the admitted default-branch SHA; otherwise the audit record marks the data as
a live read at that admitted SHA.
6. On completion, timeout, failure, or shutdown, AWF requests identity
revocation, records the revocation state, removes invocation-private state,
and admits no other repository until cleanup for the serialized lane is
complete.

Unlike static GitHub access, the delegated identity is not a reusable
job-lifetime credential. mcpg must reject replayed, expired, revoked,
wrong-tool, and wrong-repository uses. The primary agent never receives the
identity, the policy envelope, raw repository names from denied selectors,
GitHub credentials, or a direct enclave transport.

Dynamic admission is idempotent by `(run, enclave entry, invocation id,
canonical repository)`: retries receive the same recorded admission and
default-branch SHA or the same canonical denial after expiry or quota
exhaustion. Audit records include mode, enclave entry, invocation id, selector
hash, admitted repository hash, admitted default-branch SHA when available,
policy envelope id and expiry, delegated identity id hash, tool set, quota
debits, timing bucket, executor result, revocation state, and cleanup state.
Private repository names and credentials are not disclosed in primary-agent
outputs.

All inaccessible, nonexistent, out-of-policy, expired, malformed, and over-quota
selectors return the same canonical admission-denied error. That error omits the
requested owner/repository, policy reason, upstream HTTP status, credential
state, and timing detail so dynamic mode does not become an existence oracle.
Trusted operators can inspect only redacted audit diagnostics.

### Dynamic threat model

- **Repository-scope escape**: one invocation exposes one admitted repository;
every GitHub MCP call is constrained by the delegated identity and approved
tool set.
- **Search query scope escape**: organization-wide or global search is rejected
unless represented as repository-scoped reads over separately admitted
invocations.
- **Confused deputy behavior**: AWF never turns a caller-provided name into a
broader credential; the compiler-created identity is bound after policy
admission.
- **SSRF**: repository selectors are canonical data, not URLs, hostnames, proxy
configuration, or egress allowlist entries.
- **Identity replay and stale identities**: identities expire no later than the
invocation timeout, are revoked on every terminal path, and are rejected after
shutdown.
- **Races**: one admission serialization lane covers static and dynamic calls,
preventing concurrent quota bypass or repository/identity mixups.
- **Resource exhaustion**: compiler-owned quotas bound repository count,
invocation count, bytes, CPU, memory, process count, filesystem exposure,
prompt/script sizes, schema size, runtime, and cleanup grace.
- **Existence disclosure**: denial reasons collapse to one canonical error and
timing bucket; detailed reasons are redacted into audit only.
- **Cleanup failures**: shutdown closes admissions first, drains or cancels the
execution lane within the configured grace period, revokes outstanding
identities, reconciles labelled resources, records failures, and fails closed
for later admissions until reconciliation succeeds.
- **Admission and setup failures**: policy lookup, identity delegation,
default-branch resolution, runtime-registry lookup, and executor-start errors
fail closed without retrying under broader credentials or another repository
mode.

See [ADR 0001: Agent enclave repository admission](adr/0001-agent-enclaves.md)
for the stable compiler, mcpg, runtime-registry, executor, and integration
contract.

## Coverage after legacy smoke removal

No unified gh-aw enclave smoke workflow exists yet, so AWF keeps coverage local and unit-focused instead of inventing unsupported workflow syntax. Current owned-scope guidance points to:
Expand Down