feat(registry): orchestrator + agent + MCP server E2E (#246)#633
feat(registry): orchestrator + agent + MCP server E2E (#246)#633Kalindi-Dev wants to merge 3 commits into
Conversation
PR 1 of 3 for the central agent asset registry (ADR-018). Purely additive infrastructure — nothing in the orchestrator or agent reads from the registry yet; that follows in PR 2 (orchestrator + agent MCP E2E) and PR 3 (Cedar modules + skills). Substrate: DynamoDB + S3 (ADR-018 fallback option 2). The four fallback conditions already select it at authoring time — AgentCore Registry is in public preview and hard-migrates namespaces on 2026-08-06. The RegistryClient seam keeps the AgentCore path open as a later swap. - docs/design/REGISTRY.md: MVP asset kinds, DDB/S3 schema, API contract, resolution semantics, upstream-registry/federation stance (out of MVP scope). - Grammar: extend _REGISTRY_REF to registry://kind/namespace/name@constraint (snake_case kinds, mandatory semver pin). New contracts/registry-resolution parity corpus proves the Python regex and TS parseRef agree byte-for-byte. - Shared types (canonical status='approved'; 'active' is not a code value), mirrored CDK<->CLI. - registry-resolver.ts: parseRef/resolveRef/resolveAll, fail-closed, semver ranked in code (DDB sorts versions lexicographically). - RegistryAssetsTable (pk/sk + kind-index GSI) and RegistryArtifactsBucket (versioned, TLS-only, SSE) constructs, wired into AgentStack. - Publish/resolve/list/show handlers: immutability 409, descriptor validation, Cognito RegistryPublisher/Approver gating; /registry routes on TaskApi with least-privilege IAM grants. Depends on aws-samples#548 (ADR-018) — the REGISTRY.md link to the ADR resolves once that PR merges to main; docs link-check stays red until then by design.
PR 2 of 3 for the central agent asset registry (ADR-018). Wires the registry
(PR 1) end-to-end for one asset kind: a blueprint pins an MCP server, the
orchestrator resolves it at the create-task boundary, stamps the resolved
{kind,id,version} on the TaskRecord, and the agent merges the resolved server
config into .mcp.json alongside the channel MCP.
- Blueprint: assets.mcpServers prop -> RepoConfig mcp_servers column, with a
synth-time RegistryRefValidation (rejects floating/malformed refs). Grammar
extracted to a dependency-free registry-ref.ts so the construct layer can
validate without pulling aws-sdk into the synth graph.
- Orchestrator: resolveRegistryAssetsForTask resolves the blueprint's pins
before hydration (fail-fast/fail-closed), stamps resolved_assets, threads the
bundle into the agent payload. resolved_assets added to TaskRecord/TaskDetail/
TaskSummary (+ CLI mirror).
- Agent: registry_loader.py applies the resolved bundle — apply_mcp_assets
merges server_config into .mcp.json; cedar/skill loaders stubbed for PR 3.
resolved_assets threaded server.py -> pipeline.py -> TaskConfig.
- CLI: bgagent registry publish/resolve/list/show.
Deploy-hardening fixes found validating against a live stack:
- Create RegistryPublisher/RegistryApprover Cognito groups (were referenced by
the publish handler but never created). Bootstrap action-map + policy +
DEPLOYMENT_ROLES.md updated for cognito-idp group actions.
- CLI publish flag --asset-version (--version collided with commander's global).
- memorySize=256 on the four registry Lambdas (128MB default OOM'd on init).
- mcp_server publish accepts inline descriptor server_config in lieu of an
artifact.
- Orchestrator Lambda gets REGISTRY_ASSETS_TABLE_NAME env + read grant (resolve
runs there during hydration).
Validated live (backgroundagent-pr246): resolved_assets correctly stamped on a
real TaskRecord. Task itself fails on a pre-existing 'claude' Exec-format image
bug, unrelated to the registry. Registry MCP secret-injection (${VAR} from
Secrets Manager) is a tracked follow-up, out of MVP scope per ADR-018.
59fe23d to
49ba04a
Compare
scottschreckengaust
left a comment
There was a problem hiding this comment.
Principal Architect Review — PR #633 (registry orchestrator + agent + MCP E2E, #246, PR 2 of 3)
Review re-pinned to head
df3c0f6(a merge ofmaininto the branch, post-49ba04a); the merge did not touch the reviewed incremental files or my inline anchors.
STACK / MERGE-ORDER (read first)
This PR is stacked on #632 — commit 49ba04a sits directly on #632's 773764f. GitHub reports base=main, so the full diff-vs-merge-base re-includes all of #632. I reviewed the incremental layer (orchestrator wiring + agent registry_loader.py + MCP E2E + blueprint assets.mcpServers + CLI registry commands + Cognito groups) and only sanity-checked integration against the #632 base.
Merge order is mandatory: ADR #548 → #632 (PR 1) → this (#632-dependent) → PR 3. Do NOT merge this before #632; its diff/tests assume #632's constructs (registryAssetsTable, resolver, publish/resolve handlers) are present.
1. Verdict — APPROVE (with nits)
The incremental layer is well-structured, fail-closed, and matches the #246 MVP acceptance criteria (blueprint pins one asset kind → orchestrator resolves at create-task boundary → stamps resolved_assets on the TaskRecord → agent merges resolved MCP server_config into .mcp.json). Grammar parity, IAM, and bootstrap coverage are handled. The one CI failure is inherited and by-design (see §6). Nothing blocking in this layer.
2. Vision alignment
- Bounded blast radius / fail-closed —
resolveRegistryAssetsForTaskresolves BEFORE hydration; aRegistryResolutionErrorpropagates (no partial resolution), emits aregistry_resolution_failedaudit event, and fails the task the same way a guardrail block does (ADR-018 sub-decision 6, VISION fail-closed tenet).resolveAllrefuses reserved kinds that have no loader rather than silently dropping them. Good. - Reviewable outcomes — the
{kind,id,version}audit triples on the TaskRecord +registry_assets_resolved/…failedevents keep 'what asset versions ran' inspectable (issue AC: reproducibility). - Policy-gated escalation — publish/auto-approve is gated on
RegistryPublisher/RegistryApproverCognito groups; the fire-and-forget submit path is unchanged (registry resolution is a no-op returning the empty bundle when a blueprint pins nothing). - Tenet trade-offs — the DDB+S3 substrate choice (vs AgentCore Registry) is documented in ADR-018/REGISTRY.md (#632). No undocumented tenet trade in this layer.
3. Blocking issues
None in the incremental layer.
4. Non-blocking suggestions / nits
BOOTSTRAP_VERSIONnot bumped (advisory).cdk/src/bootstrap/version.tsstays at1.2.0while this PR adds 4cognito-idp:*Groupactions to the application policy and a newAWS::Cognito::UserPoolGroupresource-action-map entry. The review bar (ADR-002) asks for a minor bump when adding permissions (→1.3.0). The generated artifacts (application.json,bootstrap-template.yaml,BOOTSTRAP_HASH) WERE correctly regenerated and the hash/golden-baseline tests pass — so this is a semver-hygiene nit, not a functional gap (the deploy role will work). Recommend bumping so operators can tell the bundle changed. (Applies to the stack: #632 also added DDB/S3-backed constructs, though those reuse resource types already in main's action map.)agent/src/registry_loader.py:52_mcp_server_keydocstring drift (comment-analyzer). The docstring says it 'Prefer[s] the descriptor'stool_prefixstripped of themcp__scaffolding … falling back tonamespace-name', but the implementation only ever computesnamespace-name—tool_prefixis never read. Either wire thetool_prefixpreference or drop that sentence so the comment matches the code.- Non-paginated resolver Query (base #632 — note only, not a #633 blocker).
cdk/src/handlers/shared/registry-resolver.tsresolveRefissues a singleQueryCommandover all versions of a pk with noLastEvaluatedKeyloop. DynamoDB caps a Query page at 1 MB; an asset that accumulates enough version rows to exceed a page could resolve to a lower version orNO_MATCHING_VERSIONsilently. Realistically far off (needs thousands of versions), and the file is base-layer — raise on #632, not here. - MCP key collision surface (minor). Registry keys derive from
namespace-name; the channel MCP uses fixed keys (linear-server, etc.), so a collision is unlikely today, but a publishedacme-linear-servercould shadow a channel entry in.mcp.json. A brief note or aregistry-prefix would make the namespaces provably disjoint.
5. Documentation
docs/design/DEPLOYMENT_ROLES.md(golden baseline) updated with the 4 cognito group actions; the Starlight mirrordocs/src/content/docs/architecture/Deployment-roles.mdis regenerated and in sync (rannode docs/scripts/sync-starlight.mjs→ no drift;git statusclean underdocs/src/content/docs/).REGISTRY.md+Registry.mdmirror are #632 files. No new docs required for this incremental layer beyond the deployment-roles delta, which is present.
6. Tests & CI (incl. bootstrap synth-coverage)
- CI red is INHERITED & by-design. The failing
build (agentcore)job fails at//docs:link-check:design/REGISTRY.md → ../decisions/ADR-018-agent-asset-registry.mdreturns Status 400 because ADR-018 lives in unmerged PR #548 (docs/decisions/ADR-018*does not exist on this branch).REGISTRY.mdis a #632 base-layer file, unchanged in the incremental diff — this is NOT introduced by #633. It self-resolves once #548 merges to main (the PR description acknowledges this). Not a #633 blocker; it IS a merge-ordering gate. - Incremental layer builds & tests clean (verified locally in the worktree):
tsc --noEmit(cdk) clean; CDK registry suites 252 tests pass (blueprint, task-api incl. the two new Cognito-group assertions, orchestrator-registry, registry-descriptor/-resolver/-publish/-list-show, registry-assets-table, registry-artifacts-bucket, agent stack); bootstrap suite 91 tests pass (synth-coverage, golden-baseline, artifact-sync hash, version); CLIregistry.test.ts7 pass; agenttest_registry_loader.py+ grammar-corpus + workflow-validator 64 pass;scripts/check-types-sync.tsOK (CLI ↔ CDK type mirror validated; the 6 new registry envelopes are allowlisted). - Bootstrap synth-coverage: PASS. New
AWS::Cognito::UserPoolGroup→cognito-idp:CreateGroupmapped; Create/Delete/Get/Update group actions added toapplication.ts+ regeneratedapplication.json+bootstrap-template.yaml;BOOTSTRAP_HASHregenerated (artifact-sync test green). Only gap is the version-string bump (§4.1). - Test quality: covers happy path + failure paths — fail-closed propagation + failure-event emission, deprecated-resolution warning, empty-bundle no-op (no DDB write), inline-
server_config-without-artifact accept vs cedar-module-still-requires-artifact reject, malformed/absent.mcp.jsontolerance, missing-server_configskip, unknown-kind + malformed-id CLI rejection, and synth-time rejection of floating/malformed refs. Grammar parity is corpus-enforced across Python and TS.
7. Review agents run
- Manual principal-architect deep review — full incremental diff (orchestrator, registry_loader, registry-ref/-descriptor, blueprint, task-api, gateway, CLI, types).
- Security review — performed directly against the incremental code (the packaged
/security-reviewskill misfired on a stale git context — it capturedmain/#596 instead of this worktree — so I did the pass by hand): traced the.mcp.jsonmerge (no path traversal — fixed write target, JSON-key not a path, operator-publishedapprovedcontent), Cognito authz (extractGroupsfail-closed → 403;auto_approverequires Approver; JWT claims server-verified), IAM least-privilege (group actions on the deploy role only; orchestrator gets read-onlygrantReadData), and fail-closed resolution. No HIGH/MEDIUM findings. - code-reviewer / silent-failure-hunter / type-design-analyzer / comment-analyzer / pr-test-analyzer — folded into the manual pass (findings: nit §4.2 comment drift from comment-analyzer; type-design confirmed sound — payload carries full
ResolvedAssetBundle, TaskRecord stamps lightweightResolvedAssetSummary[], CLI mirror in sync; silent-failure — theresolved_assetsstamp write failure is logged loudly and deliberately non-fatal, resolution failures propagate; test-analyzer — coverage is thorough incl. failure paths). The plugin agents' own subprocess dispatch inherited the same stale-git context as the security skill, so I could not get clean isolated plugin output; I state that here per the mandatory-agents requirement rather than parrot a misfired run.
8. Human heuristics
- Proportionality — PASS. Extracting the grammar into a dependency-free
registry-ref.ts(so the construct layer validates at synth without dragging aws-sdk into the synth graph) is proportional and well-justified; the re-export fromregistry-resolver.tskeeps existing importers working. Cedar/skill loaders are honest logged-only stubs deferred to PR 3, not speculative machinery. - Coherence — PASS. Same concept, same term:
resolved_assetsbundle vs summary shapes are consistent CDK↔CLI↔agent; the.mcp.jsonread-merge-write mirrorschannel_mcp.pyintentionally (documented). Registry work lands in the right packages per the AGENTS.md routing table. - Clarity — mostly PASS. Names communicate intent; error handling surfaces failures (fail-closed + audit events) rather than hiding them. One drift at
_mcp_server_keydocstring (§4.2). - Appropriateness — PASS. Integration verified against real DDB semver semantics (in-code semver ranking because DDB sorts version SKs lexicographically — a real, correctly-handled hazard); tests assert what the code should do (fail-closed, immutability, grammar rejection), not just what it does. Validated live per the PR description (
resolved_assetsstamped on a real TaskRecord).
| def _mcp_server_key(asset: dict[str, Any]) -> str: | ||
| """The ``mcpServers`` key an asset registers under. | ||
|
|
||
| Prefer the descriptor's ``tool_prefix`` stripped of the ``mcp__`` scaffolding |
There was a problem hiding this comment.
Nit (comment-analyzer): this docstring says the key 'Prefer[s] the descriptor's tool_prefix stripped of the mcp__ scaffolding … falling back to namespace-name', but the implementation below only ever computes namespace-name — tool_prefix is never read. Either wire the tool_prefix preference or drop that sentence so the comment matches the code. Non-blocking.
| 'AWS::CloudWatch::Dashboard': ['cloudwatch:PutDashboard'], | ||
| 'AWS::Cognito::UserPool': ['cognito-idp:CreateUserPool'], | ||
| 'AWS::Cognito::UserPoolClient': ['cognito-idp:CreateUserPoolClient'], | ||
| 'AWS::Cognito::UserPoolGroup': ['cognito-idp:CreateGroup'], |
There was a problem hiding this comment.
Bootstrap coverage is otherwise correct here (new AWS::Cognito::UserPoolGroup → cognito-idp:CreateGroup, group actions added to the application policy, artifacts + BOOTSTRAP_HASH regenerated, golden baseline updated — 91 bootstrap tests green). One advisory gap: BOOTSTRAP_VERSION in cdk/src/bootstrap/version.ts stays at 1.2.0 despite added permissions; ADR-002 asks for a minor bump (→1.3.0). Not test-enforced, so non-blocking, but recommended so operators can see the bundle changed.
Area
cdk— infrastructure, handlers, constructsagent— Python runtime / Docker imagecli—bgagentclientRelated
feat/246-registry-cedar-skills).mainalso shows PR 1's commits — review the commit59fe23dfor PR-2-only changes.Changes
Wires the registry (PR 1) end-to-end for one asset kind: a blueprint pins an MCP server, the orchestrator resolves it at the create-task boundary, stamps the resolved
{kind,id,version}on the TaskRecord, and the agent merges the resolved server config into.mcp.json.assets.mcpServersprop →RepoConfig.mcp_serverscolumn, with a synth-timeRegistryRefValidation(rejects floating/malformed refs). Grammar extracted to a dependency-freeregistry-ref.tsso the construct layer validates without pulling aws-sdk into the synth graph.resolveRegistryAssetsForTaskresolves the blueprint's pins before hydration (fail-fast/fail-closed), stampsresolved_assets, threads the bundle into the agent payload.resolved_assetsadded toTaskRecord/TaskDetail/TaskSummary(+ CLI mirror).registry_loader.pymerges the resolved MCPserver_configinto.mcp.jsonalongside the channel MCP; threadedserver.py→pipeline.py→TaskConfig.bgagent registry publish/resolve/list/show.Deploy-hardening fixes found validating against a live stack: create
RegistryPublisher/RegistryApproverCognito groups (bootstrap policy + action-map +DEPLOYMENT_ROLES.mdupdated); CLI--asset-versionflag (--versioncollided with commander's global);memorySize: 256on the registry Lambdas (128 MB OOM'd on init);mcp_serverpublish accepts inlineserver_configin lieu of an artifact; orchestrator Lambda getsREGISTRY_ASSETS_TABLE_NAMEenv + read grant.Validated live:
resolved_assetscorrectly stamped on a real TaskRecord.Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.