feat(registry): design + storage + publish/resolve API (#246)#632
feat(registry): design + storage + publish/resolve API (#246)#632Kalindi-Dev wants to merge 4 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.
|
Qq: this seems like a lot of infra addition for the registry, isn't it worth it to wait for agentcore registry ? |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Request changes
Strong, well-documented foundational layer for the #246 asset registry — the DDB/S3 substrate, immutability guard, fail-closed resolver, Cognito group gating, byte-for-byte grammar parity corpus, and IAM least-privilege are all high quality. One blocking correctness bug in the resolver's status-vs-version selection contradicts the documented spec (REGISTRY.md §5) and, because this is the stack base that PR 2's orchestrator resolve step builds on, it should be fixed here rather than propagated. The red CI is a known stacked-ordering artifact (not a defect in this code) — noted below so it is not mistaken for a real break.
Vision alignment
Fits the north star well. Purely-additive infra (nothing reads from the registry yet) keeps blast radius bounded; resolution is fail-closed (mandatory pins, no floating latest, REGISTRY_RESOLUTION_FAILED on any unresolved ref), which directly serves "bounded blast radius" and "reviewable/reproducible outcomes" (resolved versions become an audit triple). The tenet trade-off (DDB+S3 fallback vs AgentCore Registry) is documented in REGISTRY.md and deferred to ADR-018. The maintainer question on the thread ("worth waiting for AgentCore registry?") is answered by the four fallback conditions + the RegistryClient seam that keeps the AgentCore swap open — a reasonable, documented call.
Blocking issues
B1. Resolver picks the highest version then checks status, instead of resolving the highest resolvable version — contradicts REGISTRY.md §5 (cdk/src/handlers/shared/registry-resolver.ts:206-215).
REGISTRY.md §5 states submitted/draft/rejected are "not a candidate; if it's the only match → NO_MATCHING_VERSION" — i.e. these statuses should be excluded from ranking so a lower approved/deprecated version can still resolve. The code filters matching only by semver constraint + valid-version (status is NOT in the filter), sorts descending, takes matching[0], and only then evaluates status in statusWarnings. Consequence: publish 1.2.0 as submitted (the normal publisher path — it lands submitted), and a task pinned ^1.0.0 that previously resolved to 1.0.0 approved now fails with NO_MATCHING_VERSION — an unapproved newer version silently shadows a working approved pin. The code's own comment at line 254 ("no lower approved version was selected") describes a fallback the code never performs. Suggested fix: filter to resolvable statuses (approved + deprecated) before ranking, then pick the highest of those; keep the special case that a removed version which is the highest match overall still yields the distinct REMOVED reason (tombstone visibility) rather than being silently skipped. Add resolver tests for [1.0.0 approved, 1.2.0 submitted]@^1.0.0 → 1.0.0 and [1.0.0 approved, 1.2.0 removed]@^1.0.0 → REMOVED.
Non-blocking suggestions / nits
- N1 — Test gap: exact prerelease pin resolution. The grammar + corpus accept
@1.4.1-rc.1(contracts/registry-resolution/grammar-valid-prerelease.json) but there is no resolveRef test that a matching1.4.1-rc.1row actually resolves undersemver.satisfies(..., { includePrerelease: false }). It should (exact comparator carries the prerelease at the same tuple), but the grammar permits the pin so the resolve path deserves a guard test.cdk/test/handlers/shared/registry-resolver.test.ts. - N2 — Comment/spec drift on
status_history(cdk/src/handlers/registry-publish.ts:134).RegistryStatusEventdoc says "rich actor+timestamp+rationale is a MUST", but the initial publish event omitsrationale.rationaleis optional in the type, so this is only a doc-vs-code wording mismatch — soften the type comment or note the initial event has no rationale. - N3 —
extractGroupsstring fallback (cdk/src/handlers/shared/gateway.ts). The bracket-strip + split-on-comma-or-whitespace fallback assumes API-GW stringification; it is exercised only for the authz gate (a group name containing a space would be mis-split, but Cognito group names disallow spaces). Fine to keep; a one-line test on the array path + the stringified path would lock the contract.
Documentation
docs/design/REGISTRY.mdis thorough (schema, API contract, resolution semantics, grammar, security/ACL, out-of-scope federation) and the Starlight mirrordocs/src/content/docs/architecture/Registry.mdis in sync (ranmise //docs:syncin the worktree — zero drift). NewErrorCodes and theregistry://grammar are documented.- CI RED is expected here: the only dead link is
docs/design/REGISTRY.md → ../decisions/ADR-018-agent-asset-registry.md(verified viadocs:link-check). ADR-018 (#548) is still OPEN/unmerged and the file does not exist on this branch, sodocs:link-checkfails by design per the PR body's stated merge order (#548 → this PR). Not a defect in this diff. TheSecrets/depsred is a pre-existingsvgoCVE (GHSA-2p49-hgcm-8545) inherited via the docs/astro tree from the main-merge — not introduced by this PR. - ROADMAP.md has no registry line item; #246 references a roadmap section — a one-line update would be nice but is non-blocking.
Tests & CI
- CDK: ran the registry suites in the worktree — 147 tests pass (handlers publish/resolve/list/show, both constructs, descriptor + resolver, agent stack).
tsc --noEmitclean;cdk:eslint --fixclean (no mutations). Agent: grammar parity + workflow validator = 53 pass. - Coverage is good on the happy + failure paths (401/403/400/409/422/404, immutability guard, fail-closed table-name, invalid-ref-never-queries). Gaps: the B1 status-fallback case (untested — masks the bug) and N1 prerelease resolution.
- Bootstrap synth-coverage: PASS / not-applicable. This is the key CDK check for new resource types. Verified
AWS::DynamoDB::Table(resource-action-map.ts:66) andAWS::S3::Bucket(line 93) are already incdk/src/bootstrap/resource-action-map.tsand the policy bundle (the stack already deploys many DDB tables + S3 buckets). No new CFN resource type is introduced (the four registry Lambdas areAWS::Lambda::Function, already covered), so noBOOTSTRAP_VERSIONbump / artifact regen is required — correctly omitted. The registry table/bucket use the same L2 constructs + IAM grant patterns as existing resources, so the CFN-exec-role ARN patterns already cover them. - IAM least-privilege on the four Lambdas is correct: publish = table RW + bucket
grantPut; resolve = table read + bucket read; list/show = table read only. S3 reads are only via short-lived (300s) presigned GETs from the authn'd resolve handler; the S1 access-log nag suppression is justified for an admin-published catalog.
Review agents run
- /security-review — invoked; the skill mis-detected git state (local main, not the worktree), so I performed the security pass manually over the actual registry diff. Findings: no HIGH/MED.
kind/namespace/nameare regex-constrained (^[a-z][a-z0-9-]*$/^[a-z0-9][a-z0-9._-]*$) and cannot contain/, so no path traversal into the{kind}/{namespace}/{name}/{version}/artifactS3 key or the DDBpk; DDB queries use parameterizedExpressionAttributeValues(no NoSQL injection); base64 artifact decode is bounded by API-GW payload limits; publish gated onRegistryPublisher/RegistryApprover,auto_approvegated on approver; presigned URLs are short-lived and key-scoped. - code-reviewer — ran (manual, guidelines/style): clean; L2 constructs, sane removal policies (DESTROY default with documented RETAIN-for-prod rationale), no hardcoded ARNs/accounts.
- silent-failure-hunter — ran: found B1 (fail-closed in the wrong direction — shadows an approved version); handlers otherwise fail closed with specific reasons and 500-wrap unexpected errors.
- type-design-analyzer — ran: shared types are clean and CDK↔CLI mirrored;
check-types-sync.tsallowlist correctly updated for the server-onlyRegistryAssetRecord/RegistryStatusEvent. - pr-test-analyzer — ran: strong coverage; gaps N1 + the B1 case.
- comment-analyzer — ran: N2 (status_history rationale) and the line-254 "no lower approved version" comment that describes behavior the code lacks (part of B1).
- Omitted: none in scope.
Human heuristics
- Proportionality — Pass. Complexity matches a foundational registry; the two-language
RegistryClientseam + parity corpus is justified by the real drift hazard, not over-abstraction. - Coherence — Pass. Same
approvedtoken byte-for-byte across TS resolver, Python loader, and DDB;pk/sk/artifactKeyhelpers are single-sourced; constructs mirror existing bucket/table patterns. - Clarity — Concern. The line-254 comment (
registry-resolver.ts) claims a lower-approved fallback the code does not implement (B1) — a name/behavior mismatch that would mislead the PR-2 author. - Appropriateness — Pass. Maintainable by this team; resolver semantics verified against real
node-semverbehavior (lexicographic-DDB-sort caveat handled by ranking in code), not only self-written mocks.
Stack / merge-order note
This is the base of the #246 series (ADR #548 → this PR → PR 2 orchestrator/MCP → PR 3 Cedar/skills). Two consequences: (1) merge #548 first so docs:link-check goes green — do not merge this ahead of the ADR. (2) Fix B1 in this PR, not in PR 2 — PR 2's orchestrator resolve step consumes this exact resolveRef/resolveAll; shipping the wrong status-selection semantics into the foundation would silently break approved-pin resolution for every downstream consumer.
| // DynamoDB returns rows sorted lexicographically by the version sort key, | ||
| // which is WRONG for semver (1.10.0 < 1.9.0 as strings) — so we always rank | ||
| // in code (REGISTRY.md §3.2). | ||
| const matching = rows |
There was a problem hiding this comment.
Blocking (B1): this filter selects candidates by semver constraint + valid-version only — status is not considered until matching[0] is chosen below. REGISTRY.md §5 says submitted/draft/rejected are "not a candidate", meaning they should be excluded from ranking so a lower approved/deprecated version still resolves. As written, publishing a newer submitted version (the normal publisher landing state) shadows an existing approved pin and makes ^1.0.0 fail with NO_MATCHING_VERSION. Filter to resolvable statuses (approved+deprecated) before ranking; keep the case where a removed version that is the overall highest match still surfaces the distinct REMOVED reason.
| case 'submitted': | ||
| case 'draft': | ||
| case 'rejected': | ||
| // The highest semver match is not in a resolvable state and no lower |
There was a problem hiding this comment.
This comment says "no lower approved version was selected", but the code never attempts to select a lower version — matching is not status-filtered before matching[0], so no fallback exists. Comment describes intended behavior the implementation lacks (see B1).
| status, | ||
| publisher: userId, | ||
| created_at: now, | ||
| status_history: [{ status, actor: userId, at: now }], |
There was a problem hiding this comment.
Nit (N2): RegistryStatusEvent's doc calls rationale a MUST, but the initial publish event omits it. rationale is optional in the type so this is fine at runtime — soften the type comment or note that the initial submitted/approved event carries no rationale.
Area
cdk— infrastructure, handlers, constructsagent— Python runtime / Docker imagecli—bgagentclientdocs— guides or design sources (docs/guides/,docs/design/)Related
REGISTRY.md → ADR-018link failsdocs:link-checkuntil docs(decisions): ADR-018 propose central agent asset registry (#246) #548 lands onmain.feat/246-registry-orchestrator-mcp) → PR 3 (feat/246-registry-cedar-skills).Changes
First of three PRs. Purely additive infrastructure — nothing in ABCA reads from the registry yet.
docs/design/REGISTRY.md— asset-kind catalog, DDB/S3 schema, publish/resolve/list/show API contract, resolution semantics (semver, immutability, status), and the upstream-registry/federation stance (out of MVP scope).RegistryClientseam keeps the AgentCore swap open._REGISTRY_REFtoregistry://kind/namespace/name@constraint(snake_case kinds, mandatory semver pin) on both the Python validator and the TS resolver, with acontracts/registry-resolution/parity corpus proving both agree byte-for-byte.RegistryAssetKind/Status/Ref/Descriptor/Record,ResolvedAsset(Bundle/Summary)in cdktypes.ts, mirrored in clitypes.ts. Canonical status token isapproved(noactive).registry-resolver.ts—parseRef/resolveRef/resolveAll, semver ranked in code (DDB sorts versions lexicographically), fail-closed with specific reasons.RegistryAssetsTable(pk/sk + kind-index GSI) andRegistryArtifactsBucket(versioned, TLS-only, SSE) constructs.RegistryPublisher/RegistryApprovergroup gating) +/registryroutes on TaskApi with least-privilege IAM.Tests: full cdk + agent suites green. Only the known
docs:link-check(ADR-018 cross-PR link) fails.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.