feat(registry): agent asset catalog on AgentCore — provisioning, port/adapter, API, CLI (#246) - #664
Conversation
…-check (aws-samples#246) The ADR cited cdk/src/handlers/shared/registry/ref.ts and agent/src/registry/ref.py as relative-path links, but those files ship in the implementation PRs (aws-samples#664/aws-samples#665), not on this ADR branch or main — so //docs:link-check failed with 2 dead links. Demoted both to inline code spans (with a note that they land with aws-samples#664/aws-samples#665) until the implementation merges. Mirror regenerated via docs sync.
Self-review (principal-architect pass)Ran a Fixed in this branch (commit
Open nits (non-blocking, fast-follow): no-op try/catch in Verified clean: bootstrap synth-coverage (91/91) + golden-baseline, CDK↔CLI types sync, IAM least-privilege + fail-closed auth (401/403/422), docs mirror idempotent. All CI checks green. |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Approve (with minor nits)
A large, careful, well-scoped read-side registry catalog on AgentCore. I verified the four critical review foci empirically (tests run locally, not just read): bootstrap policy coverage is complete and the golden baseline + synth-coverage pass; CDK↔CLI shared types are in sync; the registry:// grammar and resolution semantics match byte-for-byte across TS/Py; auth is Cognito-group-gated and resolution fails closed; and IAM is least-privilege on both the custom-resource role and the handler roles. No blocking issues. The open items are the same nits the author already flagged in the self-review.
Vision alignment
Advances #246 / ADR-018 without eroding tenets. Fire-and-forget is untouched (this is a catalog + API/CLI surface; no live pairing). Blast radius is bounded: publish/approve are gated by RegistryPublisher/RegistryApprover groups, resolution is fail-closed (422 with a specific reason on any unresolved/invalid ref), and IAM is scoped to registry/* + registry/*/record/*. Outcomes stay reviewable — the resolved-asset triple is stamped on TaskRecord/Detail/Summary for audit. The L2-vs-custom-resource trade is documented inline (no AgentCore L2 in preview) with a GA-throwaway note and the RegistryClient seam confining the future swap.
Blocking issues
None.
Non-blocking suggestions / nits
- No-op try/catch in
cdk/src/handlers/shared/registry/agentcore-client.ts:180-183— both catch branches re-throw (if (err instanceof ConflictException) throw err; throw err;), so the whole try/catch is dead. Drop it. (Author already noted this.) - Double
parseConstraintcall incdk/src/handlers/registry-publish.ts:124—!parseConstraint(...) || parseConstraint(...)!.op !== 'exact'parses twice; compute once into a local. Cosmetic. (Author noted.) - O(n) resolve/list —
AgentCoreRegistryClient.listRecordsdoes List + N×Get per call, andresolve/show/getRecordall funnel through it. Acceptable for the MVP given the GA-throwaway construct, but worth a// TODO(GA)marker so it isn't forgotten when the native construct lands. showresponse is an inline anonymous type on both server (registry-show.ts:68) and client (api-client.ts::registryShow) rather than a namedRegistryShowResponse. The two inline shapes match today, but a named type would put it under the types-sync guard like the other envelopes. Coherence nit.- SKILL.md runtime round-trip via a single-quoted frontmatter line (
agentcore-client.ts::parseSkillRuntime↔agentcore_client.py::_SKILL_RUNTIME_RE): a runtime JSON payload containing a'or newline could mis-parse. Shared limitation on both sides (not a parity break), but consider base64-encoding thex-abca-runtimevalue to make it robust.
Documentation
Good. docs/design/REGISTRY.md (new) documents the grammar, storage modes, and group gating; docs/design/DEPLOYMENT_ROLES.md golden baseline updated to match the new bootstrap statements; contracts/registry-resolution/README.md documents the parity corpus. I regenerated the Starlight mirror locally (node docs/scripts/sync-starlight.mjs) and git status docs/src was clean — no drift. bgagent registry is self-documenting via commander help.
Tests & CI
- CDK: ran
registry-resolution-parity,registry-handlers,agentcore-client,registry-resolver,constructs/registry,bootstrap/synth-coverage,bootstrap/golden-baseline,bootstrap/version,bootstrap/policies— 111 passed. Bootstrap synth-coverage explicitly PASSES with the new CFN types (AWS::CloudFormation::Stack,AWS::Cognito::UserPoolGroup,AWS::StepFunctions::StateMachine,Custom::AgentCoreRegistry) all mapped inresource-action-map.tsand covered by the policy bundle.BOOTSTRAP_VERSIONbumped 1.2.0→1.3.0, hash + snapshot regenerated. - Agent:
test_registry_resolution_corpus+test_registry_agentcore_client— 24 passed; full workflow/validator suite — 182 passed (the widened_REGISTRY_REFregex is backward-compatible with the legacy 2-segment corpus). - Types-sync:
scripts/check-types-sync.ts— OK, 68 CLI exports validated. - Failure paths covered: 401 (no auth), 403 (missing group / auto_approve without approver), 422 (bad ref / no matching version), 404 (unknown asset), 409 (version collision). Tests exercise these, not just the happy path.
- GitHub CI: all checks green (build agentcore, CodeQL, secrets/deps scan).
- Test perf: the registry construct test does not re-enable bundling or synth-per-test; parity/resolver tests are pure-function.
Review agents run
The pr-review-toolkit sub-agents and /security-review are not separately dispatchable in this execution context, so I performed the equivalent analysis by hand and state so here:
- code-reviewer (by hand): routing correct per AGENTS.md (agent runtime in
agent/, API/Lambdas + bootstrap incdk/, CLI incli/); L2 preferred where available, custom resource justified for the preview-only AgentCore control plane. - silent-failure-hunter (by hand): error handling surfaces failures — resolve/publish fail closed with specific codes; the custom-resource
onEvent/isCompletehandle ConflictException/ResourceNotFoundException deliberately and re-throw the rest;FAILEDregistry status throws. One dead no-op try/catch (nit #1).drainRecordsbest-effort with isComplete retry is sound. - type-design-analyzer (by hand): new wire types mirrored cdk↔cli and guarded by the sync check; domain types (
RegistryRecord,ResolvedAsset, port) intentionally excluded from the CLI contract.ParseResultdiscriminated union is clean. - comment-analyzer (by hand): comments accurate; the
\Z-vs-$parity rationale inref.pyis correct and load-bearing. - pr-test-analyzer (by hand): happy + failure paths covered on both languages; parity corpus enforces cross-language agreement including the
trailing-newline-rejectedregression case. - /security-review scope (by hand): IAM least-privilege verified (custom-resource role uses
*only for account-level Create/List/WorkloadIdentity with documented rationale + nag suppression; handler roles scoped toregistry/*/registry/*/record/*; publish=read+write, resolve/list/show=read-only); Cognito group gating fail-closed; no secrets; no Cedar engine-pin movement in this PR (cedarpy/cedar-wasm untouched — confirmed).
Human heuristics
- Proportionality — Pass. The port/adapter split is justified by the documented GA substrate swap; not an over-abstraction. File sizes are essential, not accreted.
- Coherence — Pass (minor). Same concepts use the same terms across TS/Py; parity corpus enforces real substance, not copy-paste. Only nit: the
showresponse is an inline type on both sides rather than a named shared envelope (#4). - Clarity — Pass. Names communicate intent; errors surface rather than hide; magic values (poll intervals, group names) are named constants.
- Appropriateness — Pass. Adapter decisions (name-in-
nameOption A, native-vs-CUSTOM, 3-call publish) are grounded in the live AgentCore spike findings, not self-written mocks; tests assert intended behavior (fail-closed resolution, immutability) not just current behavior.
Governance: #246 is approved P0 (backing-issue gate satisfied); feat/246-registry-catalog is the sanctioned branch name.
isadeks
left a comment
There was a problem hiding this comment.
Verdict: Request changes
I re-reviewed head d3e2b056 independently, including the areas already approved. The focused registry baseline is green (112/112 CDK assertions and 39/39 Python tests), but executable probes expose untested contract, security, and CloudFormation failures. The blocking cases and concrete reproductions are inline below; these are separate from the existing no-op catch, double-parse, and O(n) nits.
…ation, cutover) (aws-samples#246) Addresses review feedback from @krokoko, @scottschreckengaust, @isadeks: - Revert premature proposed→accepted (README rule: accepted on impl-PR merge; aws-samples#664/aws-samples#665 still in review). Soften "shipped / proven E2E on a live stack" to "targeted by aws-samples#664/aws-samples#665, exercised on a dev stack during review"; stop citing the parked DDB+S3 PRs (aws-samples#632-aws-samples#634) as current. Add a Status note in Decision. - Add short-vs-long-form kind-vocabulary migration note to sub-decision 1: WORKFLOWS.md short forms (registry://mcp/…) are lenient-only forward-decls; only the long form (mcp_server/ns/name@constraint) resolves. No auto-aliasing. - Add a federation / "registry of registries" Non-goal (answers Scott's Jul-8 question): single operator-curated catalog; external registries are discovery-only; no federation in aws-samples#246. - Promote the 2026-08-06 AgentCore namespace cutover from a cost input to a hard gate: no production dependency until the migration is GA in-region. Mirror regenerated via docs sync (idempotent).
dc33a74 to
4f8da98
Compare
|
Thanks @scottschreckengaust and @isadeks — this round addresses every inline finding. Force-pushed after rebasing onto latest @isadeks — P1 blocking
@scottschreckengaust — nits (all addressed)
Additional hardening from a second internal review pass
@isadeks — since these were the items behind your change request, could you re-review when you have a moment? |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Eight of isadeks' ten blocking findings are genuinely FIXED and I verified each one against the code at 4f8da982 (not against the commit messages), with regression tests locking most of them in. But the two P1-security findings — the ones the house rule says to weigh hardest — are only partially remediated, and while probing the fix for finding (4) I found a new blocking issue: the SKILL.md builder still raw-interpolates a caller-controlled string into frontmatter, which lets a publisher inject an x-abca-runtime line that wins on read and completely bypasses the new publish-time runtime validation this PR added. That is a validation-bypass on the exact contract finding (2) was raised to establish.
I ran the registry suites locally rather than reading them: 63 CDK registry assertions, 34 parity assertions, 109 bootstrap assertions, 42 agent pytest — all green; tsc --noEmit clean; check-types-sync.ts OK (69 CLI exports); Starlight mirror regenerated with zero drift. The engineering quality here is high and the fix commits are real work, not theater. The remaining items are narrow and mechanical.
Vision alignment
Strong on fire-and-forget default (purely additive read/write catalog; nothing in the orchestrator calls it yet) and on documented tenet trades — the L2-vs-custom-resource trade, the Option-A namespace encoding, and the new separate-RestApi trade are each written down with the reason, in registry-api.ts:20-33 and REGISTRY.md §7. Bounded blast radius improved materially this round: the handler roles moved from * to registry/{id} (finding 10), and the nested-stack split is a legitimate answer to the 500-resource cap rather than a workaround. Where it still falls short of the tenet is reviewable outcomes under a hostile publisher: the resolve response redaction is a denylist and the SKILL.md descriptor is injectable, so "the audit says the pin was honored" is not yet a claim the substrate enforces. New blocker 1 is squarely a bounded-blast-radius regression.
Disposition of each prior blocking finding
(1) SubmitRegistryRecordForApproval inside the autoApprove branch — FIXED.
agentcore-client.ts:232-244: the submit call is now unconditional and only UpdateRegistryRecordStatus(APPROVED) is gated on input.autoApprove. The comment at :228-231 states the reason correctly. Regression test present: agentcore-client.test.ts:246 — "publish without autoApprove still submits, landing in PENDING_APPROVAL (not DRAFT)".
(2) runtime validated only by typeof object — FIXED (with a narrow residue, see nit 1).
registry-publish.ts:149-151 adds isPlainObject (rejects arrays), and :160-196 validateRuntime enforces the discriminated contract per kind: transport ∈ {http,sse,stdio}, command required for stdio, url required otherwise, cedar_text non-empty for cedar, prompt_fragment non-empty for skill. I re-ran isadeks' exact probes: runtime: [], runtime: {}, {prompt_fragment:"x"} on an mcp_server, and a CUSTOM array all now return 400, not 201. Regression tests present: registry-handlers.test.ts:168-197, seven cases.
(3) P1-SECURITY credential-bearing runtime returned to every authenticated caller — PARTIALLY FIXED. See new blocking issue 2.
registry-resolve.ts:45-62 adds redactRuntimeForResponse, and I confirmed by probe that headers.Authorization now returns *** with the key retained. That closes isadeks' literal reproduction, and the rationale comment at :30-43 is accurate about the orchestrator path. But the function is a denylist over three field names (headers, command, args) applied to an open Record<string, unknown> that publish does not restrict to known keys — so any other secret-bearing field survives verbatim. Probed at head: runtime: {transport:'http', url:'https://h/mcp?token=SUPERSECRET', api_key:'AKIA-LEAKED', env:{TOKEN:'ghp_leak'}} publishes 201 and resolves 200 with url, api_key, and env.TOKEN all in cleartext to a caller in no registry groups. A token in a URL query string is the single most common MCP auth pattern, so this is not a hypothetical residue.
(4) Skill runtime not encoded as valid YAML — FIXED for the reported case; the fix opened a new hole. See new blocking issue 1.
agentcore-client.ts:117-128 base64-encodes the runtime JSON into x-abca-runtime, and :151-160 / agentcore_client.py:74-81 parse it (both accepting the legacy single-quoted form). I re-ran isadeks' exact case — prompt_fragment: "Don't skip tests" — and js-yaml now parses the generated frontmatter cleanly and the runtime round-trips. TS/Py parity holds. Regression test present: agentcore-client.test.ts:184. The description line on the same frontmatter block was not given the same treatment, which is where the new blocker lives.
(5) Custom-resource create not replay-safe — FIXED.
registry-provisioning/index.ts:80-82 derives a deterministic clientToken via SHA-256 over ${RequestId}:${registryName}, and :93-98 passes it to CreateRegistryCommand. RequestId was added to the modeled event at :49. Regression test present: index.test.ts:96 asserts the token is stable per RequestId and varies across RequestIds — exactly the duplicate-delivery test isadeks asked for.
(6) Update reports success while ignoring desired state — FIXED.
index.ts:104-127 now diffs against OldResourceProperties and issues a real UpdateRegistryCommand with name and/or the description: { optionalValue } wrapper. I confirmed against the installed SDK (models_2.d.ts:684 UpdateRegistryRequest) that both fields are in fact mutable and that UpdatedDescription is a wrapper as the comment at :121 claims. bedrock-agentcore:UpdateRegistry is granted at registry.ts:110, and bootstrap coverage rides on bedrock-agentcore:* in policies/compute-agentcore.ts:34. Regression tests present: index.test.ts:109, :123, :135 (including the no-op case).
(7) Authenticated publisher identity extracted then dropped — FIXED.
registry-publish.ts:76 sets publisher: userId on PublishInput; types.ts:166-168 documents it; the adapter writes it into MCP _meta[dev.abca.publisher] (agentcore-client.ts:498), the SKILL.md x-abca-publisher frontmatter key (:130), and the CUSTOM body (:478), and reads all three back (extractPayload, :446/:457/:469). registry-show.ts:59 now surfaces a real value. I probed the wiring: PublishInput.publisher === 'u1'. Regression test present: agentcore-client.test.ts:224 round-trips the publisher across all three storage shapes.
(8) P2 Number() rounds large semver components / TS↔Py parity — FIXED.
ref.ts:93-95 and resolver.ts:47-49 both reject non-Number.isSafeInteger components; ref.py:46,87 and resolver.py:24,44 use the identical 9007199254740991 bound. Regression tests present in the parity corpus, which is the right place: cases.json:100 component-beyond-max-safe-integer-rejected (isadeks' 9007199254740993.0.0) and resolution-cases.json:71 near-max-safe-integer-patch-comparison. Both corpora run dual-runner and pass on both sides.
(9) Async create failure/timeout treated as success — FIXED, and this is the best-executed fix in the set.
agentcore-client.ts:374-397: *_FAILED throws with the substrate statusReason; ResourceNotFoundException is treated as transient and keeps polling instead of returning; budget exhaustion throws. The new RegistryPublishIncompleteError (types.ts:146-155) carries the recordId so the stranded-record case is actionable, and registry-publish.ts:97-105 maps it to a 502 rather than a bare 500. Regression tests present: agentcore-client.test.ts:262 (CREATE_FAILED), :285 (post-create submit failure), :304 (poll budget exhausted). I saw all three fire in the test log.
(10) P1-SECURITY both resource ARNs use * — FIXED. See nit 3 for a residual.
The grants moved out of task-api.ts into the new nested stack: registry-api.ts:111-137 formats registry/{props.agentRegistryId} and registry/{props.agentRegistryId}/record/* and applies read-only to resolve/list/show, read+write to publish. task-api.ts retains no bedrock-agentcore registry grant (verified by grep). The nag suppression at :189 was rewritten to describe the narrowed scope truthfully. Regression test present: registry-api.test.ts:63, though it asserts only on the action list, not the resource ARNs — see nit 3.
New blocking issues
B1 — agentcore-client.ts:114-136: caller-controlled description is raw-interpolated into SKILL.md frontmatter; an injected x-abca-runtime line wins on read and bypasses all publish-time runtime validation. (P1-security)
buildSkillMd base64-encodes the runtime (the finding-4 fix) but builds the description line by raw String(...) interpolation with no escaping and no newline stripping. discovery is an unrestricted Record<string, unknown> that publish validates only as "is a plain object" (registry-publish.ts:138-140), so a RegistryPublisher can smuggle a newline plus a second frontmatter key. Because parseSkillRuntime (:152) uses a ^…$ multiline regex and String.match returns the first occurrence, the injected line — appearing above the real one — is the one that resolves.
Probed at head, discovery: { description: "benign\nx-abca-runtime: <base64 of {\"prompt_fragment\":\"INJECTED-EXFIL-PROMPT\"}>" } with a legitimate validated runtime: { prompt_fragment: 'THE VALIDATED BENIGN FRAGMENT' } produced:
---
name: acme-tdd
description: benign
x-abca-runtime: <injected> <- attacker's, never validated
version: 1.0.0
x-abca-runtime: <legitimate> <- the validated one, shadowed
---
and publish() returned runtime = {"prompt_fragment":"INJECTED-EXFIL-PROMPT"}. agent/src/registry/agentcore_client.py:74 uses re.search on the same pattern and has identical first-match behavior, so the agent-side loader in PR 2/3 will read the injected payload too. For skill this is direct prompt injection into a resolved asset the audit trail records as the validated pin; the same trick against a duplicated name/version line corrupts the descriptor identity.
This is a strictly worse failure mode than finding (2), because the bypassed validator is the one added to fix (2): the record's stored runtime is no longer the runtime the API validated.
Suggested fix: build the frontmatter with a real YAML serializer (js-yaml's dump — already a cdk dependency and already used in this repo — plus yaml.safe_dump on the Python read side), or at minimum (a) strip \r\n and truncate description before interpolation, (b) parse only the frontmatter block delimited by the first ---/--- pair rather than regexing the whole document, and (c) reject a descriptor carrying more than one x-abca-runtime key instead of silently taking the first. A regression test asserting that a newline-bearing description cannot change the round-tripped runtime would lock it in on both sides.
B2 — registry-resolve.ts:45-62: the secret redaction is a denylist over an open payload, so finding (3) is closed only for headers/command/args. (P1-security)
Detailed above under finding (3). The concrete probe at head: a caller in no registry groups gets 200 with url: "https://h/mcp?token=SUPERSECRET", api_key: "AKIA-LEAKED", and env: {TOKEN: "ghp_leak"} verbatim. Two independent gaps compose to produce it — publish accepts unknown keys on runtime (validateRuntime checks required fields but never rejects extras), and resolve redacts only three known names. Fail-closed says the response should be an allowlist: project the per-kind known-safe fields (transport, tool_prefix, version, header keys, and a host-only form of url) and drop everything else, rather than enumerating what to hide. Complementarily, having validateRuntime reject unknown keys per kind would make the payload closed at the gateway and is a small change to code you already touched. The mcp_server row in REGISTRY.md §2 advertising headers as part of the runtime payload should then note that the human-facing resolve response is redacted while the orchestrator path is not.
B3 — registry-provisioning/index.ts:65 and agentcore-client.ts:176: new BedrockAgentCoreControlClient({}) bypasses the attributed client factory, silently dropping solution UA attribution (#319).
Both are naked constructions. makeClient from cdk/src/handlers/shared/ua.ts:113 exists on this PR's base (03228246) and is used by ~20 other handlers (get-trace-url.ts:33, reconcile-concurrency.ts:24, …); these two are the only naked SDK clients left in cdk/src/handlers/. Per AGENTS.md this drops the md/uksb-wt64nei4u6#{component} segment from every registry control-plane call, which is exactly the omission #345 was merged to prevent — and it is invisible because nothing fails. Note the intent is clearly there: registry-api.ts:86 already sets ABCA_COMPONENT: 'registry-api', so the component label is wired and only the factory call is missing. Fix is one line each: makeClient(BedrockAgentCoreControlClient), and in the adapter opts.client ?? makeClient(BedrockAgentCoreControlClient) (the injection seam for tests is unaffected). Worth adding ABCA_COMPONENT to the provisioning Lambdas in registry.ts:70-76 too, since they currently fall through to the api default.
Non-blocking nits
registry-publish.ts:141/:174—customandauto_approveare typedboolean?but never type-checked, socustom: "false"reaches the adapter as the truthy string"false"and silently flips storage to CUSTOM (probed: 201,PublishInput.custom === "false"). This is the last live fragment of isadeks' "require booleans for the flags". Not blocking because the security-relevant direction fails closed — I verifiedauto_approve: "false"returns 403, not an approval bypass. Atypeof !== 'boolean'guard alongside the others invalidatecloses it.registry-publish.ts:134-136— the doubleparseConstraintcall from the first review round is now fixed (singleconst constraint). Noting it as resolved.registry-api.test.ts:63— the test that guards the finding-10 fix asserts only the action list, not the resource ARNs, so a regression back toresources: ['*']would keep this test green. Add aResources: Match.arrayWith([Match.objectLike({ 'Fn::Join': Match.arrayWith([Match.arrayWith(['reg-abc123'])]) })])style assertion, or simply assert the rendered policy JSON contains no bare"*"resource. This is the highest-value test to add given #10 was a security finding.agentcore-client.ts:280-283— theTODO(GA)marker for the O(n) List+N×Get was added as requested. Good.agentcore-client.ts:151-160/agentcore_client.py:78-81— the legacy single-quoted-JSON fallback is dead on arrival: no records exist yet (this PR provisions the registry for the first time), so it is permanent complexity guarding an empty set, plus it is the branch that makes the parser lenient. Consider dropping it; if kept,agentcore-client.ts:157is uncovered (visible in the coverage report).registry.ts:105—resources: ['*']on the create policy remains, with a documented "AccessDenied when scoped, observed on deploy" rationale and a matching nag suppression. Accepted as-is:CreateRegistrygenuinely has no resource to scope to. Flagging only so it stays visible at GA when the native construct lands.
Documentation status
Good. docs/design/REGISTRY.md §7 gained an accurate "Why a separate API" box explaining the 500-resource cap and the registry_api_url config cost, and I verified the CLI side actually implements that claim (stack-outputs.ts:90,100; configure.ts:38,71,78). §6 now matches the shipped always-submit behavior. I regenerated the Starlight mirror (node docs/scripts/sync-starlight.mjs) and git status docs/ came back empty — no drift, so CI's "Fail build on mutation" step will pass. One doc gap tied to B2: §2's mcp_server row and §7.2's resolve contract should state that the human-facing response is redacted.
Tests and CI
Run locally at 4f8da982, not read:
- CDK registry:
registry-handlers+agentcore-client+registry-provisioning+registry-resolver— 63 passed. - Parity:
registry-resolution-parity+registry-resolution-ranking-parity— 34 passed. - Constructs:
registry+registry-api— green;registry-api.test.tssynths once per helper call and does not re-enableaws:cdk:bundling-stacks, so #366 is respected. - Bootstrap: all six suites — 109 passed, including
synth-coverageandgolden-baseline. - Agent:
test_registry_agentcore_client+ both corpora — 42 passed. tsc --noEmitoncdk/— clean.scripts/check-types-sync.ts— OK, 69 CLI exports validated (RegistryShowResponseis now a named shared type, so it rides the guard; that was a first-round nit and it is resolved).- GitHub checks: all 8 green.
Bootstrap synth-coverage: verified, not taken on trust. BOOTSTRAP_VERSION 1.4.0 → 1.5.0 (version.ts:37) with the bump history extended; BOOTSTRAP_VERSION / BOOTSTRAP_HASH / bootstrap-template.yaml / the version snapshot are all regenerated and committed; artifact-sync.test.ts passes, which is what would catch an uncommitted regeneration. The new CFN types are mapped in resource-action-map.ts (AWS::CloudFormation::Stack, AWS::Cognito::UserPoolGroup, AWS::StepFunctions::StateMachine, Custom::AgentCoreRegistry). On the ARN-pattern gap that usually bites here: infrastructure.ts:57 correctly widens to stack/backgroundagent-dev-* for the nested stacks — necessary because both new nested stacks synth as backgroundagent-dev-<Child><hash> — and application.ts adds the Step Functions statement scoped to stateMachine:backgroundagent-dev-* plus states.amazonaws.com in the iam:PassRole service allowlist. I checked the nested-stack move specifically, since it shifts logical IDs, and synth-coverage passes at head. DEPLOYMENT_ROLES.md golden baseline is in sync.
Test-coverage gaps worth noting: nothing exercises the B1 frontmatter-injection path or the B2 non-denylisted-field path — both would be single tests. Jest reports the registry slice below the global branch/function thresholds when run in isolation, which is expected for a subset run and not a finding.
Review agents run
The pr-review-toolkit sub-agents are not dispatchable from this execution context (no Agent tool; ToolSearch for it returns nothing), so I applied each rubric explicitly by hand and label the results rubric-applied, not agent-run:
- code-reviewer (rubric-applied) — Routing is correct per AGENTS.md. The nested-stack extraction is the right call and honestly documented. Found the #319 attribution break (B3), which is a repo-specific guideline violation rather than a logic bug.
- silent-failure-hunter (rubric-applied) — This is where the PR improved most: finding 9's fix converts three silent successes into loud typed failures, and
RegistryPublishIncompleteErrorturns a swallowed stranded-record into an actionable 502. Remaining silent failures: B1 (injected runtime silently shadows the validated one — no error, wrong data), and nit 1 (a string"false"silently changes storage mode).drainRecordsConflictException swallowing is correct-by-design with the isComplete retry. - type-design-analyzer (rubric-applied) —
RegistryPublishIncompleteErrorand theParseResultdiscriminated union are well designed. The weakness is thatruntimeanddiscoveryare both openRecord<string, unknown>on the wire whileRuntimePayloadis a closed discriminated union internally, and theas unknown as RuntimePayloadcast atregistry-publish.ts:75is where that gap is laundered — which is the type-level root cause of both B1 and B2. Narrowing the wire types per kind would collapse both findings. - comment-analyzer (rubric-applied) — Comments are unusually accurate and I spot-checked the load-bearing ones against behavior.
:117-121(base64 rationale),:366-372(prior-behavior description),:89-92(registry.tsAccessDenied rationale), and:121(optionalValueclears the description — confirmed against the SDK types) all hold. One comment is now overstated:registry-resolve.ts:30-43reads as though the secret-leak class is closed, but it describes a denylist over exactly two field families; it should say what is not redacted. - pr-test-analyzer (rubric-applied) — Eight of ten prior findings have a targeted regression test, which is a strong ratio. Gaps: nothing locks the finding-10 resource ARNs (nit 3), and nothing covers B1/B2.
- security-review skill scope (rubric-applied) — IAM: handler roles now least-privilege on
registry/{id}(+ record wildcard, justified); provisioning*accepted with a deploy-observed rationale;iam:PassRoleservice allowlist addition is scoped. Auth: Cognito group gating verified fail-closed by probe (403 on non-booleanauto_approve). Input gateway: two bypasses found (B1 descriptor injection, B2 open runtime keys). No Cedar engine-pin movement in this PR (cedar-wasm/cedarpyuntouched — confirmed). No secrets committed. Attribution regression B3.
Human heuristics
- Proportionality — Pass. The port/adapter split is earned by the documented GA substrate swap, and the nested-stack split is a response to a hard CloudFormation limit, not speculative structure. The fix commits are proportionate to what was asked; none of them over-reach.
- Coherence — Concern.
agentcore-client.ts:122vs:114is incoherent within a single function: the runtime is carefully encoded to be injection-proof while thedescriptionon the adjacent line is raw-interpolated. Same class of bug, same frontmatter block, two different standards — which is precisely how B1 survived a fix aimed at it. Likewiseregistry-publish.ts:160validates the runtime's required fields rigorously but leaves the payload open, soregistry-resolve.ts:45has to compensate with a denylist; one closed contract would remove the need for both. - Clarity — Pass. Naming, named constants over magic values, and typed errors that carry recovery information (
recordId) are all good. The comment overstatement atregistry-resolve.ts:30-43is the one place clarity works against the reader. - Appropriateness — Pass. Adapter decisions remain grounded in the live spike findings rather than self-written mocks, tests assert intended behavior (fail-closed resolution, immutability, PENDING_APPROVAL landing) rather than merely current behavior, and the new tests added this round test the fix, not the implementation.
Governance is clear (#246 approved + P0) and I am not raising the branch name. To be explicit about what I did and did not inherit: I did not adopt ayushtr-aws' approval, and I did not adopt isadeks' change request either — eight of their ten findings are genuinely fixed and I say so with evidence. The request for changes rests on B1 (validation bypass via descriptor injection, both languages), B2 (denylist redaction leaves cleartext secrets on an ungated endpoint), and B3 (attribution regression). B3 is two lines; B1 and B2 are contained within buildSkillMd/parseSkillRuntime and redactRuntimeForResponse plus a tightening of validateRuntime. Happy to re-review quickly on the next push.
4f8da98 to
c8e4790
Compare
…-check (aws-samples#246) The ADR cited cdk/src/handlers/shared/registry/ref.ts and agent/src/registry/ref.py as relative-path links, but those files ship in the implementation PRs (aws-samples#664/aws-samples#665), not on this ADR branch or main — so //docs:link-check failed with 2 dead links. Demoted both to inline code spans (with a note that they land with aws-samples#664/aws-samples#665) until the implementation merges. Mirror regenerated via docs sync.
…ation, cutover) (aws-samples#246) Addresses review feedback from @krokoko, @scottschreckengaust, @isadeks: - Revert premature proposed→accepted (README rule: accepted on impl-PR merge; aws-samples#664/aws-samples#665 still in review). Soften "shipped / proven E2E on a live stack" to "targeted by aws-samples#664/aws-samples#665, exercised on a dev stack during review"; stop citing the parked DDB+S3 PRs (aws-samples#632-aws-samples#634) as current. Add a Status note in Decision. - Add short-vs-long-form kind-vocabulary migration note to sub-decision 1: WORKFLOWS.md short forms (registry://mcp/…) are lenient-only forward-decls; only the long form (mcp_server/ns/name@constraint) resolves. No auto-aliasing. - Add a federation / "registry of registries" Non-goal (answers Scott's Jul-8 question): single operator-curated catalog; external registries are discovery-only; no federation in aws-samples#246. - Promote the 2026-08-06 AgentCore namespace cutover from a cost input to a hard gate: no production dependency until the migration is GA in-region. Mirror regenerated via docs sync (idempotent).
|
Thanks @scottschreckengaust — round-2 findings addressed in |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Request changes — all three of my prior blocking findings are genuinely FIXED and locked by tests; one new blocker: this PR silently blinded the bootstrap deploy-IAM guard by moving resources into nested stacks.
Credit where it is due: this was a real remediation pass, not a comment-answering pass. I re-probed every one of my 2026-08-11 findings against this head SHA (c8e4790c) and each fix removes the capability, not just the symptom. Details and my reproduction attempts are in the table below.
The one blocker is a regression this PR introduced in cdk/test/bootstrap/synth-coverage.test.ts coverage — not in the shipped policies (those are correct today). It is ~10 lines to fix.
Vision alignment
Advances the platform. The registry replaces "vendor an MCP server into the image + CDK deploy" with a versioned, auditable, pinned catalog — that is squarely reviewable outcomes and bounded blast radius (immutability per version, fail-closed resolution, resolved_assets audit triple). The fail-closed choices are the right ones and are now consistent across both languages:
agentcore-client.ts:355-364andagentcore_client.py:166-172both refuse to resolve a record with an empty/unreadable runtime rather than handing back{}— that is the tenet-correct call (a task must never run with a substituted asset while the audit claims the pin was honored).waitPastCreating(agentcore-client.ts:404-423) now throws on*_FAILEDand on poll-budget exhaustion instead of reporting 201 for a record the substrate rejected.- Resolve/list/show being open to any authenticated caller is documented (
docs/design/REGISTRY.md:154) and is now safe because the response is an allowlist projection (see B2 below). Good.
The nested-stack split is a legitimate response to a hard CloudFormation constraint, and the rationale is written down honestly in three places (registry-api.ts:19-33, registry.ts:200-208, REGISTRY.md:90) including the cost (a second invoke URL). No undocumented tenet trade.
Status of prior blocking findings
| # | Finding (2026-08-11) | Status | Evidence at this head |
|---|---|---|---|
| B1 | Frontmatter injection — caller-controlled discovery.description raw-interpolated above x-abca-runtime; first-match parsers on both sides let an injected key shadow the validated runtime |
FIXED | Fix is structural, which is what I asked for. buildSkillMd (cdk/src/handlers/shared/registry/agentcore-client.ts:104-121) now emits via yaml.dump, and parseSkillFrontmatter (:127-139) yaml.loads the whole block instead of a per-line regex. Python mirrors it (agent/src/registry/agentcore_client.py:76-92, yaml.safe_load over _SKILL_FRONTMATTER_RE). I re-ran my original payload plus five escalations — plain \n, a \n---\nx-abca-runtime: …\n---\n close-and-reopen, leading ---, \r\n, body-block injection, and a description that is the key — and all six fail: yaml.dump block-scalars or double-quotes the value, so the injected text stays inside description and the round-tripped x-abca-runtime is always the validated one. Verified identically through PyYAML on the exact bytes js-yaml emits, so TS↔Py parity holds. Regression tests: cdk/test/handlers/shared/agentcore-client.test.ts:207-226 and agent/tests/test_registry_agentcore_client.py:107-129 (both assert the round-tripped runtime, not just the serialized string). |
| B2 | Denylist redaction — open runtime + ~3 masked names meant url?token=…, api_key, env.TOKEN returned cleartext to a caller in no registry groups |
FIXED | Closed at both boundaries, as requested. Read path redactRuntimeForResponse (cdk/src/handlers/registry-resolve.ts:44-80) is now a per-kind allowlist that constructs a fresh object — url is reduced to new URL(...).origin (I confirmed this strips both ?token= and user:tok@ userinfo), header values masked with keys retained, and api_key/env/command/args/anything unnamed are structurally unreachable because they are never copied. Publish path validateRuntime (registry-publish.ts:172-186) rejects unknown keys per kind, so the payload is also closed at the gateway. My exact 2026-08-11 probe payload now yields a 400 at publish and, for a pre-existing record, a response with none of the three secrets. Tests: registry-handlers.test.ts:259 (url-token + header masking), :286 (fail-closed drop of unknown/secret keys), :197 (publish rejects api_key). The comment at :30-43 was also rewritten to describe the allowlist truthfully rather than implying the whole leak class was closed — thank you. |
| B3 | UA attribution (#319) — naked new BedrockAgentCoreControlClient({}) in the provisioning handler and the adapter |
FIXED | Both sites route through the attributed factory: cdk/src/handlers/registry-provisioning/index.ts:68 (makeClient(BedrockAgentCoreControlClient)) and shared/registry/agentcore-client.ts:202 (opts.client ?? makeClient(...), so the test seam is intact). ABCA_COMPONENT: 'registry-provisioning' is set on the provisioning Lambdas (registry.ts:76-78) and 'registry-api' on the API Lambdas (registry-api.ts:86). I swept cdk/src/handlers/, agent/src/registry/, and cli/src/ for new XxxClient(/boto3.client(/boto3.resource( — zero naked clients remain in this diff's scope. |
Also re-verified fixed (from the 07-27/07-28 rounds, since the branch was force-pushed): SubmitRegistryRecordForApproval now runs for every publish with only the APPROVED transition gated on autoApprove (agentcore-client.ts:255-269); publisher identity is threaded and stamped for all three descriptor types (registry-publish.ts:75, customBody :500, _meta :523, frontmatter :117); clientToken derived from the CFN RequestId (registry-provisioning/index.ts:81, deterministic-per-RequestId test at index.test.ts:96); the Update branch issues a real UpdateRegistryCommand (:99-116); non-boolean custom/auto_approve now 400 (registry-publish.ts:145-150, test :205); IAM scoped to registry/{id} + record/* with a resource-asserting guard test (registry-api.test.ts:81-101 — it now walks every statement and fails a bare "*", which is the robust form I asked for). Registry parity corpora run both languages (43 Python cases pass locally: uv run pytest tests/test_registry_* → 43 passed).
Nothing I previously flagged survives. The blocker below is new and was introduced by the nested-stack commit (607279e4).
Blocking issues
B4 — [NEW] The bootstrap deploy-IAM guard is now blind to everything in the two new nested stacks
cdk/test/bootstrap/synth-coverage.test.ts:36-72 builds the root AgentStack and asserts over Template.fromStack(root). Before this PR there were no NestedStacks under cdk/src/ (git grep -l NestedStack main -- cdk/src/ → empty). This PR adds two (AgentRegistryStack, RegistryApi), and a nested stack's resources do not appear in the parent template — the parent only gets an AWS::CloudFormation::Stack. I confirmed this directly:
ROOT template resource types: ["AWS::CloudFormation::Stack"]
NESTED template resource types: ["AWS::IAM::Role","AWS::StepFunctions::StateMachine"]
Root sees StateMachine? false
So the guard that exists specifically to prevent #350's failure mode (deploy dies at CFN with AccessDenied on an unmapped resource type) now provides zero coverage for AWS::StepFunctions::StateMachine, AWS::Cognito::UserPoolGroup, Custom::AgentCoreRegistry, and the whole second AWS::ApiGateway::* surface.
To be explicit and fair: the shipped bundle is correct today. I hand-checked every CFN type in both nested stacks against RESOURCE_ACTION_MAP + CFN_TYPES_WITHOUT_EXEC_ROLE_IAM and the policy documents — StateMachine, UserPoolGroup, CloudFormation::Stack, Custom::AgentCoreRegistry, apigw types, Logs::LogGroup are all mapped and granted (resource-action-map.ts:63,70,110-116; application.ts:155-159,241-256; infrastructure.ts:56-58,117), BOOTSTRAP_VERSION is a correct minor bump to 1.5.0 with a truthful changelog comment (version.ts:28-37), artifacts are regenerated, and the DEPLOYMENT_ROLES.md golden baseline is updated. You did the work by hand — that is not the problem.
The problem is that the next person to add a resource to either nested stack gets a green synth-coverage run and a red deploy, which is precisely the class of bug ADR-002 built this test to make impossible. A test that passes vacuously is worse than no test, because it launders a missing check as a satisfied one.
Risk: silent erosion of least-privilege bootstrap coverage; a future deploy-time AccessDenied that CI cannot predict.
Suggested fix (in synth-coverage.test.ts, union the nested templates into the assertion):
const root = app.node.tryFindChild('backgroundagent-dev') as Stack;
const stacks: Stack[] = [root, ...root.node.findAll().filter((c): c is NestedStack => c instanceof NestedStack)];
const typesInTemplate = new Set(
stacks.flatMap((s) => Object.values(Template.fromStack(s).toJSON().Resources as Record<string, { Type: string }>).map((r) => r.Type)),
);Please also add a one-line assertion that at least one nested stack was discovered, so the traversal itself cannot silently regress to only-the-root:
expect(stacks.length).toBeGreaterThan(1);I would expect this to newly surface any nested-stack type you have not mapped — if it comes back clean, that is the proof the bundle is complete rather than the current assumption of it.
Non-blocking suggestions / nits
- No throttling on the registry
RestApi— with an O(n) read path behind it (cdk/src/constructs/registry-api.ts:147-153). The siblingTaskApisetsthrottlingRateLimit: 60/throttlingBurstLimit: 100(task-api.ts:322-324); this new API sets neither, so it inherits the 10,000 rps account default. MeanwhilelistRecordsisList + N×GetRegistryRecordand every read path funnels through it (agentcore-client.ts:305-330, correctlyTODO(GA)-marked), and resolve/list/show are open to any authenticated caller. So one caller-visible request amplifies into1 + NAgentCore control-plane calls against a preview service's quota. Today the blast radius is confined to this API (nothing consumes resolve yet —REGISTRY.md:172), which is why this is a nit rather than a blocker. It becomes load-bearing the moment PR 2 wires the orchestrator resolve step, because resolution is fail-closed and a quota-starved resolve turns into FAILED tasks. Please mirror the TaskApi limits here now — it is two lines in the existingdeployOptions. - No WAF on the registry API, unlike the main one.
AwsSolutions-APIG3is suppressed atregistry-api.ts:199with an "internal/dev, Cognito-authenticated" rationale, whileTaskApidoes associate a Web ACL (task-api.ts:532). The rationale is stated rather than hidden, so this is fine for MVP — but it is a real posture divergence between two APIs on the same user pool and is worth a line inREGISTRY.mdso it is a decision rather than an omission. - Duplicate
AWS::ApiGateway::Account— an account-level singleton now managed by two stacks. BothRestApis create one (root:TaskApiAccount…; nested:ApiAccount…, verified by synth). Two CFN resources contend for the same account-wide CloudWatch-role setting; deploy order decides which role ARN wins, and it makes the nested stack's lifecycle able to perturb the main API's access logging. ConsidercloudWatchRole: falseon the registryRestApiand letting the root own the singleton. - Dead branch:
runtime.type(registry-resolve.ts:65). The reader projectstype, butALLOWED_RUNTIME_KEYS.mcp_server(registry-publish.ts:173) does not includetype— so publish rejects it and this line can never fire for a record published through this API. Either drop it or addtypeto the publish allowlist if the intent is to accept theserver.jsonspelling. As-is it is a small contract-drift signal between the two allowlists that the "kept in sync" comment at :169-171 claims are aligned. - TS↔Py asymmetry on a corrupt descriptor (availability, not disclosure). In TS,
extractPayload(agentcore-client.ts:467,488) andparseSkillRuntime(:181-183)JSON.parseeagerly insidegetRecordById, whichlistRecordscalls for every record — so one malformedinlineContentor non-base64 frontmatter value throws aSyntaxErrorthat escapes to a 500 and takes downlist/show/resolvefor every asset in that kind/namespace. Python is deliberately better here: it only parses the winner and catches(ValueError, JSONDecodeError)into a scopedREMOVEDresolution error (agentcore_client.py:157-165). This also means the nice fail-closedisNonEmptyRuntimecheck at :355 is unreachable for a corrupt record on the TS side — the throw happens first. Requires an out-of-band write to reach (the code comments acknowledge that path), so it is a nit, but the TS side should mirror Python's per-recordtry/catchso one poisoned record degrades to one unresolvable asset rather than a namespace-wide outage. - Stray whitespace-only diff.
cdk/src/constructs/task-api.ts:235adds a blank line insideTaskApiPropsfor no reason. Worth dropping so the only change to that file is the explanatory comment block.
Documentation
Good coverage, and it explains why rather than just what.
docs/design/REGISTRY.mdis new and substantive: the separate-API rationale with the 500-resource constraint and theregistry_api_urlcost (:88-90), the access-control model (:149-154), the grammar plus the explicit "two grammars in the tree" note reconciling it with the looseragent/src/workflow/validator.pycheck (:167), and the staged PR1/PR2/PR3 plan (:172-178). The note that resolve happens in the orchestrator rather thancreateTaskCore, with the trade-off stated (FAILED task vs. 422 at submit), is exactly the kind of decision record that should be written down.docs/design/DEPLOYMENT_ROLES.mdgolden baseline updated in the same PR (Cognito group actions, theStepFunctionsstatement, the nested-stack CFN ARN,states.amazonaws.cominPassRole) — this is the file that most often drifts, so thank you for keeping it in lockstep.- Starlight mirror is in sync. I diffed
docs/src/content/docs/architecture/Registry.mdandDeployment-roles.mdagainst their sources; the only deltas are the generator's link rewrites. Not hand-edited. contracts/registry-resolution/ships a README plus two corpora consumed by four runners (two TS, two Python) — the right pattern, mirroringcontracts/cedar-parity/.- Issue tracking: #246 is
approved+ P0 and covers this stack. Governance is settled; I am not re-litigating it, and thepr/*-style branch name is a waived nit.
Minor gap: nits 2 and 3 above (WAF divergence, apigw Account singleton) deserve a sentence each in REGISTRY.md so a future reader knows they were considered.
Tests & CI
CI is fully green (8/8, incl. build (agentcore), CodeQL ×3, secrets/deps scan). All 21 review threads have author replies. Two other approvals on record (ayushtr-aws, theagenticguy) — noting for completeness that neither an approval nor green CI discharges a standing change request, which is why I re-probed everything above independently rather than relying on either.
Coverage is genuinely good, and the new tests are the right kind: they assert what the code should do (the round-tripped runtime must equal the validated payload; no bare "*" resource in any bedrock-agentcore: statement) rather than merely pinning current output. That is the distinction that matters for regression tests on security findings, and the B1/B2/#10 tests all clear it.
Locally: uv run pytest agent/tests/test_registry_agentcore_client.py test_registry_resolution_corpus.py test_registry_resolution_ranking_corpus.py → 43 passed. I could not run the CDK suites in my worktree (TS5103: Invalid value for '--ignoreDeprecations' — cdk/tsconfig.jest.json:5 asks for "6.0" but the hoisted typescript resolves to 5.9.3 in my tree; this is a local-environment artifact of the TS 6.x pin, not a defect in this PR, and CI runs them green). I read all the relevant CDK tests instead.
Bootstrap synth-coverage status: passing but incomplete — see B4. BOOTSTRAP_VERSION 1.4.0 → 1.5.0, artifacts regenerated, BOOTSTRAP_HASH updated, golden baseline updated, policies.test.ts extended with the StepFunctions sid and states service. The bundle content is right; the guard's reach is not.
Test-performance check (#366): all four new CDK test files synth once in beforeAll and none re-enable aws:cdk:bundling-stacks. Clean.
Review agents run
I must be explicit about a process limitation: I am running as a fan-out subagent of /review_prs, and agent nesting in this harness is one level deep — I cannot dispatch the pr-review-toolkit agents (code-reviewer, silent-failure-hunter, type-design-analyzer, comment-analyzer, pr-test-analyzer) or the security-review skill as nested agents from this context. Rather than silently skip the mandatory Stage-3 step, I applied each agent's rubric by hand and am listing the rubric per dimension so the gaps in my coverage are auditable:
- code-reviewer (guidelines/style/routing) — applied. Routing matches the AGENTS.md table (API/Lambdas in
cdk/, read-only port inagent/,bgagentsurface incli/). #319 UA rule swept across all three trees (clean). CDK↔CLI types-sync verified by hand: all seven registry wire types present in bothcdk/src/handlers/shared/types.tsandcli/src/types.tswith identical field sets. No hardcoded ARNs (formatArnthroughout). No Cedar engine pin movement in this diff, so no parity-fixture obligation. Found: nits 4 and 6. - silent-failure-hunter (error handling/fallbacks) — applied; this was the highest-yield dimension. Traced every
catch. The previously-dangerous swallows are now correct:waitPastCreatingre-throws non-ResourceNotFoundExceptionand throws on budget exhaustion (:404-423);RegistryPublishIncompleteErrorsurfaces the strandedrecordIdas a 502 rather than a bare 500 (registry-publish.ts:94-102) — good, an operator can act on that. Two remaining swallows are justified and commented (parseSkillFrontmatter'scatch → {}at :136 is the fail-closed direction becauseresolvethen rejects the empty runtime;ConflictExceptiontolerance indrainRecordsis retried byisComplete). Found: nit 5 (the TS eager-parse blast radius, and the resulting unreachability of theisNonEmptyRuntimefail-closed check for corrupt records). - type-design-analyzer (new types) — applied.
RegistryStatusas a closed union mirroring the substrate tokens,StorageMode, and the discriminatedRuntimePayloadare well-modelled, and keeping the port-internal domain types deliberately out of the CLI types-sync contract (documented atregistry/types.ts:24-25) is the right boundary. One observation:RuntimePayloadis a real union butruntimecrosses the API boundary asRecord<string, unknown>and is cast withas unknown as RuntimePayload(registry-publish.ts:73), so the union buys no compile-time safety at the gateway — the runtimevalidateRuntimeallowlist is doing all the work. That is acceptable (validation is the actual gate) but the cast is the seam a future refactor will misread; a narrowing parse returningRuntimePayload | nullwould make the type earn its keep. - comment-analyzer (comment accuracy) — applied, and specifically re-checked the comments attached to the fixes, since a stale comment is how a fixed finding gets silently reintroduced.
registry-resolve.ts:30-43now accurately describes an allowlist and correctly states the orchestrator bypasses this handler.agentcore-client.ts:104-110and :127-131 correctly explain why structural YAML emit/parse defeats key injection (not merely that it does).registry.ts:87-96explains whyCreateRegistryneedsresources: ['*']and the non-obvious workload-identity dependency — that comment will save someone a CREATE_FAILED.version.ts:28-37changelog matches the actual policy delta. TheTODO(GA)at :305 and the GA-throwaway markers are honest. No inaccurate comments found. - pr-test-analyzer (coverage gaps) — applied. Failure paths are covered, not just happy paths (401/403/400/409/422/502,
CREATE_FAILED, poll timeout, duplicate-delivery idempotency, the two injection regressions, the two leak regressions, the IAM resource guard). Found: B4 (the coverage hole is itself a test-design defect) and the observation that no test asserts either nested stack's resources are reachable from the bootstrap guard. - security-review (IAM/Cedar/secrets/input-gateway) — applied by hand; this diff touches IAM, a new input gateway, and secret-bearing payloads, so it was in scope. Handler IAM is scoped to
registry/{id}+registry/{id}/record/*with a regression test that rejects a bare"*". Provisioning'sresources: ['*']is genuinely create-time-unavoidable and suppressed with a truthful reason. Auth fails closed everywhere (extractUserId→ 401;userInGroup→ 403;auto_approvere-checked againstRegistryApproverafter validation).userInGroup(gateway.ts:43-56) correctly normalizes thecognito:groupsclaim across both array and delimited-string shapes and returnsfalseon absence — fail-closed. Secrets: the allowlist projection is the fix, and publish additionally refuses to store unknown keys with an error message that tells the publisher to reference a Secrets Manager ARN instead of inlining. Cedar: untouched, no engine pin movement. Found: nits 1-3.
No agent's scope was omitted as out-of-scope; the omission is the mechanical dispatch, and it is disclosed above.
Human heuristics
- Proportionality — pass. The port/adapter seam looks like ceremony until you read that it is deliberately GA-throwaway (
registry.ts:26-28), at which point one interface per language is the cheapest way to make the swap self-contained. No speculative abstraction: exactly one implementation per side, and the "engine" temptation was avoided.agentcore-client.tsat 540 lines is the largest new file and its size is essential (substrate encode/decode for three descriptor shapes), not accreted. The nested-stack split is forced by a hard CFN limit rather than chosen. - Coherence — concern (minor). Same concepts are named the same across the two languages (
RUNTIME_META_KEY/_RUNTIME_META_KEY,x-abca-runtime, identical status tokens), and the parity corpora enforce it mechanically. But the new API diverges from its sibling on two operational conventions with no stated reason — no throttling and no WAF (nits 1-2,registry-api.ts:147vs.task-api.ts:322) — and the two allowlists that a comment claims are "kept in sync" disagree ontype(nit 4). These are exactly the drifts that copy-paste-adjacent construct work produces. - Clarity — pass. Names communicate intent (
redactRuntimeForResponse,waitPastCreating,isNonEmptyRuntime,RegistryPublishIncompleteError). Error handling surfaces failures rather than hiding them behind plausible defaults — the 502-with-recordIdfor a stranded partial record is a notably good call, and the publish validation error text tells the caller what to do instead. Magic values are named constants (RECORD_CREATE_POLL_MS,SKILL_NAME_MAX,CLIENT_TOKEN_LENGTH). - Appropriateness — concern (B4). Integration behavior was verified against the real substrate, not self-written mocks — the async-create/
_meta-survival/CUSTOM-round-trip spike findings are cited inline and drive real design choices (agentcore-client.ts:19-32), and the workload-identity IAM dependency atregistry.ts:87-96is clearly a scar from an actual failed deploy rather than a guess. That is the AI001 trap avoided properly. The AI005 trap is also avoided: the new tests assert what the code should do. The failure is maintainability of the guard rather than of the code — B4 leaves the next contributor with a test that passes for the wrong reason.
|
@scottschreckengaust thanks — confirming your three prior blockers (B1 frontmatter injection, B2 denylist→allowlist, B3 UA) are all resolved, and thank you for re-probing each against |
…/adapter, API, CLI (#246) Introduces the read-side catalog for the central agent asset registry built on AWS Agent Registry (Bedrock AgentCore), with nothing upstream importing the AWS SDK directly: - Provisioning: `AgentRegistryStack` (NestedStack) creates the registry via a custom resource (async CreateRegistry, no L2 in preview); bootstrap IAM + resource-action-map updated (states, cognito group, cloudformation nested stack) with the golden DEPLOYMENT_ROLES.md kept in sync. - Ports & adapters: `RegistryClient` port (TS + Py) with a single `AgentCoreRegistryClient` adapter per language. Native descriptor storage — MCP server.json + `_meta`, AGENT_SKILLS markdown frontmatter, CUSTOM verbatim. - Grammar: `registry://kind/namespace/name@constraint` with mandatory semver pin, mirrored byte-for-byte across ref.ts / ref.py and enforced by the `contracts/registry-resolution/` parity corpus. - API: publish / resolve / list / show routes on TaskApi, gated by two Cognito groups (RegistryPublisher / RegistryApprover); `bgagent registry` CLI. - Wire types (shared/types.ts + cli/types.ts): resolved-asset triple stamped on TaskRecord/TaskDetail/TaskSummary for audit. Integration (orchestrator resolve-step, agent loaders, blueprint asset pins) lands in the follow-up PR that builds on this catalog.
#246) Review follow-ups on the catalog PR: - Python ref/semver regexes anchored with \Z instead of $. Python's $ also matches just before a trailing newline, so `registry://…@1.0.0\n` parsed in Python but was rejected by the JS mirror (ref.ts, no m flag) — a byte-for-byte parity break the grammar explicitly promises not to have. Added a `trailing-newline-rejected` case to the shared resolution corpus so CI catches any future regression on either side. - Bumped BOOTSTRAP_VERSION 1.2.0 → 1.3.0 (policy surface changed this PR: Cognito group, Step Functions, CloudFormation nested-stack ARN) and regenerated the bootstrap template so operators know their role is stale.
…orrectness, provisioning (#246) Security: - Redact secret headers in the resolve response (open to any authenticated caller); orchestrator port path stays unredacted. - Scope registry handler IAM to registry/<agentRegistryId> + /record/*, not *. Publish / adapter correctness: - Always SubmitForApproval so a normal publish reaches PENDING_APPROVAL; gate only the APPROVED transition on autoApprove. - Enforce the per-kind runtime contract at publish (reject arrays/empty/ wrong-kind) instead of only typeof object. - Treat async CREATE_FAILED and poll-budget exhaustion as publish failure (was silent success). - Base64-encode skill runtime in SKILL.md frontmatter (apostrophe/newline-safe) with a legacy single-quoted-JSON fallback; TS+Py parity. - Persist the authenticated publisher across MCP/skill/CUSTOM and surface it in show (was always null). Provisioning: - Make CreateRegistry replay-safe via a clientToken derived from the CFN RequestId. - Handle custom-resource Update (apply name/description via UpdateRegistry) instead of reporting success while ignoring desired state; add UpdateRegistry to the scoped registry IAM policy. Parity + nits: - Reject semver components beyond MAX_SAFE_INTEGER in TS+Py (+ corpus case). - Drop the no-op try/catch, hoist the double parseConstraint, add TODO(GA) on the O(n) list path, and name RegistryShowResponse (types-sync guarded).
… under the 500-resource cap (#246) The orchestration arc (#695) grew the root AgentStack to ~467 resources; adding the registry surface pushed it to 506 (518 on the ECS compute path), over CloudFormation's hard 500-resource-per-stack limit — the stack no longer synthesized or deployed. API Gateway routes must live on the same stack as their RestApi, so the only way to move the registry surface off the root is to give it its own API: - New RegistryApi nested stack: own RestApi + Cognito authorizer (bound to the SHARED user pool, so a caller's JWT works on both APIs) + the four publish/resolve/list/show Lambdas + routes + access logging + the two Cognito groups. ~37 resources move off the root. - Root AgentStack now synthesizes at 469 (default) / 481 (ecs) — both under 500. - CLI: registry commands target a separate `registry_api_url` (from the new RegistryApiUrl stack output). `bgagent configure --stack-name` captures it automatically; `--registry-api-url` sets it manually. Optional in config for backward compatibility; `bgagent registry` errors clearly if unset. - REGISTRY.md §7 documents the separate-API rationale + the two-URL setup. - Also folds in the CLIENT_TOKEN_LENGTH lint fix for the provisioning handler.
…gs (#246) Second review pass (@scottschreckengaust) on the catalog PR: - B1: SKILL.md frontmatter is now emitted/parsed via a real YAML serializer (js-yaml / pyyaml) instead of line concatenation + first-match regex, so a caller-controlled `description` can no longer inject a shadowing `x-abca-runtime` key and bypass publish-time validation. TS + Python parity, legacy single-quoted form still read; injection regression tests both sides. - B2: resolve-response redaction switches from a 3-key denylist to a per-kind allowlist (drops api_key/env/etc.; url reduced to origin), and publish now rejects unknown runtime keys — closing the open payload at both boundaries. - B3: registry SDK clients route through makeClient for solution UA (#319); ABCA_COMPONENT set on the provisioning Lambdas. - nits: reject non-boolean custom/auto_approve flags; registry-api IAM test now asserts scoped registry ARNs (no bare "*").
c8e4790 to
e3b2cae
Compare
scottschreckengaust
left a comment
There was a problem hiding this comment.
Conditional approval based on backlog issues for blockers and nits.
…mples#246) (aws-samples#548) * docs(decisions): ADR-018 propose central agent asset registry (aws-samples#246) Draft ADR proposing a versioned, immutable-per-version asset registry for MCP servers, Cedar policy modules, and skills. Fixes the contract (semver grammar, immutability, resolve-at-create-task, descriptor validation, governance workflow) and defers substrate selection to the design PR. Ranks AWS Agent Registry as preferred with DynamoDB+S3 as fallback; surveys mcp-gateway-registry, agentregistry.ai, and Entra Agent Registry+AGT as considered alternatives. Status: proposed. Requires the aws-samples#246 `approved` label before opening a follow-up implementation PR (per ADR-003). Includes regenerated Starlight mirror. * docs(decisions): reconcile ADR-018 with shipped AgentCore implementation (aws-samples#246) Addresses review on aws-samples#548 (@scottschreckengaust, @isadeks) and records the resolved substrate decision. Refinements (not a reversal — the ranking, alternatives, and flip-conditions are retained as the decision record): - Status proposed → accepted; substrate resolved to AWS Agent Registry (Bedrock AgentCore), built behind the RegistryClient seam and proven E2E. - Resolution owner: TypeScript orchestrator owns catalog/semver resolution; Python loads the already-resolved bundle (mirrored resolver only for the parity contract + direct lookups). Keeps sub-decision 6 true. - Governance: separated substrate-provided lifecycle machinery from the thin ABCA MVP surface (publish + auto_approve + resolve/list/show); standalone promote/reject/deprecate, env-gated auto-approve, and event consumption named as future scope (aws-samples#478/aws-samples#230). - Descriptor validation: MVP delegates to native descriptor types (MCP server.json, skill frontmatter, CUSTOM); shared JSON Schema capability descriptor is future scope (aws-samples#481). - Grammar: reframed "already committed" → "extends the committed shape"; authoritative strict grammar in registry/ref.{ts,py}. - Accuracy: single canonical `approved` token; aws-samples#381-split ref 12 → 13; reference URLs normalized (cloud.google.com, learn.microsoft.com). - Added dated Changelog per docs/decisions/README refinement rules. Starlight mirror regenerated via docs sync (not hand-edited). * docs(decisions): de-link not-yet-merged ref.{ts,py} paths to fix link-check (aws-samples#246) The ADR cited cdk/src/handlers/shared/registry/ref.ts and agent/src/registry/ref.py as relative-path links, but those files ship in the implementation PRs (aws-samples#664/aws-samples#665), not on this ADR branch or main — so //docs:link-check failed with 2 dead links. Demoted both to inline code spans (with a note that they land with aws-samples#664/aws-samples#665) until the implementation merges. Mirror regenerated via docs sync. * docs(decisions): keep ADR-018 proposed + address review (vocab, federation, cutover) (aws-samples#246) Addresses review feedback from @krokoko, @scottschreckengaust, @isadeks: - Revert premature proposed→accepted (README rule: accepted on impl-PR merge; aws-samples#664/aws-samples#665 still in review). Soften "shipped / proven E2E on a live stack" to "targeted by aws-samples#664/aws-samples#665, exercised on a dev stack during review"; stop citing the parked DDB+S3 PRs (aws-samples#632-aws-samples#634) as current. Add a Status note in Decision. - Add short-vs-long-form kind-vocabulary migration note to sub-decision 1: WORKFLOWS.md short forms (registry://mcp/…) are lenient-only forward-decls; only the long form (mcp_server/ns/name@constraint) resolves. No auto-aliasing. - Add a federation / "registry of registries" Non-goal (answers Scott's Jul-8 question): single operator-curated catalog; external registries are discovery-only; no federation in aws-samples#246. - Promote the 2026-08-06 AgentCore namespace cutover from a cost input to a hard gate: no production dependency until the migration is GA in-region. Mirror regenerated via docs sync (idempotent). * docs(decisions): renumber registry ADR 018 -> 022 to avoid collision (aws-samples#246) ADR-018 is already taken on main (ADR-018-linear-agent-session-interaction), and 019/020/021 are claimed by open PR aws-samples#663 and merged main ADRs. 022 is the next unclaimed number. Renames the source + Starlight mirror and updates the H1 titles; numbers are never reused (docs/decisions/README.md). * docs(decisions): add read-path + descriptor-integrity invariants to ADR-022 (aws-samples#246) Second review pass (@scottschreckengaust): - Sub-decision 11 + the substrate-invariant list gain read-path confidentiality: runtime payloads reference credentials (never inline), and open read surfaces redact by allowlist, not denylist. - Sub-decision 7 requires the validated descriptor be carried isolated from caller-controlled discovery prose (non-bypassable validation), incl. CUSTOM. - Collapse the residual submitted/PENDING_APPROVAL dual token; bump Last-updated; add the 018->022 renumber changelog entry; mark previously-pending items landed. --------- Co-authored-by: bgagent <bgagent@noreply.github.com>
Summary
Read-side catalog for the central agent asset registry (#246, ADR-018 #548) built on AWS Agent Registry (Bedrock AgentCore) — the chosen substrate. Nothing upstream imports the AWS SDK directly; the AgentCore control plane sits behind a
RegistryClientport.AgentRegistryStack(NestedStack) creates the registry via a custom resource (async CreateRegistry, no L2 in preview); bootstrap IAM + resource-action-map + goldenDEPLOYMENT_ROLES.mdin sync. Nested to keep the root stack under CloudFormation's 500-resource limit.RegistryClientport (TS + Py), oneAgentCoreRegistryClientadapter per language. Native descriptors — MCPserver.json+_meta, AGENT_SKILLS markdown frontmatter, CUSTOM verbatim.registry://kind/namespace/name@constraintwith mandatory semver pin, mirrored byte-for-byte acrossref.ts/ref.pyand enforced by thecontracts/registry-resolution/parity corpus.bgagent registryCLI.Integration (orchestrator resolve-step, agent loaders, blueprint pins) lands in the follow-up PR that builds on this catalog.
Test plan
mise run buildgreen (2583 CDK tests + agent quality)