Skip to content

Feat/agent releases - #64

Open
gibbsie wants to merge 5 commits into
mainfrom
feat/agent-releases
Open

Feat/agent releases#64
gibbsie wants to merge 5 commits into
mainfrom
feat/agent-releases

Conversation

@gibbsie

@gibbsie gibbsie commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

feat(release): immutable agent release bundles

Introduces the per-agent release unit the platform lacked: agent config, prompts, exec spec, model config, tools, policies, and evaluation evidence previously versioned independently with no bundle binding them together. Shipped in four parts, each with its own tests.

The record. A release is content-addressed — its id IS the hash of the constituents it pins, computed order-independently. Evaluation evidence is required, not optional, so a release cannot exist without the run, suite, and suite version it was judged against.

The cut. Constituents are derived server-side from validated source records rather than accepted from the caller, so a caller cannot submit a snapshot differing from what was checked. A cut is refused unless the registry record and exec spec are approved and the eval run is complete with a matching suite version. Every org boundary fails closed, including refusing when an org cannot be determined at all; the exec spec has no org field of its own and is resolved through its project owner, following existing precedent.

The pointer. A mutable cursor per org, agent, and environment naming the current release, with the version check enforced as a condition expression on the write rather than an application-level comparison, so concurrent promotions cannot silently lose one another. Each move retains what it moved away from, which rollback will read. Moving requires an explicit promote permission.

Dispatch. The supervisor and step runner resolve the pointer to the immutable snapshot, read-only, driven by the existing enforcement hierarchy rather than a parallel mode system: permissive emits telemetry, shadow records a would-block, strict refuses when no release resolves. Grandfathered agents are never blocked. A failed lookup is treated as unknown state and refused in strict mode rather than passed through.

Rollout safety

Default mode is non-blocking. A deployment with no releases and no pointers dispatches exactly as before, performing no lookup when the feature is off. Telemetry is emitted in every mode so the rollout can be measured before strict is flipped.

Immutability is enforced structurally, not by convention

A release cannot be mutated by any principal: a single store module is the only writer and exposes no update or delete, a guard test fails the build if any other file writes to the table, the conditional put makes identical content idempotent, and no IAM policy grants UpdateItem or DeleteItem on the releases table. The pointer table needs update capability and is therefore reached through a separate role; template assertions cover that separation. Review twice caught wiring that would have widened the shared role and undone this.

Verification

pytest 1453 passed; jest 412 suites / 6153 tests; lint 0 warnings under --max-warnings 0; tsc clean; cdk synth clean with zero policy findings.

Follow-ups filed

The grandfathering rule is now duplicated as a Python port because the TypeScript module is unreachable from the Python runtime — parity-tested today, but it wants a cross-language guard since it decides exemption from a security gate. The pointer's lock rejection is proven against a mock rather than stateful emulation.

Not in scope

Quality gating, promotion, canary, rollback, and release diff are later stories; each has a named seam.

Oliver Gibbs added 5 commits August 6, 2026 12:38
Introduce the release bundle as an immutable, content-addressed record: the
release id IS the hash of everything it pins, computed order-independently so
the same constituents always yield the same id. Evaluation evidence is a
required field rather than an optional afterthought, so a release cannot exist
without the run, suite, and suite version it was judged against.

Immutability is enforced at four layers rather than in application code alone.
A single store module is the only writer and exposes no update or delete
function; a guard test fails the build if any other file touches the table or
issues a raw write; the conditional put makes identical content idempotent and
never overwrites; and no principal is granted UpdateItem or DeleteItem, so a
future seeder or migration physically cannot mutate a release. The table is
retained with point-in-time recovery.

Assembly, environment pointers, and dispatch integration follow separately.
…inputs

Add the assembly operation that turns references into an immutable release.
Constituents are derived server-side from the validated source records rather
than taken from the caller, so a caller cannot smuggle a snapshot that differs
from what was checked. A cut is refused unless the registry record and
execution specification are approved and the referenced evaluation run is
complete with a suite version matching the one being pinned.

Every org boundary fails closed. The registry descriptor, execution
specification, evaluation run, and evaluation suite must all resolve to the
caller's org, and a cut is refused when an org cannot be determined at all
rather than proceeding. The execution specification has no org field of its
own, so it is resolved through its project owner, following existing
precedent. The stored org is derived from the caller, never from the
forgeable value in the request.

The release id hashes only the constituents, excluding creation metadata, so
retrying after a partial failure re-derives the same id and stores one row
instead of a duplicate. Storing precedes freezing the evidence suite on
purpose: an over-frozen suite is harmless, whereas a stored release whose
evidence is still mutable would break the guarantee the release exists to
make. A freeze failure is propagated rather than swallowed, and retrying is
safe because both halves are idempotent.
Thread the releases table and its writer role through to the governance stack,
add the resolver function assuming that existing role rather than taking a
fresh grant, and expose the release type, cut input, and cut mutation through
the schema with the data source and resolver to match.

The wiring initially widened the write floor: granting read-write on the
evaluation suites table added update, delete, and batch-write to the shared
writer role, which every assumer of that role would have inherited — quietly
undoing the guarantee that a release cannot be mutated. The function now takes
read access plus a statement scoped to the single update it actually performs
when freezing evidence. Template assertions ship with this change rather than
after it, and cover the absence of update and delete on the releases table.

A policy finding on the registry read grant is suppressed through the existing
central convention used by the sibling resolver functions, so synthesis is
clean rather than merely quiet.
Add the per-environment pointer: the mutable cursor naming the release an
environment currently runs, keyed per org, agent, and environment, with read
queries for one environment and across environments.

Concurrent moves cannot silently lose one another. The version check is a
condition expression on the write itself rather than an application-level
comparison, so a stale writer is rejected by the database. Each move retains
the release it moved away from, which is what a later rollback will read.

Moving the pointer requires an explicit promote permission. Quality gating
arrives in a later story and has a named seam to attach to, but the seam is a
deliberate no-op today — the path is permission-gated rather than ungated in
the meantime.

The pointer needs update capability, which a release must never have. The two
tables are therefore reached through separate roles, and the template
assertions cover that the releases table still carries no update or delete for
any principal.
…elease

Dispatch now resolves the environment pointer to the immutable release
snapshot at the supervisor and step-runner choke points, read-only. Behaviour
follows the existing enforcement hierarchy rather than a parallel mode system:
permissive emits telemetry and proceeds, shadow records a would-block and
proceeds, and strict refuses when no release resolves. Grandfathered agents
are not blocked in strict mode.

A deployment with no releases and no pointers dispatches exactly as before,
with no lookup performed when the feature is switched off, so existing callers
are unaffected.

A failed lookup is treated differently from a clean absence of a release. A
throttle, missing table, or missing package resolves to a lookup failure,
which strict mode always refuses and which grandfathering never excuses —
refusing rather than proceeding on an unknown state. Telemetry is emitted in
every mode so the rollout can be measured before strict is flipped.

The step runner's function asset did not include the governance package, so
the check would have been unreachable there; it is now supplied through the
shared layer. The arbiter roles take read access only, leaving the create-only
floor on releases intact.
from typing import Any

import boto3
from botocore.exceptions import ClientError
# governance_package_unavailable refusal the authority gate already
# has, not a silent release-gate bypass.
resolve_release = _gov_pkg.release_resolution.resolve_release
ReleaseResolution = _gov_pkg.release_resolution.ReleaseResolution
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant