fix(registry): migrate Agent Registry from preview to GA - #60
Merged
Conversation
Agent Registry graduated out of AgentCore into its own AWS service. The
rename is a SILENT break: the deprecated bedrock-agentcore-control model
still exposes the Registry operations with the old descriptorType, so
preview code keeps getting HTTP 200s under an IAM prefix it no longer has.
Migration:
- clients bedrock-agentcore{,-control} -> agent-registry{,-control} (both
sign as agent-registry); IAM prefix agent-registry:*
- descriptorType -> recordType, MCP|A2A|CUSTOM|AGENT_SKILLS ->
MCP|AGENT|CUSTOM|SKILL (preview spellings kept as input aliases)
- descriptors reshaped to *.data/dataSchemaVersion; added mcpServer and
agentSkillsDefinition builders
- SearchRegistryRecords -> SearchDiscoverableRegistryRecords with the GA
structured filter shape
- boto3 >= 1.43.66 hard floor (first release carrying the service models)
Verified against the live GA service, not just the boto3 models: a
throwaway registry, every descriptor builder submitted through the
shipping adapter, and the approval lifecycle end to end. That found four
defects the rename alone would have left in place:
1. Every redeploy silently failed to re-register. name+recordVersion is a
uniqueness key, so the SECOND deploy of an agent raised
ConflictException inside the best-effort auto-register handler — the
governance record stayed frozen at the first deployment's runtime ARN
forever, with nothing surfaced. register() is now an upsert, which
needs the new agent-registry:UpdateRegistryRecord grant on the
status_update step role.
2. available() reported a still-provisioning registry as usable. It
returned True the instant GetRegistry succeeded, but a registry in
CREATING/UPDATING/DELETING rejects CreateRegistryRecord. Enabling
federation on a freshly created registry therefore passed validation
and raced into that conflict on first deploy. Now gated on READY, and
POST /aws-config returns 409 rather than a 400 blaming the registryId.
3. Search could show a stale APPROVED badge. The data plane is a search
index: a record demoted to DRAFT is still served as APPROVED for 20+
minutes. Combined with the upsert (which demotes on redeploy) this is
reachable on the ordinary path. GET /aws-search now reconciles every
hit's status against the control plane and reports
status_authoritative: false — dropping status rather than serving the
index's copy — when it cannot. Gating always read the control plane
and was never affected; an AST-level guard test keeps it that way.
4. Descriptor content contracts were wrong, each an outright live
rejection reported only as an unactionable descriptor-wide error: A2A
card skills require all of id/name/description/tags; mcpServer.data
needs a namespaced <namespace>/<server> name plus description and
version; agentSkillsDefinition must OMIT dataSchemaVersion, unlike
every other descriptor; tools/skills payloads must be objects, never
bare arrays. Under-specified input is normalized, not forwarded.
Also fixed: an unqueryable registry was indistinguishable from a rejected
integration, so an AccessDenied on ListRegistryRecords rendered as a 403
telling the operator their integrations had been rejected. Absent data and
negative data are now distinct (RegistryQueryFailed -> 503); gating stays
fail-closed for a successful query that finds no approval.
IAM is proven rather than asserted: iam:SimulateCustomPolicy against the
synthesized template allows all 9 operations the adapter calls on the API
role, and confirms the step role's 3 denials are correct for its code
path — a test now pins that, because granting the deploy pipeline
approval permissions would let a record approve itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `Python lint & format` job resolved its own ruff version at runtime. ruff-action derives the version from the nearest pyproject.toml, and there is none at the repo root (the Python projects are backend/ and infra/), so it logged "Could not find pyproject.toml. Using latest version." and installed whatever was newest that day. The job's verdict therefore depended on Astral's release schedule rather than on the commit under test. That came due when ruff 0.16 promoted formatting of Python code blocks *inside* markdown from experimental to on-by-default. main went green on 0.15.22, which skipped markdown entirely; every PR opened after 0.16.4 shipped goes red citing docs/ files the author never touched. It also broke the local-CI contract -- `ruff format --check .` passing on a contributor's machine stopped predicting the gate. Fixed at both layers rather than just the symptom: - `version: 0.16.4` on both ruff-action steps, so the gate is reproducible and moves only when someone changes this file - `ruff==0.16.4` in backend/pyproject.toml's dev extras, so a contributor gets the same formatter CI runs. Exact pin on purpose: formatter output changes between minors, so `>=` would let local and CI disagree while both were satisfied. Dependabot already watches /backend, so bumps arrive as a reviewable PR instead of as an overnight red build. - The two flagged doc snippets reformatted (quote style, line wrapping). The changes are cosmetic; no snippet's meaning moved. Verified with ruff 0.16.4 locally: `check .` and `format --check .` both clean across 272 files, and 1286 backend tests still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Agent Registry graduated out of AgentCore into its own AWS service. The rename is a silent break: the deprecated
bedrock-agentcore-controlmodel still exposes the Registry operations with the olddescriptorType, so preview code keeps getting HTTP 200s under an IAM prefix it no longer has. Nothing raisesUnknownOperation; the deploy-path auto-register is best-effort, so anAccessDeniedunder the old prefix surfaces only as a "skipped" log line.Migration
bedrock-agentcore{,-control}→agent-registry{,-control}(both sign asagent-registry); IAM prefixagent-registry:*descriptorType→recordType; enumMCP|A2A|CUSTOM|AGENT_SKILLS→MCP|AGENT|CUSTOM|SKILL(preview spellings kept as input aliases)*.data/dataSchemaVersion; addedmcpServerandagentSkillsDefinitionbuildersSearchRegistryRecords→SearchDiscoverableRegistryRecordswith the GA structured filter shapeboto3 >= 1.43.66hard floor — the first release carrying the service modelsVerified against the live GA service
Not just the boto3 models: a throwaway registry, every descriptor builder submitted through the shipping adapter (loaded via
importlib, deliberately fed under-specified input so the normalizers were exercised rather than bypassed), and the approval lifecycle end to end. 24/24 live checks. That found four defects the rename alone would have left in place.1. Every redeploy silently failed to re-register.
name+recordVersionis a uniqueness key andrecordVersionis"1.0"for everything the platform registers, so the second deployment of an agent raisedConflictExceptioninside the best-effort handler. The governance record stayed frozen at the first deployment's runtime ARN and endpoint — stale forever, nothing surfaced anywhere.register()is now an upsert, which needs the newagent-registry:UpdateRegistryRecordgrant on thestatus_updatestep role. Updating content demotesAPPROVED→DRAFT, so an upsert cannot slip changed content past an old approval.2.
available()reported a still-provisioning registry as usable. It returned True the instantGetRegistrysucceeded, but a registry inCREATING/UPDATING/DELETINGrejectsCreateRegistryRecord. Enabling federation on a freshly created registry — the common sequence — therefore passed validation and raced into that conflict on first deploy. Now gated onREADY, withregistry_status()keeping "not READY" distinct from "could not ask";POST /aws-configreturns 409 instead of a 400 blaming the registryId.3. Search could show a stale
APPROVEDbadge. The data plane is a search index, not the record store: a record demoted toDRAFTkeeps being served asAPPROVEDfor 20+ minutes (measured, stable across polling — not eventual consistency catching up). Combined with the upsert, which demotes on every redeploy, this is reachable on the ordinary path.GET /aws-searchnow reconciles each hit's status against the control plane and reportsstatus_authoritative: false— droppingstatusrather than serving the index's copy — when it cannot. Approval gating always read the control plane and was never affected; a new AST-level guard test keeps it that way.4. Descriptor content contracts were wrong — each an outright rejection by the live schema validator, reported only as an unactionable descriptor-wide error:
a2aAgentCardurl; each skill needs all ofid/name/description/tags(emptytagsok, absent not)mcpServer<namespace>/<server>name (a bare name is rejected), plusdescriptionandversionagentSkillsDefinitiondataSchemaVersionentirely, unlike every other descriptor{"tools": [...]}), never bare arraysUnder-specified input is normalized rather than forwarded.
UpdateRegistryRecordalso takes a different shape from Create — every branch and scalar leaf wrapped in anoptionalValuepatch envelope — which otherwise fails in botocore's client-side validation and never reaches AWS.Also fixed
An unqueryable registry was indistinguishable from a rejected integration: gating swallowed every error into "nothing is approved", so an
AccessDeniedonListRegistryRecordsrendered as a 403 telling the operator their integrations had been rejected. Absent data and negative data are now distinct (RegistryQueryFailed→ 503). Gating stays fail-closed for a successful query that finds no approval.IAM is proven, not asserted
iam:SimulateCustomPolicyagainst the synthesized template: the API role allows all 9 operations the adapter's call sites make; the step role allows 6 and denies 3. Those 3 denials are correct for its code path (auto-register only reads status, creates, looks up, updates) and are now pinned by a test — granting the deploy pipeline approval permissions would let a record approve itself.The dependency floor is verified in the built artifact, not just the pin:
backend/lib/ships verbatim into the Lambda and PYTHONPATH shadows the runtime's boto3, so the bundle was rebuilt and confirmed to resolve boto3 1.43.81 and construct both GA clients (15 control ops, 3 data ops).Gates
Both ruff gates, 1286 backend tests, 47 infra tests +
cdk synth,tsc -b, 253 vitest, eslint (0 errors),npm run build. The same change is applied byte-identically inAgentic-ai-self-service(aws-samples/sample-ai-agent-factory#fix/agent-registry-ga).Not covered: no full
cdk deploy+ UI deploy was run, so residual risk is wiring-level rather than registry-level; the policy simulation and bundle verification are what narrow it to that.🤖 Generated with Claude Code