Feat/agent releases - #64
Open
gibbsie wants to merge 5 commits into
Open
Conversation
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.