Skip to content

fix(registry): migrate Agent Registry from preview to GA - #60

Merged
omrsamer merged 2 commits into
mainfrom
fix/agent-registry-ga
Aug 27, 2026
Merged

fix(registry): migrate Agent Registry from preview to GA#60
omrsamer merged 2 commits into
mainfrom
fix/agent-registry-ga

Conversation

@omrsamer

Copy link
Copy Markdown
Contributor

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. Nothing raises UnknownOperation; the deploy-path auto-register is best-effort, so an AccessDenied under the old prefix surfaces only as a "skipped" log line.

Migration

  • clients bedrock-agentcore{,-control}agent-registry{,-control} (both sign as agent-registry); IAM prefix agent-registry:*
  • descriptorTyperecordType; enum MCP|A2A|CUSTOM|AGENT_SKILLSMCP|AGENT|CUSTOM|SKILL (preview spellings kept as input aliases)
  • descriptors reshaped to *.data / dataSchemaVersion; added mcpServer and agentSkillsDefinition builders
  • SearchRegistryRecordsSearchDiscoverableRegistryRecords with the GA structured filter shape
  • boto3 >= 1.43.66 hard floor — the 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 (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 + recordVersion is a uniqueness key and recordVersion is "1.0" for everything the platform registers, so the second deployment of an agent raised ConflictException inside 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 new agent-registry:UpdateRegistryRecord grant on the status_update step role. Updating content demotes APPROVEDDRAFT, so an upsert cannot slip changed content past an old approval.

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 — the common sequence — therefore passed validation and raced into that conflict on first deploy. Now gated on READY, with registry_status() keeping "not READY" distinct from "could not ask"; POST /aws-config returns 409 instead of a 400 blaming the registryId.

3. Search could show a stale APPROVED badge. The data plane is a search index, not the record store: a record demoted to DRAFT keeps being served as APPROVED for 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-search now reconciles each hit's status against the control plane and reports status_authoritative: false — dropping status rather 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:

descriptor contract
a2aAgentCard card needs url; each skill needs all of id/name/description/tags (empty tags ok, absent not)
mcpServer server.json with a namespaced <namespace>/<server> name (a bare name is rejected), plus description and version
agentSkillsDefinition must omit dataSchemaVersion entirely, unlike every other descriptor
tools / skills payloads must be objects ({"tools": [...]}), never bare arrays

Under-specified input is normalized rather than forwarded. UpdateRegistryRecord also takes a different shape from Create — every branch and scalar leaf wrapped in an optionalValue patch 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 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, not asserted

iam:SimulateCustomPolicy against 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 in Agentic-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

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>
@omrsamer
omrsamer merged commit 3821d9e into main Aug 27, 2026
8 checks passed
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