delegation

package
v0.4.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package delegation implements the github-repository-delegation-v1 control-plane contract described by github/gh-aw-firewall ADR 0001 ("Agent enclave repository admission"). It lets AWF create/confirm and revoke short-lived, invocation-scoped mcpg identities for dynamically admitted agent enclaves without adding, removing, or mutating any configured MCP backend, route, or tool.

Index

Constants

View Source
const EnvControlCapabilityKey = "MCP_GATEWAY_DELEGATION_CONTROL_KEY"

EnvControlCapabilityKey is the environment variable holding the AWF-only delegation-control capability value minted by the compiler at startup. Primary and enclave agents must never receive this value: it is only ever read by the process hosting the controller and compared against inbound requests on the private awf-enclave-mcp-control channel.

View Source
const EnvControlListenAddr = "MCP_GATEWAY_DELEGATION_CONTROL_LISTEN"

EnvControlListenAddr is the private listener address for the AWF control channel. It must not be shared with the executor-facing proxy listener.

View Source
const ToolPolicyGitHubRepositoryReadV1 = "github-repository-read-v1"

ToolPolicyGitHubRepositoryReadV1 is the only delegated tool policy this package understands: a closed allowlist of repository-scoped read-only GitHub tools (list_issues and issue_read).

Variables

This section is empty.

Functions

func DelegatedTools

func DelegatedTools() []string

DelegatedTools returns the exact, closed set of tools permitted by github-repository-read-v1. Each call allocates a fresh slice; callers on a hot path (such as per-call authorization checks) should use IsDelegatedTool instead.

func IsCanonicalOwner added in v0.4.17

func IsCanonicalOwner(selector string) bool

IsCanonicalOwner reports whether selector is already the exact canonical ASCII byte sequence required for a repository owner: ^[a-z0-9](?:[a-z0-9-]{0,38})$. There is no trimming, case folding, Unicode normalization, or URL decoding: callers must reject any selector for which this returns false rather than attempt to normalize it.

func IsCanonicalRepositorySelector

func IsCanonicalRepositorySelector(selector string) bool

IsCanonicalRepositorySelector reports whether selector is already the exact canonical ASCII byte sequence required by the ADR:

^[a-z0-9](?:[a-z0-9-]{0,38})/(?!\.\.?$)(?!.*\.\.)[a-z0-9._-]{1,100}$

There is no trimming, case folding, Unicode normalization, URL decoding, or alternate syntax: callers must reject any selector for which this returns false rather than attempt to normalize it.

func IsDelegatedTool

func IsDelegatedTool(tool string) bool

IsDelegatedTool reports whether tool is a member of the closed github-repository-read-v1 tool set, without allocating.

Types

type AuditEvent

type AuditEvent struct {
	Operation      string // "create_or_confirm", "confirm", "revoke", "revoke_by_labels", "expire", "reconcile"
	RunIDHash      string
	EnclaveEntryID string
	InvocationID   string
	RepositoryHash string
	HandleHash     string
	PolicyGen      uint64
	Outcome        string // "admitted", "confirmed", "denied", "revoked", "mismatch", "expired"
	Reason         string // coarse, non-identifying reason code only
}

AuditEvent is a redacted lifecycle record for one delegation-control operation. Per the ADR, EnclaveEntryID and InvocationID are structural correlation labels the compiler already assigns and are retained in the clear so operators can reconcile a run's dynamic state; they never carry a repository name, credential, or header. Every value that could disclose an identity, a repository selector, or an idempotency key (RunIDHash, RepositoryHash, HandleHash) is hashed before it reaches this struct, so AuditEvent as a whole never carries a bearer value, header, or unredacted private repository name.

type ControlCapability

type ControlCapability struct {
	// contains filtered or unexported fields
}

ControlCapability authenticates inbound control-plane requests against the single AWF-only capability value installed for this run. It intentionally has no relationship to per-invocation Claims verified by internal/enclavegithub: that verifier authenticates enclave executors to the GitHub read proxy, while ControlCapability authenticates only AWF to the delegation controller itself.

func NewControlCapability

func NewControlCapability(secret string) (*ControlCapability, error)

NewControlCapability creates a capability verifier from a non-empty secret value. The secret is never retained in comparable form: only its SHA-256 digest is stored, so a leaked process memory dump of this struct cannot be used to recover or forge the capability value.

func (*ControlCapability) Authenticate

func (c *ControlCapability) Authenticate(authorizationHeader string) error

Authenticate verifies an Authorization header value against the capability. It accepts the header value with a leading Bearer-scheme prefix (stripped before comparison) or as a bare value, per MCP spec 7.1. Comparison is constant-time over fixed-size digests to avoid leaking the capability's length or contents through timing.

type CreateOrConfirmRequest

type CreateOrConfirmRequest struct {
	// RunID must equal the envelope's bound workflow run.
	RunID string `json:"run_id"`
	// EnclaveBackend must equal the envelope's single AWF enclave backend.
	EnclaveBackend string `json:"enclave_backend"`
	// EnclaveEntryID identifies the enclave entry (frontmatter block) this
	// invocation belongs to.
	EnclaveEntryID string `json:"enclave_entry_id"`
	// InvocationID identifies one bounded enclave invocation.
	InvocationID string `json:"invocation_id"`
	// Repository is the canonical, exact-byte owner/repo selector chosen
	// for this invocation. It must already be canonical: the Store performs
	// no trimming, case folding, Unicode normalization, or URL decoding.
	Repository string `json:"repository"`
	// ToolPolicy must equal ToolPolicyGitHubRepositoryReadV1.
	ToolPolicy string `json:"tool_policy"`
	// SchemaHash is the finite response schema hash approved for this
	// invocation; it must be a member of the envelope's allowed set.
	SchemaHash string `json:"schema_hash"`
	// AdmittedDefaultBranchSHA is the default-branch SHA AWF resolved
	// during live-read admission, when known at request time.
	AdmittedDefaultBranchSHA string `json:"admitted_default_branch_sha,omitempty"`
	// RequestedTTL bounds how long the identity should live; it is capped
	// by (and must not exceed) the envelope's MaxIdentityTTL.
	RequestedTTL time.Duration `json:"requested_ttl"`
	// InvocationExpiresAt is the absolute deadline of the invocation, when
	// one is supplied by AWF. An identity can never outlive this deadline.
	InvocationExpiresAt time.Time `json:"invocation_expires_at,omitempty"`
	// IdempotencyKey deduplicates retried create/confirm calls for the same
	// (RunID, EnclaveEntryID, InvocationID, Repository) tuple.
	IdempotencyKey string `json:"idempotency_key"`
}

CreateOrConfirmRequest is an AWF-authenticated request to create or confirm exactly one delegated identity. Every field must be a strict subset of the compiler-installed Envelope; the Store rejects anything wider than the envelope allows.

type Envelope

type Envelope struct {
	// RunID is the workflow run this envelope, and every identity minted
	// from it, is bound to.
	RunID string `json:"run_id"`
	// EnclaveBackend is the single AWF enclave backend identities may be
	// bound to.
	EnclaveBackend string `json:"enclave_backend"`
	// AllowedRepositories is the closed set of canonical owner/repo
	// selectors the compiler admitted for this run. Selectors are compared
	// as exact ASCII byte sequences; no normalization is performed.
	AllowedRepositories []string `json:"allowed_repositories"`
	// AllowedOwners is the closed set of canonical repository owners the
	// compiler admitted for this run's dynamic enclaves. When set, AWF may
	// select any exact repository under an allowed owner at invocation
	// time without the compiler enumerating every repository in advance.
	// A repository is admitted if it is a member of AllowedRepositories or
	// its owner segment is a member of AllowedOwners; one identity remains
	// bound to exactly one repository either way. Owners are compared as
	// exact ASCII byte sequences; no normalization is performed.
	AllowedOwners []string `json:"allowed_owners,omitempty"`
	// ToolPolicy is the single delegated tool policy this envelope allows.
	// Only ToolPolicyGitHubRepositoryReadV1 is currently supported.
	ToolPolicy string `json:"tool_policy"`
	// AllowedSchemaHashes is the closed set of finite response schema
	// hashes the compiler approved for this run. It may be left empty to
	// use MaxDynamicSchemaHashes' bounded runtime admission instead.
	AllowedSchemaHashes []string `json:"allowed_schema_hashes"`
	// MaxDynamicSchemaHashes bounds how many distinct invocation-supplied
	// schema hashes may be admitted at runtime when AllowedSchemaHashes is
	// empty, so a dynamic enclave can be authorized against a bounded
	// finite-schema policy without every hash being enumerated at compile
	// time. It has no effect when AllowedSchemaHashes is non-empty: in
	// that case only the exact compiled hashes are ever admitted.
	MaxDynamicSchemaHashes int `json:"max_dynamic_schema_hashes,omitempty"`
	// MaxIdentityTTL bounds how long any single delegated identity may
	// live, and therefore how long an executor bearer remains valid.
	MaxIdentityTTL time.Duration `json:"max_identity_ttl"`
	// ExpiresAt is the envelope's own absolute expiry, no later than the
	// workflow job lifetime. No identity may be created once the envelope
	// itself has expired.
	ExpiresAt time.Time `json:"expires_at"`
}

Envelope is the compiler-installed, compiler-bounded policy envelope bootstrapped into the controller at gateway startup. Every delegated identity must be a strict subset of this envelope; the controller rejects any request for a server, tool, repository, credential, backend URL, or guard policy outside it. The compiler is not a runtime service: once installed the envelope is immutable for the lifetime of the process.

func (*Envelope) AllowsRepository

func (e *Envelope) AllowsRepository(repo string) bool

AllowsRepository reports whether repo is admitted by this envelope: either as an exact-byte member of AllowedRepositories, or by its owner segment being an exact-byte member of AllowedOwners. repo must already be a canonical selector; a non-canonical selector is never admitted through the owner path even if its literal prefix would otherwise match.

func (*Envelope) AllowsSchemaHash

func (e *Envelope) AllowsSchemaHash(schemaHash string) bool

AllowsSchemaHash reports whether schemaHash is an exact-byte member of the envelope's admitted schema hash set.

func (*Envelope) Validate

func (e *Envelope) Validate() error

Validate checks the envelope's own invariants. It does not check any per-request binding; use Envelope.Admits for that.

type Identity

type Identity struct {
	Handle                   string        `json:"handle"`
	ExecutorBearer           string        `json:"executor_bearer"`
	RunID                    string        `json:"run_id"`
	EnclaveBackend           string        `json:"enclave_backend"`
	EnclaveEntryID           string        `json:"enclave_entry_id"`
	InvocationID             string        `json:"invocation_id"`
	Repository               string        `json:"repository"`
	ToolPolicy               string        `json:"tool_policy"`
	SchemaHash               string        `json:"schema_hash"`
	AdmittedDefaultBranchSHA string        `json:"admitted_default_branch_sha,omitempty"`
	RequestedTTL             time.Duration `json:"requested_ttl,omitempty"`
	ExpiresAt                time.Time     `json:"expires_at"`
	InvocationExpiresAt      time.Time     `json:"invocation_expires_at,omitempty"`
	PolicyGeneration         uint64        `json:"policy_generation"`
	IdempotencyKey           string        `json:"idempotency_key"`
	CreatedAt                time.Time     `json:"created_at"`
	Revoked                  bool          `json:"revoked"`
}

Identity is one invocation-scoped delegated identity bound to a single canonical repository under github-repository-read-v1.

type IdentityResult

type IdentityResult struct {
	Handle                   string    `json:"handle"`
	ExecutorBearer           string    `json:"executor_bearer"`
	Repository               string    `json:"repository"`
	ToolPolicy               string    `json:"tool_policy"`
	Tools                    []string  `json:"tools"`
	AdmittedDefaultBranchSHA string    `json:"admitted_default_branch_sha,omitempty"`
	ExpiresAt                time.Time `json:"expires_at"`
}

IdentityResult is returned to AWF on a successful create or confirm call. It intentionally excludes any field not required by the executor: no credentials, headers, or repository contents are ever included.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store atomically creates/confirms and revokes delegated identities for one compiler-installed Envelope. It is safe for concurrent use: every mutating operation is serialized so that concurrent create/confirm/revoke calls for the same idempotency key are atomic and generation-safe.

func LoadStore

func LoadStore(path string, envelope *Envelope, generation uint64) (*Store, error)

LoadStore reconstructs a Store from a prior SaveState file. If path does not exist, this is a fresh start (no prior state to reconcile) and an empty, fully-reconciled Store is returned. If path exists but is corrupt, truncated, or fails checksum verification, an empty Store is returned with recoveryIncomplete set: callers must fail closed for new dynamic admissions until MarkReconciled is called after an operator confirms outstanding identities are safe to disregard.

Identities that already expired at load time are dropped silently: their absence is ordinary lifecycle behavior, not incomplete reconstruction.

func NewStore

func NewStore(envelope *Envelope, generation uint64) (*Store, error)

NewStore creates an empty Store bound to envelope. generation is the compiler-supplied policy generation for this envelope installation; it is stamped onto every identity minted from this Store so a later envelope replacement can be distinguished from stale identities.

func (*Store) Authorize

func (s *Store) Authorize(executorBearer, runID, enclaveBackend, repository, tool string) error

Authorize enforces that executorBearer is a live identity bound to exactly the given run, enclave backend, repository, and tool. It rejects replayed, expired, revoked, wrong-repository, wrong-tool, and wrong-run identities; none of those can establish or continue a session.

func (*Store) AuthorizeExecutor

func (s *Store) AuthorizeExecutor(executorBearer, repository, tool string) (string, error)

AuthorizeExecutor authorizes an executor bearer against the repository and tool derived by the data-plane route. The run and backend are read from the authenticated identity rather than supplied by the executor. On success it returns the identity's opaque handle so the caller can bind the request to a delegation-specific isolation context (for example a DIFC identity) rather than falling back to the shared proxy identity.

func (*Store) CreateOrConfirm

func (s *Store) CreateOrConfirm(req CreateOrConfirmRequest) (*IdentityResult, error)

CreateOrConfirm atomically creates a new delegated identity, or confirms an existing one for the same invocation scope. A repeated call for the same (run, enclave entry, invocation) tuple returns the exact same binding, expiry, admitted default-branch SHA, opaque handle, and executor bearer, regardless of whether the caller supplies the same idempotency key: one active or terminal identity binding is enforced per invocation even when a caller varies the idempotency key across retries. A request whose binding differs from the stored identity is terminal: the stored identity is revoked and an error is returned.

func (*Store) IsRecoveryIncomplete

func (s *Store) IsRecoveryIncomplete() bool

IsRecoveryIncomplete reports whether restart reconstruction left this Store unable to vouch for prior state. When true the Store holds zero identities and both new dynamic admissions and data-plane authorizations are refused until MarkReconciledAndSaveState clears the flag.

func (*Store) LabelHandles added in v0.4.17

func (s *Store) LabelHandles(runID, enclaveEntryID string) []string

LabelHandles returns the opaque handles of every live identity bound to (runID, enclaveEntryID), sorted for deterministic output, so AWF can enumerate labelled state during reconciliation without any repository, credential, or header ever being disclosed.

func (*Store) MarkReconciledAndSaveState added in v0.4.17

func (s *Store) MarkReconciledAndSaveState(path string) error

MarkReconciledAndSaveState publishes the reconciled state to path and only then opens the in-memory admission gate, so reconciliation is atomic from the caller's perspective.

Ordering matters: the durable write happens first and the in-memory recoveryIncomplete flag is cleared afterwards, while s.persistMu is still held. A persistence failure therefore returns with the gate untouched — it never has to be "restored", and there is no window in which a concurrent CreateOrConfirm can admit a new identity against state that was never persisted. The reverse order (clear, then save, then restore on error) leaves exactly that window open, because the store lock is released while the file is written.

If the process dies between the successful write and the flag flip, the on-disk state already records the reconciliation, so the next LoadStore comes back reconciled. That is the intended outcome: the operator did complete reconciliation.

func (*Store) Revoke

func (s *Store) Revoke(handle string) error

Revoke idempotently revokes the identity bound to handle. Revoking an unknown or already-revoked handle is not an error.

func (*Store) RevokeByLabels

func (s *Store) RevokeByLabels(runID, enclaveEntryID string) int

RevokeByLabels idempotently revokes every identity bound to (runID, enclaveEntryID) and returns how many identities were revoked. It is used both for explicit label-scoped revocation and for shutdown/reconciliation cleanup.

func (*Store) SaveState

func (s *Store) SaveState(path string) error

SaveState persists every currently live identity to path so the controller can reconstruct labelled live delegations after a restart. The file is written with 0600 permissions because it contains executor bearer secrets. A trailing SHA-256 checksum lets LoadStore detect truncation or corruption and fail closed instead of silently reconstructing partial state.

SaveState serializes concurrent callers on s.persistMu so that whichever snapshot is taken later (in lock-acquisition order) is always the one published last: an older, already-superseded snapshot can never overwrite a newer one on disk merely because its write happened to finish first.

func (*Store) Snapshot

func (s *Store) Snapshot() map[string]Identity

Snapshot returns a defensive copy of every currently live (non-expired, non-revoked) identity, keyed by handle. It is used for state persistence ahead of restart recovery.

func (*Store) Status added in v0.4.17

func (s *Store) Status() StoreStatus

Status returns a redacted snapshot of controller health for the status control-plane operation.

type StoreStatus added in v0.4.17

type StoreStatus struct {
	// RecoveryIncomplete reports whether restart reconstruction left this
	// Store unable to vouch for prior state. New dynamic admissions are
	// refused while this is true.
	RecoveryIncomplete bool `json:"recovery_incomplete"`
	// Generation is the compiler-supplied policy generation bound to this
	// Store.
	Generation uint64 `json:"generation"`
	// LiveIdentityCount is the number of currently live (non-expired,
	// non-revoked) identities.
	LiveIdentityCount int `json:"live_identity_count"`
}

StoreStatus is a redacted snapshot of controller health AWF uses to decide whether it may resume delegated admissions. It never discloses a repository, credential, or header.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL