Skip to content

feat(registry): resolve + load registry assets into tasks (#246) - #665

Merged
Kalindi-Dev merged 3 commits into
mainfrom
feat/246-registry-integration
Aug 12, 2026
Merged

feat(registry): resolve + load registry assets into tasks (#246)#665
Kalindi-Dev merged 3 commits into
mainfrom
feat/246-registry-integration

Conversation

@Kalindi-Dev

@Kalindi-Dev Kalindi-Dev commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on the catalog PR (#664) to consume registry assets at task time.

  • Orchestrator resolve-step (resolveRegistryAssets): resolves a blueprint's registry:// mcp_server/cedar_policy_module/skill refs at task start, fail-closed; stamps {kind,id,version} triples on the TaskRecord, merges resolved cedar_text into cedar_policies, threads the runtime bundle into the agent payload.
  • Blueprint asset props + onUpdate fix: assets.{mcpServers,cedarPolicyModules,skills} with RegistryRefValidation; the three onUpdate helpers now write the asset-ref columns so redeploying an onboarded repo no longer drops them.
  • Agent loaders (registry.loader): mcp_server → .mcp.json; cedar_policy_module → PolicyEngine unannotated extra_policies; skill prompt fragments → system prompt.
  • TaskOrchestrator IAM: read-only bedrock-agentcore registry access, scoped to the wired registry.

Depends on #664. Base is feat/246-registry-catalog; retarget to main once #664 merges.

Test plan

  • mise run build green
  • Deploy with --context forkBlueprintRepo=owner/repo; submit a task that pins all three asset kinds
  • Confirm the opened PR shows the skill marker and that .mcp.json is not committed (last verified: task 01KZVFVGP89077B385ZQDXGCZ4 → PR chore(project): update structure #7)

Local end-to-end test

Verified against a fresh deploy of this branch (rebased on main @ d981c955). The E2E exercises all three MVP asset kinds in a single task.

The three pinned assets

The ForkBlueprint (cdk/src/stacks/agent.ts, opt-in via --context forkBlueprintRepo=) pins one of each kind at ^1.0.0:

Kind Ref What it does How you observe it
mcp_server registry://mcp_server/acme/aws-knowledge@^1.0.0 AWS Knowledge MCP (public, read-only, no auth) — {transport: http, url: https://knowledge-mcp.global.api.aws/mcp} merged into the run's .mcp.json; the server is available to the agent (and, per the secret-containment fix, .mcp.json is not committed to the PR)
cedar_policy_module registry://cedar_policy_module/acme/guard@^1.0.0 forbid (principal, action == Agent::Action::"invoke_tool", resource == Agent::Tool::"WebSearch"); concatenated into cedar_policies; the WebSearch tool is denied at the PolicyEngine
skill registry://skill/acme/readme-helper@^1.0.0 prompt fragment: "…add a comment line … that reads exactly: ABCA-REVIEWED" (tool_hints: [Edit, Write]) appended to the system prompt; edited files carry the ABCA-REVIEWED marker

Sample payloads live in spike/assets/ (discovery.json/runtime.json, cedar-*.json, skill-*.json).

Steps

1. Bootstrap + deploy (bundle is v1.5.0; re-bootstrap so the CFN role has the registry IAM):

cd cdk
MISE_EXPERIMENTAL=1 mise //cdk:bootstrap
MISE_EXPERIMENTAL=1 mise //cdk:deploy -- \
  --context stackName=<stack> \
  --context forkBlueprintRepo=<owner/repo> \
  --require-approval never

2. Configure the CLI + authenticate:

bgagent configure --stack-name <stack> --region <region>
bgagent login --username <you>

3. Join the registry Cognito groups (publish needs RegistryPublisher; --auto-approve needs RegistryApprover), then re-login for fresh claims:

POOL_ID=$(aws cloudformation describe-stacks --stack-name <stack> --region <region> \
  --query "Stacks[0].Outputs[?OutputKey=='UserPoolId'].OutputValue" --output text)
aws cognito-idp admin-add-user-to-group --region <region> --user-pool-id "$POOL_ID" --username <you> --group-name RegistryPublisher
aws cognito-idp admin-add-user-to-group --region <region> --user-pool-id "$POOL_ID" --username <you> --group-name RegistryApprover
bgagent login --username <you>

4. Publish the three assets (APPROVED):

S=spike/assets
bgagent registry publish --kind mcp_server --namespace acme --name aws-knowledge \
  --asset-version 1.0.0 --discovery "$S/discovery.json" --runtime "$S/runtime.json" --auto-approve
bgagent registry publish --kind cedar_policy_module --namespace acme --name guard \
  --asset-version 1.0.0 --discovery "$S/cedar-discovery.json" --runtime "$S/cedar-runtime.json" --custom --auto-approve
bgagent registry publish --kind skill --namespace acme --name readme-helper \
  --asset-version 1.0.0 --discovery "$S/skill-discovery.json" --runtime "$S/skill-runtime.json" --custom --auto-approve

(cedar/skill use --custom — verbatim CUSTOM storage; mcp_server uses the native MCP descriptor.)

5. Sanity-check resolution (fail-closed):

bgagent registry resolve "registry://mcp_server/acme/aws-knowledge@^1.0.0"
bgagent registry resolve "registry://mcp_server/acme/aws-knowledge@2.0.0"   # -> REGISTRY_RESOLUTION_FAILED / NO_MATCHING_VERSION

6. Submit a task against the onboarded fork repo (onboarded by the ForkBlueprint at deploy — no repo onboard needed):

bgagent submit --repo <owner/repo> --task "Add a one-line note to the README describing this repo." --wait
bgagent status <task-id> --output json | jq '{status, resolved_assets, pr_url}'

Expected result (last verified: task 01KZVFVGP89077B385ZQDXGCZ4 -> PR #7)

  • status: COMPLETED; resolved_assets lists all three {kind, id, version:"1.0.0"} triples.
  • The opened PR's diff contains the ABCA-REVIEWED marker (skill loaded).
  • The PR diff does not contain .mcp.json — the resolved MCP config is loaded for the run but kept out of the commit (secret-containment; skip-worktree guard).
  • resolved_assets is stamped on the TaskRecord for audit.

Fail-closed checks: an unresolvable/malformed pin fails the task (never runs with a substituted asset); a deprecated asset resolves with a durable registry_asset_warning TaskEvent.

Known follow-ups (edge cases beyond the happy-path E2E): the .mcp.json secret-containment guard has open hardening issues — #758 (symlinked path write-through), #759 (guard fails open under index.lock/unmerged-index), #760 (skip-worktree drops legitimate .mcp.json edits). Recommended durable fix (route registry MCP servers through the SDK in-process mcp_servers option) is captured there.

Kalindi-Dev pushed a commit to Kalindi-Dev/sample-autonomous-cloud-coding-agents that referenced this pull request Jul 27, 2026
…-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.
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-integration branch from d3dde44 to e05bfce Compare July 27, 2026 21:52
@Kalindi-Dev
Kalindi-Dev marked this pull request as ready for review July 27, 2026 21:54
@Kalindi-Dev
Kalindi-Dev requested review from a team as code owners July 27, 2026 21:54
@Kalindi-Dev

Copy link
Copy Markdown
Contributor Author

Self-review (principal-architect pass)

Ran a /review_pr-style self-review plus a re-review after rebasing onto the updated base (#664) so the parity \Z fix is physically present in this branch. Verdict: no blocking issues.

Verified correct:

  • Fail-closed resolutionresolveRegistryAssets throws on an unresolvable/malformed ref; hydrateAndTransition fails the task at HYDRATING before the RUNNING transition. No silent proceed.
  • Cedar merge safety by construction — resolved cedar_text flows through PolicyEngine's legacy extra_policies, which hard-raises on @tier/@rule_id and force-wraps every rule as @tier("soft"). A registry module cannot escalate to hard-deny.
  • onUpdate fix — all three helpers (buildUpdateFields/buildExpressionNames/buildExpressionValues) now write the asset-ref columns, with a regression test asserting it — closes the "redeploy silently drops asset refs" footgun.
  • Loader orderingapply_resolved_assets runs after setup.repo_dir exists; .mcp.json merge preserves existing servers (test-covered).

One finding worth stating explicitly (documented tradeoff, not a blocker): a resolved mcp_server url is written into .mcp.json verbatim and is bounded only by the VPC-wide DNS firewall (observation-mode + fail-open by default). This is the same posture as existing channel MCP (LINEAR_MCP_URL/JIRA_MCP_URL) and repo-committed MCP servers — this PR doesn't weaken an existing boundary, it adds a registry-mediated way to populate the same file, gated by fail-closed resolution + IAM-gated publish. Binding registry MCP URLs to the blueprint's egress allowlist at resolve time would need a design decision. Treat registry publish as a privileged, audited operation.

Rebased onto d3e2b056; full build green (2596 tests). CI green on the rebased tip.

@scottschreckengaust scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Approve with nits. Stacked on #664 (base feat/246-registry-catalog, present locally at d3e2b056). The three critical foci — fail-closed resolution, Cedar merge safety, and least-privilege registry IAM — all hold. The onUpdate column-drop fix is real and has a regression test. Nits below are non-blocking (two are pre-existing shared patterns). Merge order: #664 first, then retarget to main.

Vision alignment

Advances bounded blast radius and reviewable outcomes (VISION.md): registry pins are semver-locked, resolved fail-closed at admission, and stamped as an immutable {kind,id,version} audit triple on the TaskRecord. Cedar modules ride the same cedar_policies payload as inline blueprint policies, so the PolicyEngine's soft-tier force-wrap invariant is preserved by construction — a registry module cannot widen authorization. The documented tradeoff (registry MCP url is bounded only by the VPC DNS firewall, same posture as channel MCP) is a coherent tenet trade, not a regression; registry publish is correctly treated as a privileged, IAM-gated operation. No undocumented tenet trade.

Blocking issues

None.

Non-blocking suggestions / nits

  1. Agent-side MCP load fails open (fail-closed is resolution-only). agent/src/registry/loader.py:189-195 — on an .mcp.json write error, apply_mcp_assets logs ERROR, returns 0, and the task proceeds without the operator-pinned MCP server. The fail-closed guarantee is entirely at the orchestrator resolve step (resolveRegistryAssetsfailTask(HYDRATING)); once a bundle is threaded, a load failure is silent. cedar_policy_module still fails-closed (PolicyEngine raises at construction) and skill is pure prompt text, so only mcp_server degrades silently — and it mirrors the existing configure_channel_mcp posture. Consider surfacing a task-visible warning (progress event) when a resolved MCP asset fails to load, so a pinned tool silently missing is observable.
  2. Registry cedar_text bypasses the 64 KB blueprint cap. agent/src/policy.py:884-889 counts only blueprint_hard_policies + blueprint_soft_policies; registry cedar modules arrive via the legacy extra_policies path (runner.py:288), which is uncapped. Pre-existing property of the extra_policies kwarg, not introduced here, but the registry now makes it operator-reachable at scale — worth a follow-up to fold extra_policies byte-length into the cap.
  3. onUpdate does not REMOVE dropped asset columns. cdk/src/constructs/blueprint.ts:363-365,400-402 gate the write on length > 0, so redeploying an onboarded repo that removed all its asset refs leaves the stale mcp_servers/cedar_policy_modules/skills columns in DDB. This exactly matches the sibling cedar_policies/egress_allowlist behavior, so it is consistent — but the PR's own framing ("redeploy must not drop asset refs") is the mirror-image gap. Non-blocking; note it as a known limitation.
  4. forkBlueprintRepo demo hook is undocumented. cdk/src/stacks/agent.ts:176-192 adds an opt-in context/env flag pinning hardcoded acme/* refs. It is clearly commented and opt-in, but a one-line mention in the deployment guide would help operators reproduce the E2E test plan.

Documentation

No docs changed in this PR, and none are strictly required: the resolve-step + loader behavior and the registry:// grammar were documented ahead of implementation in docs/design/Registry.md (mirror docs/src/content/docs/architecture/Registry.md, lines 176/189), which already describes "PR 2" resolve+load and the fail-closed pin semantics. Mirror is in sync (this PR touches no doc sources). Only gap is the undocumented forkBlueprintRepo demo flag (nit 4). Issue #246 is approved (P0) — governance satisfied.

Tests & CI

Strong coverage on both sides. Agent: agent/tests/test_registry_loader.py (15 tests) covers merge/preserve/multi-server, non-mcp skip, empty-runtime skip, missing repo_dir, malformed-existing-treated-as-absent, skill fragment assembly + tool_hints + ordering + blank skip. CDK: cdk/test/handlers/shared/registry-orchestrator.test.ts covers happy path, multi-ref order, fail-closed on malformed ref (resolve never called), fail-closed on NO_MATCHING_VERSION, DEPRECATED-warns-but-resolves, and all three kinds together; cdk/test/constructs/blueprint.test.ts adds onCreate mapping, omit-when-empty, the onUpdate regression guard, and synth-time rejection of floating/malformed refs. I ran both locally: agent 15/15 pass; CDK registry-orchestrator + blueprint 54/54 pass. All PR CI checks green (build agentcore, secrets/deps scan, dead-code advisory, PR title). No new CDK per-test synth or re-enabled bundling. No bootstrap update needed — the IAM change is a runtime data-plane grant on the orchestrator Lambda role, not a new CFN resource type (the registry construct + BOOTSTRAP_VERSION bump landed in #664, d3e2b056).

Review agents run

Specialized pr-review-toolkit subagents were not separately dispatchable in this execution context, so I performed the equivalent analysis by hand and state that explicitly:

  • code-reviewer (by hand): routing correct (agent runtime in agent/, orchestration/IAM in cdk/); L2/IAM idioms clean; ArnFormat.SLASH_RESOURCE_NAME wildcards justified.
  • silent-failure-hunter (by hand): resolution path fail-closed and verified against the caller (orchestrate-task.ts:154); found the one fail-open surface (nit 1, agent MCP write) and confirmed the other two kinds fail-closed.
  • type-design-analyzer (by hand): ResolvedAssetTriple parity CDK↔CLI confirmed; agent resolved_assets: list[dict[str,Any]] is a pragmatic passthrough shape.
  • comment-analyzer (by hand): comments accurate; the "byte-identical from PolicyEngine's view" claim verified against policy.py extra_policies handling.
  • pr-test-analyzer (by hand): happy + failure paths both covered; ran the suites.
  • security-review scope (by hand): IAM least-privilege + Cedar privilege-surface reviewed; no secrets/network regressions.

Human heuristics

  • Proportionality — Pass. Per-kind loaders are small and single-purpose; no over-abstraction. Resolve-step reuses the existing cedar_policies threading rather than inventing a parallel channel.
  • Coherence — Pass. registry:// grammar, ResolvedAsset, and the triple are spelled consistently across TS/Py/CLI; resolve-step mirrors how cedar_policies already flows (orchestrator.ts:748-758).
  • Clarity — Mostly pass; one concern: loader.py:189-195 fail-open on write hides a missing pinned tool behind an ERROR log (nit 1).
  • Appropriateness — Pass. Cedar merge verified against real PolicyEngine behavior (policy.py:856-905), not a self-written mock; tests assert intended fail-closed semantics, not just current output.

@isadeks isadeks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request changes

I re-reviewed approved head e05bfce3 independently against ADR-018 and the composed #664 base. The focused suites remain green (112/112 CDK, 39/39 Python), but live in-process probes show that typed Blueprint refs, detach/update behavior, loader completion, MCP identity, Cedar limits, and deprecation audit are not enforced. These let the task record claim a pinned asset that was not actually applied, so they block the advertised reproducibility/fail-closed contract.

Comment thread cdk/src/constructs/blueprint.ts Outdated
Comment thread cdk/src/constructs/blueprint.ts
Comment thread agent/src/registry/loader.py Outdated
Comment thread agent/src/registry/loader.py Outdated
Comment thread cdk/src/handlers/shared/orchestrator.ts
Comment thread cdk/src/handlers/shared/orchestrator.ts
Comment thread agent/src/registry/loader.py Outdated
Kalindi-Dev pushed a commit to Kalindi-Dev/sample-autonomous-cloud-coding-agents that referenced this pull request Aug 4, 2026
…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).
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-catalog branch from d3e2b05 to dc33a74 Compare August 10, 2026 22:51
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-integration branch 3 times, most recently from 1eac65b to bb85527 Compare August 10, 2026 23:22
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-catalog branch from dc33a74 to 4f8da98 Compare August 10, 2026 23:48
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-integration branch from bb85527 to 8362f4f Compare August 10, 2026 23:48
@Kalindi-Dev

Copy link
Copy Markdown
Contributor Author

Thanks @scottschreckengaust and @isadeks. This round closes all of @isadeks's P1/P2 inline findings and the fail-open surface @scottschreckengaust flagged. Rebased on the updated #664 and latest main; build (agentcore) green. Details:

@isadeks — P1/P2 blocking

  1. Validate expected kind per typed Blueprint field (blueprint.ts:240) — RegistryRefValidation now takes an expectedKind and rejects a mismatch at synth (a skill ref under mcpServers, etc.). Added blueprint tests for each cross-kind rejection.

  2. Emit REMOVE so empty asset arrays detach stored refs (blueprint.ts:397) — onUpdate now emits REMOVE #mcp_servers, … for empty asset arrays, so redeploying a repo that dropped its last ref actually detaches it in DynamoDB. Added populated→empty transition tests.

  3. Use an injective MCP server key (loader.py:35) — _server_key no longer collapses hyphen→underscore, so foo-bar and foo_bar stay distinct instead of one silently overwriting the other. Added a collision test asserting both survive with distinct URLs.

  4. Fail the task when a resolved asset can't be applied (loader.py:148) — The loader is now fail-closed: apply_mcp_assets raises RegistryAssetLoadError on empty/malformed runtime, and build_skill_prompt_fragment raises on a missing/blank prompt_fragment — instead of warn-and-skip. So a pinned asset either loads or the task fails; the stamped audit bundle can no longer claim an asset that wasn't applied. Added a pipeline.run_task propagation test proving a malformed asset marks the task FAILED and the agent never runs.

  5. Include registry Cedar bytes in the 64 KiB cap (orchestrator.ts:852 / policy.py) — policy.py now counts extra_policies (the path registry cedar arrives on) toward the 64 KiB aggregate, so a large registry policy can't bypass the bound. Added a cap test with registry-supplied text.

  6. Write the MCP config schema the runtime consumes (loader.py:83) — _to_mcp_config now maps transport → type (the discriminant the Claude Agent SDK + channel_mcp actually read) and validates the shape (http/sse require url, stdio requires command, unknown transport rejected). Tests assert a consumable config, not byte-for-byte persistence.

  7. [P2] Persist deprecation warnings in the task audit surface (orchestrator.ts:543) — Deprecation now emits a durable registry_asset_warning TaskEvent and keeps warnings on the stamped resolved_assets triple, not just a Lambda log. Added an orchestrator test asserting the TaskEvent + stamped warning.

@scottschreckengaust — approve-with-nits

  1. Agent-side MCP load failing open — Addressed by @isadeks feat: add FargateAgentStack as alternative compute backend #4 above: the loader is now fail-closed on a pinned asset, so a missing pinned tool fails the task rather than silently proceeding.
  2. Registry cedar_text bypassing the 64 KB cap — Fixed (same as @isadeks feat: add Iteration 3e for memory security and integrity (OWASP ASI06) #5).
  3. onUpdate not REMOVE-ing dropped columns — Fixed (same as @isadeks Docs: specify that you can't use the agent with the canned repo #2).
  4. forkBlueprintRepo demo hook undocumented — Documented the demo flag (REGISTRY.md §12.1).

Additional hardening from a second internal review pass

  • Security (ADR-016 Linear bypass): the registry MCP merge ran after strip_linear_mcp_servers, so a registry-published Linear server could be re-added under bypassPermissions. Fixed by re-running the strip after the registry merge, with a WARN log if it removes a registry-introduced Linear entry. Test added.
  • Fail-open on empty cedar: an empty/whitespace cedar_text on a pinned module was silently dropped by a .filter(length>0) while still stamped as applied (a dropped deny rule = widened permissions). Now throws RegistryResolutionError. Test added.
  • Fail-closed propagation tests for both hydrateAndTransition (CDK) and pipeline.run_task (agent) proving an unresolvable/malformed asset fails the task.

@isadeks — these were the items behind your change request; re-review welcome when you have a moment.

ayushtr-aws
ayushtr-aws previously approved these changes Aug 11, 2026

@scottschreckengaust scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Request changes — all 7 of isadeks' prior blocking findings are genuinely FIXED at 8362f4fb, each with a regression test; I verified every one against current code, not the commit message. But the newly-added loader creates one new P1 that #665 itself introduces: the registry merge writes the unredacted runtime payload (which the base PR's own redaction docstring says may carry Authorization headers / --api-key args) into <repo_dir>/.mcp.json inside a live git working tree, and the post-hook safety net (git add -u → commit → push) will exfiltrate it to the PR whenever the target repo tracks .mcp.json. That is a #665-created exposure, not a re-report of #664's resolve-endpoint blocker.

This is a high-quality revision. The gap is narrow and mechanically fixable.

Vision alignment

Strongly advances bounded blast radius and reviewable outcomes (VISION.md, ADR-022). The whole point of the force-push was to convert five fail-open seams into fail-closed ones, and it does: synth-time kind validation, DDB REMOVE for detach, RegistryAssetLoadError propagating to write_terminal(FAILED), the 64 KB cap folded over extra_policies, and a durable registry_asset_warning TaskEvent. The resolved_assets audit triple is now accurate by construction rather than by hope — that is exactly the right invariant.

One genuinely excellent touch nobody asked for: agent/src/pipeline.py:1168 re-runs strip_linear_mcp_servers after the registry merge, closing an ADR-016 bypass (a registry-published Linear server would otherwise land post-strip and run under bypassPermissions). That is the house fail-closed instinct applied unprompted.

Where it drifts from the tenet is secret containment: the loader treats the runtime payload as inert config, but #664 explicitly classifies it as secret-bearing. Fail-closed on availability is done; fail-closed on confidentiality is not.

Disposition of each prior blocking finding

I re-tested all 7 at the current head. 7 FIXED, 0 still live, 0 moot.

(1) P1 typed Blueprint fields don't validate expected kind — FIXED.
cdk/src/constructs/blueprint.ts:538-559: RegistryRefValidation now takes an expectedKind third arg and rejects result.ref.kind !== this.expectedKind; wired at :261-263 with 'mcp_server' / 'cedar_policy_module' / 'skill'. Regression test: cdk/test/constructs/blueprint.test.ts'rejects a ref whose kind does not match its field at synth' asserts the exact message for a skill ref under mcpServers. Locks the fix in.

(2) P1 empty arrays don't remove stored refs — FIXED.
blueprint.ts:443-461: new emptyAssetFields() / buildRemoveClause() / buildRemoveNames(); :337 composes SET … ${this.buildUpdateFields(props)}${this.buildRemoveClause()} and :342 merges buildRemoveNames(). Two regression tests: 'onUpdate REMOVEs asset columns that are now empty' (partial: REMOVE #cedar_policy_modules, #skills) and '…when none are pinned' (all three), plus the populated case asserting not.toContain('REMOVE'). Populated→empty transition is covered as asked.

(3) P1 MCP server key not injective — FIXED.
agent/src/registry/loader.py:32-41: the hyphen→underscore normalization is gone; _server_key returns f"{namespace}__{name}" raw, with a comment stating why. foo-baracme__foo-bar and foo_baracme__foo_bar are now distinct. Regression test: test_hyphen_and_underscore_names_do_not_collide (loader.py test file, line 86) — asserts two keys survive. Verified injective.

(4) P1 task not failed when a resolved asset can't be applied — FIXED, and this is the deepest fix.
New RegistryAssetLoadError (loader.py:104) raised on: missing/non-dir repo_dir (:135), empty/non-dict runtime (:151), structurally invalid transport (:63-77), OSError on write (:166). The wrapper apply_resolved_assets (:209) now returns the written keys and propagates. The pipeline caller at agent/src/pipeline.py:1157-1160 no longer discards it, and the raise reaches the outer handler at pipeline.py:1746task_state.write_terminal(config.task_id, "FAILED", …)raise. Skills fail closed too (loader.py:198, blank prompt_fragment raises). Regression tests: test_pipeline.py::test_malformed_registry_asset_fails_the_task_closed asserts agent_ran is False and a write_terminal(…, 'FAILED', …) call — i.e. it tests the invariant, not the current output. Plus 6 loader-level raise tests. I ran the agent suite: 226 passed.

(5) P1 registry Cedar bytes excluded from the enforced cap — FIXED.
agent/src/policy.py:882-902: operator_text now concatenates blueprint_hard_policies + blueprint_soft_policies + *(extra_policies or []) before the POLICIES_MAX_BYTES check. isadeks' 65,574-byte probe would now raise. Regression test: test_policy_three_outcome.py::test_registry_extra_policies_counted_in_64kb_cap passes an oversized policy via extra_policies= alone and expects ValueError(/64 KB cap/). This also closes what my own prior review filed as non-blocking nit 2 — nice.

(6) P2 deprecation warnings dropped from the audit surface — FIXED (exceeded the ask).
cdk/src/handlers/shared/orchestrator.ts:803-810 persists ...(a.warnings.length > 0 && { warnings: [...a.warnings] }) into the resolved_assets stamp, and :816-827 emits a durable emitTaskEvent(task.task_id, 'registry_asset_warning', …) per warned asset. ADR-022 sub-decision 4 is now actually satisfied. Regression test: orchestrate-task.test.ts'emits a registry_asset_warning TaskEvent for a DEPRECATED asset' asserts both the Put and [':ra'][0].warnings.

(7) P1 MCP config schema mismatch (transport vs type) — FIXED.
loader.py:44-82: new _to_mcp_config maps transporttype (the discriminant McpHttpServerConfig/McpSSEServerConfig and channel_mcp._jira_server_entry() at channel_mcp.py:78 actually use), passes everything else through, and is idempotent for payloads already in SDK shape (:78-79). It also validates — http/sse without url, stdio without command, unknown transport all raise. Regression tests: test_normalizes_transport_to_type, test_http_without_url_raises, test_stdio_without_command_raises, test_unknown_transport_raises, test_stdio_with_command_loads — and per isadeks' explicit request these assert a consumable config, not byte-for-byte persistence.

On the base PR (#664) credential finding — not double-reported

I treated as established fact that #664's resolve path returns credential-bearing runtime data in cleartext (denylist redaction over 3 field names vs. publish accepting unknown runtime keys). That is #664's blocker and I am not re-reporting it here. But per the two implied questions:

  • Does #665 lean on a fail-closed guarantee #664 doesn't provide? No. #665's fail-closed claims are all about availability/audit accuracy (a pinned asset either loads or the task fails), and it enforces those itself. It does not depend on #664's redaction for anything.
  • Does #665 WIDEN the exposure? Yes — see B1. #664 leaks to an authenticated API caller; #665 is what writes the same values into a file inside a git working tree that gets committed and pushed to a public-capable PR. The additional exposure is created by #665 code (loader.py:161-165), so it is reported here.

New blocking issues

B1 [P1-security] agent/src/registry/loader.py:161-165 — the unredacted runtime payload is written into a tracked git working tree and can be committed + pushed to the PR

config["mcpServers"] = servers; json.dump(config, f, indent=2) writes the resolved runtime verbatim into <repo_dir>/.mcp.json. _to_mcp_config (:80-82) deliberately passes every non-transport key through untouched — so headers: {Authorization: "Bearer …"}, url: "https://…?token=…", api_key, env.TOKEN, and args: ["--api-key=…"] all land on disk in cleartext. #664's own redactRuntimeForResponse docstring (cdk/src/handlers/registry-resolve.ts:31-43) states these fields are secret-bearing, and publish validation (registry-publish.ts:174-176) only type-checks headers — it does not require ${ENV_VAR} placeholders, so literal secrets are publishable.

Risk — verified empirically, not reasoned: repo_dir is the live clone. If the target repo tracks .mcp.json (common — ABCA gitignores it, but arbitrary onboarded repos commit theirs; the strip logic at channel_mcp.py:200 exists precisely because "a repo could COMMIT a .mcp.json"), then the registry merge shows as a modification to a tracked file. I reproduced the full chain in a scratch repo:

$ git status --porcelain     →   M .mcp.json
$ git add -u                 # exactly post_hooks.ensure_committed:286
$ git diff --cached --quiet  →  STAGED (exit 1)
$ git diff --cached | grep -c 'SUPERSECRET|sk-live-abc123|ghp_realtoken'  →  3

post_hooks.ensure_committed (agent/src/post_hooks.py:253-324, invoked at pipeline.py:1434) stages tracked-but-modified files, commits chore(agent): save uncommitted work from session end, then ensure_pushed (post_hooks.py:418-436) does git push -u origin <branch>. Net effect: an operator-pinned MCP server's bearer token is committed to the agent's branch and pushed to the PR. There is no secret scan on the agent's push path (output_scanner.py screens tool output, not commits). This breaks bounded blast radius: the blast radius of one pinned asset becomes "the credential is now in git history."

Note git add -u does not stage it when .mcp.json is untracked — so the exposure is conditional on the target repo tracking it. That makes it a narrow window, not a theoretical one, and the agent can also git add <specific files> per its own prompt (prompts/new_task.py:67).

Suggested fix — pick either, both are small:

  1. (preferred, defense in depth) After writing, mark the file so git cannot stage it: git update-index --skip-worktree .mcp.json (add --add / git add --intent-to-add first if untracked). I verified this blocks both git add -u and an explicit git add .mcp.json ("paths … outside of your sparse-checkout definition, so will not be updated in the index"). This mirrors the ADR-016 posture: enforce mechanically, don't rely on the agent's good behavior.
  2. Require indirection at the boundary: have registry-publish.ts::validateRuntime reject literal secrets in headers/args/url (accept only ${ENV_VAR} placeholders, as channel_mcp._jira_server_entry() already does with Bearer ${JIRA_API_TOKEN}), so nothing secret can be published and therefore nothing secret can be written. This is the real fix but it touches #664's handler.

Either way please add a regression test: write a header/api_key-bearing asset, run apply_mcp_assets, then assert git add -u && git diff --cached is empty (or that the value on disk is a placeholder).

Non-blocking nits

  1. loader.py:109 and :123 — docstrings contradict the code they document. :109 says degraded conditions "(empty runtime, malformed existing config), which warn + skip", and :123 lists "an empty / non-dict runtime payload" under fail-closed. The code at :151-154 raises on empty runtime. The class docstring and the apply_mcp_assets list disagree with each other; :123 is the correct one. Same stale phrasing was copied into pipeline.py:1155-1156 ("Degraded-but-safe cases (empty runtime) are warn+skip inside the loader") — that comment is now false. Please align all three; a comment that misstates fail-open/fail-closed is the kind of thing a future reader will trust.
  2. loader.py:158-159 — dead branch. if not written: return [] is unreachable: the loop either appends to written or raises, and the not mcp_assets early-return at :132 already covers the empty case. Harmless, but it implies a skip path that no longer exists (and reinforces nit 1's wrong mental model).
  3. cdk/src/constructs/task-orchestrator.ts:487-497 — IAM resource ARNs use '*' for the registry id when the id is in scope. The statement is guarded by if (props.agentRegistryId) at :479, and registry-api.ts:111-122 scopes the same actions to resourceName: props.agentRegistryId and `${props.agentRegistryId}/record/*`. The orchestrator instead uses '*' and '*/record/*', granting read across every AgentCore registry in the account. The cdk-nag reason at :665 justifies the wildcard as "record ids are server-assigned" — true for the /record/* suffix, but it does not justify wildcarding the registry id, which is known. Swap in props.agentRegistryId to match the sibling construct. I am filing this as a nit rather than blocking because (a) it is a read-only grant on a preview API, and (b) the near-identical '*'-ARN concern is already an open P1 on #664 — fix it in whichever PR you prefer, but please don't let it merge as-is in both.
  4. No cap on total skill prompt_fragment bytes. build_skill_prompt_fragment (loader.py:173-206) concatenates unbounded operator text into the system prompt. Cedar text now has a 64 KB cap (finding 5) and MCP has no size axis, so skills are the remaining unbounded operator-supplied surface — it burns context/cost rather than widening authorization, so it is a cost-bound nit, not a security one. Worth a follow-up cap for symmetry with POLICIES_MAX_BYTES.
  5. stacks/agent.ts:194-207 forkBlueprintRepo — my prior review's nit 4 is now resolved: documented at docs/design/REGISTRY.md §12.1 with both invocation forms and an explicit "those acme/* records must be published first" caveat. Thanks.

Documentation status

Good, and the mirror is correctly synced. docs/design/REGISTRY.md §12.1 (+19) and its Starlight mirror docs/src/content/docs/architecture/Registry.md (+19) were both regenerated — I diffed the new section between source and mirror and they are byte-identical, so the "Fail build on mutation" step will not trip. No hand-edit of docs/src/content/docs/ beyond the generated mirror. The prose does not over-claim: it says the acme/* records are "illustrative, not seeded" and that admission "fails closed on the unresolved pins", which matches what the code actually does.

Tests and CI

  • Agent (ran locally): 226 passed in 1.81s across test_registry_loader.py (25 tests), test_pipeline.py, test_policy_three_outcome.py, test_entrypoint.py. Coverage is genuinely invariant-oriented — the pipeline test asserts agent_ran is False and the FAILED write, rather than snapshotting output.
  • CDK: could not run locally — npx jest dies with TS5103: Invalid value for '--ignoreDeprecations' (stale TypeScript 5.9.3 in my environment vs. cdk/package.json's "typescript": "^6.0.3"). This is my environment, not the PR: it reproduces identically on main. So I read the CDK tests rather than executing them — 10 new blueprint.test.ts cases, 5 new orchestrate-task.test.ts registry cases, and the new registry-orchestrator.test.ts (+141). Marking the CDK run unverified locally; CI covers it.
  • CI at 8362f4fb: build (agentcore) success, Secrets, deps, and workflow scan success, Dead-code detection (advisory) success, Validate PR title success. (Green CI is not why I'd approve; noting it only for completeness.)
  • No CDK test-perf regression (#366): no aws:cdk:bundling-stacks re-enable, no per-test synth introduced.
  • Bootstrap synth coverage (ADR-002 / #350): no update needed in this PR, verified. git diff 4f8da982..HEAD -- cdk/src/bootstrap cdk/bootstrap docs/design/DEPLOYMENT_ROLES.md is empty, and correctly so: #665 introduces no new CloudFormation resource type. Its only infra deltas are one iam.PolicyStatement on an existing Lambda role, one env var, and one conditional Blueprint (an AwsCustomResource type already covered). The bootstrap artifacts (BOOTSTRAP_VERSION, BOOTSTRAP_HASH, bootstrap-template.yaml, policies/*.json, src/bootstrap/policies/*.ts) all moved in the base PR at d3e2b056/4f8da982 where the registry construct + nested stack actually landed — that is the right PR for them. I did not re-audit #664's ARN patterns here.
  • Repo-specific sync checks: cdk/src/handlers/shared/types.ts and cli/src/types.ts are untouched by this PR (git diff 4f8da982..HEAD empty for both), and I confirmed ResolvedAssetTriple is already identical in the two (types.ts:56-60cli/src/types.ts:37-41) — types-sync contract holds. No Cedar engine pin movement (cedar-wasm/cedarpy untouched), so no parity-fixture refresh owed. Solution UA (#319): clean — the diff adds no new XxxClient({}) and no bare boto3.client(...); the only client construction is the pre-existing makeDocClient() at repo-config.ts:1328.

Review agents run

The pr-review-toolkit subagents were not dispatchable in this execution context (I am myself a subagent; nesting is limited to one level and the Agent tool is unavailable — ToolSearch for it returned no match). Per instruction I applied each rubric explicitly and label it rubric-applied, not agent-run:

  • code-reviewer (rubric-applied): change routing correct per AGENTS.md (agent runtime in agent/, orchestration/IAM in cdk/, docs + mirror together). CDK L2/IAM idioms clean; ArnFormat.SLASH_RESOURCE_NAME used correctly. Found nit 3 (registry-id wildcard diverging from the sibling registry-api.ts).
  • silent-failure-hunter (rubric-applied): the primary lens for this re-review. Traced every new error path to a terminal state: loader raise → pipeline.py:1157 (no try/except swallow) → outer except Exception at 1746write_terminal(FAILED)raise. Confirmed the orchestrator resolve path also fails closed (orchestrator.ts:795, empty-cedar_text raise at :845-852). Remaining fail-open surfaces are all pre-existing and intentional (_read_existing_mcp_config warn+treat-as-absent; strip_linear_mcp_servers best-effort). No swallowed failure found in new code — this rubric is what confirms finding 4 is truly fixed.
  • type-design-analyzer (rubric-applied): RegistryAssetLoadError(RuntimeError) is the right granularity — it distinguishes infra failure from degraded-safe, and is narrow enough that the pipeline needs no discriminating except. ResolvedAssetTriple CDK↔CLI parity re-confirmed. resolved_assets: list[dict[str, Any]] (models.py:235) stays an untyped passthrough — pragmatic, but it is why the secret-bearing keys in B1 are invisible to the type system; a typed per-kind runtime model would have surfaced it.
  • comment-analyzer (rubric-applied): found nits 1 and 2. The new comments are unusually good where they explain why (the _server_key injectivity note, the ADR-016 re-strip rationale), but three fail-closed/fail-open descriptions are now stale relative to the code they sit on.
  • pr-test-analyzer (rubric-applied): every one of the 7 fixes has a matching regression test that asserts the intended invariant. Gaps: no test for B1's disk-exposure path, and no test that a skill fragment is bounded (nit 4).
  • security-review skill scope (rubric-applied): in scope (IAM statement, Cedar policy limits, secrets on disk, publish input gateway). Produced B1 and nit 3. Confirmed the ADR-016 re-strip actually closes the registry-Linear bypass, and that the 64 KB cap now genuinely bounds registry Cedar.
  • Omitted: none — the diff touches every rubric's scope.

Human heuristics

  • Proportionality — Pass. Seven findings addressed with ~+120 lines of production code and ~+400 of tests. No speculative abstraction; _to_mcp_config and emptyAssetFields() are each one small single-purpose function. The fixes reuse existing seams (extra_policies cap, emitTaskEvent) rather than inventing parallel machinery.
  • Coherence — Pass. The transporttype mapping now agrees with channel_mcp._jira_server_entry() and the SDK; REMOVE semantics match the SET structure; the registry merge sits in the correct pipeline slot (after clone, after the first strip, before discover_project_config at pipeline.py:1204) and re-applies the ADR-016 strip. One coherence seam: task-orchestrator.ts:487 scopes registry IAM differently from registry-api.ts:111 for the same actions (nit 3).
  • Clarity — Concern. agent/src/registry/loader.py:109, :123, and agent/src/pipeline.py:1155-1156 describe empty-runtime as "warn + skip" while :151-154 raises. The single most important property of this file is which conditions fail the task, and the docstrings currently get it wrong (nits 1-2).
  • Appropriateness — Pass, with one gap. Tests assert intended semantics against the real PolicyEngine and the real .mcp.json consumer shape, not a self-written mock — and the pipeline test verifies the agent never ran, which is the property that actually matters. The gap is that "fail closed" was interpreted purely as availability; confidentiality of the payload the loader now persists to disk was not considered (B1).

To be explicit about what I did not inherit: I did not treat ayushtr-aws' same-day APPROVE as discharging isadeks' change request, and I did not carry forward my own earlier APPROVE. Every disposition above is from reading current code at 8362f4fb. Fix B1 (a two-line git update-index --skip-worktree plus a test is sufficient) and align the three stale comments, and I would approve — the prior-finding work is done and done well. Merge order remains #664#665.

Comment thread agent/src/registry/loader.py
Comment thread agent/src/registry/loader.py Outdated
Comment thread agent/src/registry/loader.py Outdated
Comment thread cdk/src/constructs/task-orchestrator.ts Outdated
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-catalog branch from 4f8da98 to c8e4790 Compare August 12, 2026 01:43
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-integration branch from 8362f4f to be84b37 Compare August 12, 2026 01:43
Kalindi-Dev pushed a commit to Kalindi-Dev/sample-autonomous-cloud-coding-agents that referenced this pull request Aug 12, 2026
…-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.
Kalindi-Dev pushed a commit to Kalindi-Dev/sample-autonomous-cloud-coding-agents that referenced this pull request Aug 12, 2026
…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).
@Kalindi-Dev

Copy link
Copy Markdown
Contributor Author

Thanks @scottschreckengaust — all 7 prior findings you re-verified stay fixed, and this round closes B4 (the .mcp.jsongit add -u → push exfiltration path) with the skip-worktree guard you verified, plus the 3 nits (docstring/comment accuracy, dead branch, orchestrator IAM scoped to the wired registry). Pushed as 0b06ff56; build (agentcore) green. Details inline. Re-review welcome.

theagenticguy
theagenticguy previously approved these changes Aug 12, 2026

@theagenticguy theagenticguy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

Approve. We reviewed head 0b06ff56 against the composed base (feat/246-registry-catalog @ c8e4790c), re-verified every prior blocking finding in current code, and ran both touched test suites locally. All of @isadeks's 7 findings and @scottschreckengaust's B4 secret-exfiltration blocker are genuinely fixed with regression tests that assert the invariant. One narrow residual in the new commit-guard is filed inline as a non-blocking suggestion.

Disposition of prior blocking findings (verified in code at 0b06ff56, not from commit messages)

Finding Status Where verified
isadeks 1 — typed field kind validation Fixed blueprint.ts RegistryRefValidation(field, refs, expectedKind) rejects cross-kind refs at synth; mismatch tests present
isadeks 2 — empty arrays don't detach refs Fixed emptyAssetFields()/buildRemoveClause() emit REMOVE on onUpdate; populated→empty tested
isadeks 3 — non-injective MCP key Fixed _server_key no longer collapses hyphens; collision test present
isadeks 4 — task not failed on unapplied asset Fixed RegistryAssetLoadError raised on missing repo_dir / empty runtime / invalid transport / write error; propagates to write_terminal(FAILED); pipeline test asserts agent_ran is False
isadeks 5 — registry Cedar bypasses 64 KB cap Fixed policy.py counts extra_policies (the path registry cedar arrives on via runner.py:289) in the cap; oversized-via-extra_policies test present
isadeks 6 — deprecation warnings not durable Fixed warnings persisted on the resolved_assets stamp + registry_asset_warning TaskEvent per warned asset
isadeks 7 — transport vs type schema mismatch Fixed _to_mcp_config maps the discriminant and validates shape (http/sse need url, stdio needs command)
scottschreckengaust B4 — .mcp.json secret exfiltration via safety-net commit Fixed _protect_mcp_json_from_commit marks the file skip-worktree (--intent-to-add first when untracked); regression tests cover both the tracked and untracked cases with secret-bearing payloads
scottschreckengaust nits 1–3 (stale docstrings, dead branch, registry-id IAM wildcard) Fixed Docstring/comment now match the fail-closed behavior; dead if not written removed; grant scoped to registry/{agentRegistryId} with only the server-assigned record/* suffix wildcarded, plus a construct test rejecting registry/*

We independently re-probed the B4 mechanics in a scratch repo: with skip-worktree set, git add -u, explicit git add .mcp.json, git add -f, and git commit -a all fail to stage the file, and the flag survives git checkout -b (relevant because reconcile_agent_branch can move the agent to a different branch before ensure_committed runs). The exact chain Scott reproduced (tracked .mcp.jsonadd -u → staged secret) is closed.

Tests and CI

  • Agent (ran locally): 229 passed in 2.23s across test_registry_loader.py, test_pipeline.py, test_policy_three_outcome.py, test_entrypoint.py — includes the two new TestMcpJsonNotCommittable cases.
  • CDK (ran locally): the 4 touched suites (blueprint, task-orchestrator, orchestrate-task, registry-orchestrator) — 211 passed. (The coverage-threshold warning is an artifact of running a subset, not a failure.)
  • CI at 0b06ff56: all four checks green (build (agentcore), secrets/deps scan, dead-code advisory, PR title).

Residual notes (non-blocking)

  1. Commit-guard is best-effort even in the dangerous case — inline comment on loader.py. Narrow window; suggestion is a two-line tightening.
  2. Skill prompt_fragment bytes remain uncapped (Scott's prior nit 4, not addressed this round). Cost-bound, not authorization-bound — fine as the already-noted follow-up.
  3. Repoless boundary observation, no action needed: apply_resolved_assets only runs on the repo path. A repoless task can't reach it today because asset refs live on per-repo Blueprint config, so a repoless task never has resolved_assets with an mcp_server — but if assets ever attach to a non-repo scope, the mcp_server kind would be stamped resolved yet never loaded. Worth a one-line guard or comment in PR 3 territory.

Merge order remains #664#665 (retarget to main after #664 lands).

Comment thread agent/src/registry/loader.py

@scottschreckengaust scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Seven of the eight prior findings are genuinely fixed and locked by regression tests — good, disciplined work. The one P1-security blocker (B4, .mcp.json exfiltration into git) is PARTIALLY FIXED: the symptom on the exact path I reproduced is now blocked, but the capability survives on three concrete paths I reproduced against the real apply_mcp_assets code on this head commit (0b06ff5). The skip-worktree guard is a per-path index flag, and the exfiltration channel is "any tracked path the loader's write reaches, plus any state in which update-index fails" — the fix covers one path in the happy state.

update-index --skip-worktree also introduces a new silent-data-loss failure mode that is unrelated to security (blocking #3 below).

To be explicit about what does not discharge the standing change request: two APPROVEs and fully green CI. No CI check on this repo exercises the symlink or index.lock paths, and TestMcpJsonNotCommittable only covers the two states the fix was written for.


Vision alignment

Aligned with bounded blast radius and the fire-and-forget default: fail-closed resolve, mandatory pins, audit triple on the TaskRecord, ADR-016-style mechanical enforcement rather than prompt-level convention. The re-strip of Linear MCP entries after the registry merge (agent/src/pipeline.py:1172-1181) is exactly the right instinct — a registry-published Linear server would otherwise reintroduce what ADR-016 forbids, and I verified the rewrite does not shake the skip-worktree flag off.

Where it diverges from the tenets: the resolved runtime is still written to disk in cleartext inside a tree the model can Read under bypassPermissions. That is a confidentiality boundary the PR chose not to close, and the durable fix (${ENV_VAR}-placeholder validation at publish) was deferred to a follow-up that has no filed issue. See Documentation.


Status of prior blocking findings

# Finding (file:line then) Status Evidence
1 Wrong-kind ref accepted under a typed assets.* field (blueprint.ts:240) FIXED RegistryRefValidation now takes expectedKind and rejects a kind mismatch at synth — cdk/src/constructs/blueprint.ts:539-561, wired at :261-263. Locked by the per-field mismatch tests in cdk/test/constructs/blueprint.test.ts.
2 Empty asset arrays never REMOVE stale DDB refs (blueprint.ts:397) FIXED emptyAssetFields() / buildRemoveClause() / buildRemoveNames()cdk/src/constructs/blueprint.ts:449-467, spliced into onUpdate at :336-341. Populated→empty transition tested.
3 Non-injective MCP server key collapsed foo-bar/foo_bar (loader.py:35) FIXED Hyphen normalization removed; _server_key returns raw f"{namespace}__{name}"agent/src/registry/loader.py:36-45, with the rationale in-comment. Collision test present.
4 Loader fail-open — result discarded, caller ignored completion (loader.py:148) FIXED RegistryAssetLoadError raised on missing repo_dir (loader.py:184-188), empty/non-dict runtime (:200-203), structurally invalid config (_to_mcp_config, :66-81), write error (:212-213), and blank skill fragment (:244-248); propagated by pipeline.py:1165-1168. Locked end-to-end by agent/tests/test_pipeline.py:168 test_malformed_registry_asset_fails_the_task_closed, which asserts the agent never runs.
5 Registry Cedar bytes bypassed the 64 KiB cap (orchestrator.ts:852) FIXED The cap now counts extra_policiesagent/src/policy.py:882-901. Verified the counted text is the raw operator bytes (pre-synthetic-wrapper), which is the right denominator. Locked by agent/tests/test_policy_three_outcome.py:718.
6 Deprecation warnings not durable (orchestrator.ts:543) FIXED warnings persisted on the resolved_assets triple and a durable registry_asset_warning TaskEvent emitted per warned asset — cdk/src/handlers/shared/orchestrator.ts:797-838.
7 .mcp.json written with transport the SDK does not consume (loader.py:83) FIXED _to_mcp_config maps transport → type and validates shape — agent/src/registry/loader.py:48-86. I confirmed against the installed SDK (claude_agent_sdk/types.py:611-636): McpSSEServerConfig / McpHttpServerConfig are discriminated on type. Tests assert a consumable config.
8 (B4) Unredacted runtime written into the live git tree, exfiltrated by ensure_committed (loader.py:210) PARTIALLY FIXED Symptom blocked on the reproduced path (regular tracked/untracked file, update-index succeeding). Capability retained on three paths — reproduced below against the real loader.
nit Stale "warn + skip" docstring (loader.py:109) FIXED agent/src/registry/loader.py:108-115 now enumerates the fail-closed set and names _read_existing_mcp_config as the sole warn-and-continue case; the stale pipeline.py comment corrected at :1158-1163. All three now agree.
nit Dead if not written: return [] (loader.py:158) FIXED Removed.
nit Registry grant wildcarded the registry id (task-orchestrator.ts:489) FIXED — and genuinely narrower, not widened resourceName: props.agentRegistryId + `${props.agentRegistryId}/record/*`cdk/src/constructs/task-orchestrator.ts:486-500; matches registry-api.ts:111-122. Actions unchanged (GetRegistryRecord, ListRegistryRecords — read-only, no Create*/Update*/Delete* crept in), agentRegistryId threaded from agent.ts:917, nag reason trimmed to match reality. Locked by cdk/test/constructs/task-orchestrator.test.ts:457. I checked specifically for a widened grant: there is none.

I also confirmed @theagenticguy's undocumented property holds: skip-worktree survives git checkout -b, so reconcile_agent_branch's branch -f + checkout does not shake the flag off. Worth a one-line comment.


Blocking issues

1. B4 capability survives: a symlinked .mcp.json puts the secret in git under a different tracked path

agent/src/registry/loader.py:190 computes mcp_path = os.path.join(repo_dir, ".mcp.json") and :209 opens it "w". Python's open(..., "w") follows symlinks. _protect_mcp_json_from_commit then flags the literal pathspec .mcp.json — which is the symlink's index entry, not the entry for the file the bytes actually landed in.

Reproduced with the real loader on this head commit (untrusted repo ships .mcp.json -> config/mcp.json, both tracked — a normal shared-config layout):

$ apply_mcp_assets(repo, [secret_asset])
  TASK Registry: merged 1 MCP server(s) into <repo>/.mcp.json
$ git ls-files -v
  S .mcp.json          <-- guard applied to the symlink
  H config/mcp.json    <-- secret actually lives here, UNGUARDED
$ git status --porcelain   ->  M config/mcp.json
$ git add -u               # == post_hooks.ensure_committed:286
$ git diff --cached | grep -c 'sk-live-abc123\|SUPERSECRET'  ->  2
$ git commit -m "chore(agent): save uncommitted work from session end"
   config/mcp.json | 12 +++++++++++-
>>> SECRET IN GIT HISTORY: True

ensure_pushed (agent/src/post_hooks.py:418-436) then pushes it. Same chain as my original report, identical outcome — only the pathspec moved. The repo layout is attacker-controlled input (the agent clones untrusted repos, including a PR branch at agent/src/repo.py:299), so this is not a hypothetical.

Worse, the same symlink-follow makes the loader an arbitrary-file-write primitive. I verified .mcp.json -> .github/workflows/ci.yml gets the JSON blob written over the workflow (and add -u stages it), and .mcp.json -> .git/config corrupts the repo config so every later git call fails fatal: bad config line 1 in file .git/config.

Suggested fix — before writing, refuse a non-regular target, then resolve and guard the real path:

if os.path.islink(mcp_path):
    raise RegistryAssetLoadError(
        f"refusing to write resolved MCP config through a symlink: {mcp_path}"
    )

(Cheapest and strictest — .mcp.json is a config file the loader owns; a symlink there has no legitimate use. os.open(..., O_NOFOLLOW) is the race-free variant if you prefer.) Regression test: symlink .mcp.json at a second tracked file, apply_mcp_assets, assert it raises and git add -u && git diff --cached is empty.

2. B4 capability survives: the guard fails open on update-index failure, in states the agent reaches routinely

agent/src/registry/loader.py:150-158 logs WARN and continues when update-index --skip-worktree fails. The secret is already on disk at that point, so a failed guard means the full add -u → commit → push chain is live. Two states I reproduced with the real loader:

  • index.lock contention (concurrent git — prek hooks, a parallel agent tool call, a crashed prior git):
    WARN Registry: could not skip-worktree … (exit 128): fatal: Unable to create '…/.git/index.lock': File exists.
    TASK Registry: merged 1 MCP server(s) into …
    # lock released
    $ git add -u  ->  SECRET STAGED: True
    
  • Unmerged index entry for .mcp.json (a conflicted predecessor merge — repo.py:656 does exactly this for merge_branches; --abort at :667 is the clean path but the window exists):
    WARN Registry: could not skip-worktree … (exit 128): fatal: Unable to mark file .mcp.json
    $ git add -u  ->  SECRET STAGED: True
    

@theagenticguy raised this shape as non-blocking; I'm escalating it, because I can now name two reachable triggers rather than a hypothetical. Notably, in both cases the WARN is followed by a cheerful TASK Registry: merged 1 MCP server(s) success line — an operator reading logs sees success.

Suggested fix — fail closed on confidentiality once the bytes are on disk. Verify the flag rather than trusting the exit code, and treat a still-stageable file as fatal:

verify = _git("ls-files", "-v", "--", ".mcp.json").stdout
if not verify.startswith(("S", "h")):
    raise RegistryAssetLoadError(
        f"could not make {mcp_path} unstageable; refusing to run with a "
        f"secret-bearing config in a committable tree"
    )

Checking the observed flag rather than the exit code also covers the symlink case in #1 for free. Regression test: monkeypatch subprocess.run to fail, assert the raise (the existing TestMcpJsonNotCommittable scaffolding makes this cheap).

3. NEW — the guard silently discards the agent's legitimate .mcp.json work, and lies to it about why

skip-worktree makes git ignore all worktree changes to the path, not just the loader's. Once a registry mcp_server asset is pinned, any task whose actual job touches .mcp.json (a very plausible task on this codebase — channel_mcp.py writes it, strip_linear_mcp_servers edits it) has its work silently dropped. Reproduced with the real loader:

# task: "register our new internal MCP server in .mcp.json"
agent: git add .mcp.json   -> exit 1
  "The following paths … matched paths that exist outside of your
   sparse-checkout definition, so will not be updated in the index: .mcp.json"
agent: git commit -am …    -> exit 1  ("nothing to commit, working tree clean")
git status --porcelain     -> ''      (edit is invisible)
newLegit reached HEAD      -> False

Two harms: (a) silent loss of the agent's output while the PR reports success — the same class of failure ensure_committed exists to prevent; (b) the error text names sparse-checkout, which is not what happened, so a model will burn turns chasing a nonexistent sparse config, plausibly tripping stuck_guard.

This is the structural cost of masking a confidentiality problem with a VCS flag. It goes away under the option-2 fix (${ENV_VAR} placeholders at publish, so nothing secret is ever on disk) — which is why I'd rather see that than more hardening on the flag. If you keep the flag, at minimum log a TASK-level line naming .mcp.json as platform-managed-and-uncommittable for this task, so the trajectory records the cause.


Non-blocking suggestions / nits

  1. Still cleartext on disk, readable by the model. Independent of git: the resolved headers/url-token/api_key sit in <repo_dir>/.mcp.json, and the SDK runs bypassPermissions with setting_sources=["project"] (agent/src/runner.py:435, :547), so Read .mcp.json succeeds and the value can reach a PR comment. output_scanner is a partial net at best — I checked the patterns (agent/src/output_scanner.py:33-72): BEARER_TOKEN needs ≥20 token chars (a 14-char sk-live-abc123 slips through), CONNECTION_STRING needs user:pass@ so ?token=… slips through, and bare api_key has no pattern. The SDK's programmatic mcp_servers option (ClaudeAgentOptions.mcp_servers: dict[str, McpServerConfig], already used for the clarification server at runner.py:525-553) would let the registry runtime be passed in-process, never touching disk — that closes #1, #2, and #3 at once and is the fix I'd actually build. Worth costing before adding more flag hardening.
  2. _protect_mcp_json_from_commit takes both repo_dir and mcp_path, but only uses mcp_path for log text and hardcodes the pathspec ".mcp.json". Deriving the pathspec from mcp_path (os.path.relpath) would keep the two in step if the filename is ever parameterized.
  3. Naming: the docstring at :131-133 says "Best-effort: git plumbing failures here are logged, not fatal" — accurate today, but it is the sentence blocking issue #2 asks you to change. If you adopt the fail-closed variant, this docstring and apply_mcp_assets's fail-closed list (:167-173) both need the new condition added, or you reintroduce exactly the three-way docstring disagreement that nit #9 just fixed.
  4. agent/src/stacks/agent.ts:194-208 — the forkBlueprintRepo hook hardcodes three acme/* refs that are documented as not seeded, so enabling it fails every task closed until an operator publishes them. Correct fail-closed behavior and the docs say so; consider making the ref list contextable so the hook is usable without editing the stack.
  5. No cap on the number of asset refs a Blueprint may pin. resolveRegistryAssets (cdk/src/handlers/shared/orchestrator.ts:527-547) resolves them serially with no bound, so N refs = N sequential AgentCore round-trips inside the orchestrator's timeout. The Cedar-bytes cap is enforced, the ref-count one isn't. Low risk (operator-supplied), but a bounded loop is cheap.

Documentation

  • docs/design/REGISTRY.md §12.1 documents the forkBlueprintRepo E2E hook, and the Starlight mirror (docs/src/content/docs/architecture/Registry.md) is regenerated in the same PR with byte-identical content — I diffed both hunks. Mirror-sync is clean; no hand-edit of generated content.
  • Missing: the security posture of .mcp.json under registry assets is undocumented. Once a mechanism exists that makes a repo file platform-managed and uncommittable, that belongs in REGISTRY.md (§ near the loader) and is arguably an ADR-016 sibling — it is the second instance of "enforce mechanically in .mcp.json because a prompt is not a boundary." A future contributor removing the update-index call to fix issue #3 above has nothing telling them it is load-bearing.
  • Missing: the deferred option-2 work (${ENV_VAR}-placeholder validation in registry-publish.ts::validateRuntime) is promised in a PR comment only. Per Stage 4 that needs a filed issue with a priority label — I searched and found none. Please file it (P1, security + registry) and link it from the _protect_mcp_json_from_commit docstring, so the reason this guard is a stopgap is discoverable from the code.

Tests & CI

  • CI fully green on 0b06ff5 (build/agentcore, secrets+deps+workflow scan, PR-title lint, advisory dead-code). I ran uv run pytest tests/test_registry_loader.py in a scratch worktree: 27 passed.
  • Regression coverage for findings 1-7 and the nits is real and asserts behavior, not implementation — test_malformed_registry_asset_fails_the_task_closed (agent/tests/test_pipeline.py:168) driving pipeline.run_task and asserting the agent never runs is the right altitude, and test_registry_extra_policies_counted_in_64kb_cap uses registry-shaped text rather than a synthetic blob.
  • TestMcpJsonNotCommittable (agent/tests/test_registry_loader.py:288-348) is well-constructed but tests only the two states the fix was designed for. Neither the symlink case, the update-index-failure case, nor the legitimate-edit case is covered — which is exactly why green CI here is not evidence the capability is gone. Adding the three tests named in blocking issues #1-#3 is the ask.
  • Bootstrap policy coverage: not applicable. No new CFN resource type. agent.ts:917 threads an existing registry id into an existing construct, and the orchestrator change is an IAM statement narrowing on an existing Lambda role — bedrock-agentcore registry actions came in with #664. BOOTSTRAP_VERSION/artifacts/DEPLOYMENT_ROLES.md correctly untouched.
  • CDK test performance: createStack in task-orchestrator.test.ts was already per-test; the new test reuses it and adds no bundling. No aws:cdk:bundling-stacks re-enable. Clean per #366.
  • #319 solution-UA: clean. No naked client anywhere in the diff (grepped new *Client( / boto3.client( / boto3.resource( across all 21 files — zero hits). The registry client is built through the attributed factory: makeRegistryClient()AgentCoreRegistryClientmakeClient(BedrockAgentCoreControlClient) (cdk/src/handlers/shared/registry/agentcore-client.ts:202).
  • Merge order: this PR is stacked on feat/246-registry-catalog (#664), which has a standing change request. #664 must land first, and the shared code paths (registry-publish.ts::validateRuntime, registry-resolve.ts redaction) are where the durable fix for B4 lives. Please don't let both merge with the on-disk-cleartext posture unresolved in either.

Review agents run

I could not dispatch the pr-review-toolkit agents from this context (a workflow subagent cannot nest a further agent dispatch), so per the review_pr Stage-3 escape hatch I applied each agent's rubric by hand. Naming them explicitly so the gap is auditable:

  • code-reviewer (guidelines/style/routing) — applied. Routing correct per the AGENTS.md table: agent runtime in agent/src/, orchestrator/constructs in cdk/src/, docs + regenerated mirror in docs/. No shared-API-shape change, so no cli/src/types.ts sync obligation. Cedar engine pins untouched (no cedarpy/cedar-wasm lockstep obligation). No hardcoded ARNs/account ids — both registry ARNs go through Stack.of(this).formatArn.
  • silent-failure-hunter — applied, and it is where the surviving blockers came from. Two fail-open catches: loader.py:151-156 (non-zero exit → WARN + continue) and :157-158 (except (OSError, SubprocessError) → WARN + continue), both after the secret is already on disk, both followed by a success log line. The _read_existing_mcp_config warn-and-continue at :103-105 is correctly benign (the file is replaced).
  • type-design-analyzer — applied. New types are proportionate. RegistryAssetLoadError(RuntimeError) is a single well-scoped error class, not a hierarchy. assets?: {mcpServers?, cedarPolicyModules?, skills?} on BlueprintProps (blueprint.ts:182-190) mirrors the existing security/networking/pipeline grouping. One weakness worth naming: resolved_assets: list[dict[str, Any]] (agent/src/models.py:231-235) keeps the runtime bundle untyped end-to-end, so every consumer re-validates shape ad hoc (isinstance(runtime, dict) at loader.py:200, :243). A Pydantic model would move that to the boundary — non-blocking, but it is the reason the loader needs three defensive isinstance checks.
  • comment-analyzer — applied. The prior stale-docstring nit is genuinely fixed and all three sites now agree. New comments are accurate and explain why (the _server_key non-normalization rationale at :39-44 and the cedar-drop-widens-permissions note at orchestrator.ts:836-840 are both the good kind). One comment will become inaccurate if you take fix #2: :131-133's "Best-effort … not fatal" (see nit 3).
  • pr-test-analyzer — applied. See Tests & CI: coverage for findings 1-7 is genuinely locking; B4's coverage is scoped to the states the fix anticipated, which is the AI005 failure mode (tests assert what the code does, not what it should — "cannot be exfiltrated" vs. "this one pathspec is flagged").
  • security-review skill — applied by hand over the IAM + secrets surface: registry grant scoping verified narrowed not widened (read-only actions, registry-scoped ARNs, record/* suffix justified); Cedar aggregate cap verified closed on the extra_policies path; secrets handling is the surviving blocker set above.
  • Omitted: none in scope. No Cedar grammar/parity-fixture change, no network/VPC change, no input-gateway/guardrail change in this diff.

Human heuristics

  • ProportionalityConcern. _protect_mcp_json_from_commit (loader.py:118-158) spends 40 lines and a git-subprocess dependency inside the asset loader to stop a secret from being committed, when the root cause is that a secret is written to disk at all. The SDK's in-process mcp_servers option (runner.py:525-553, already in use) is strictly less machinery and closes all three blockers. This is a mask sized like a mechanism.
  • CoherencePass. Terminology is consistent (kind/namespace/name/version/runtime end-to-end); registry cedar deliberately reuses the existing cedar_policies payload field so the parity contract holds by construction (orchestrator.ts:831-834); IAM scoping now matches its sibling construct instead of diverging. The #246 review breadcrumbs make the fix history legible.
  • ClarityConcern. Names are good, but two error surfaces mislead: the guard logs WARN … could not skip-worktree immediately followed by TASK Registry: merged 1 MCP server(s) (reads as success), and git reports the blocking-issue-#3 case as a sparse-checkout problem, which it isn't.
  • AppropriatenessConcern. AI001: the skip-worktree behavior was verified against real git (good — I re-verified it independently and the tracked-case claim holds), but only in the states the fix anticipated. The adversarial question "what is the next input that defeats this?" has three answers, two of which need no attacker at all — a symlink in an untrusted repo, and a concurrent index.lock.

Comment thread agent/src/registry/loader.py
Comment thread agent/src/registry/loader.py
Comment thread agent/src/registry/loader.py
bgagent added 3 commits August 12, 2026 14:34
Builds on the catalog PR to actually consume registry assets at task time:

- Orchestrator resolve-step (`resolveRegistryAssets`): resolves a blueprint's
  `registry://` mcp_server / cedar_policy_module / skill refs at task start,
  fail-closed; stamps the `{kind,id,version}` triples on the TaskRecord for
  audit, merges resolved cedar_text into `cedar_policies`, and threads the
  runtime bundle into the agent payload.
- Blueprint asset props + onUpdate fix: `assets.{mcpServers,cedarPolicyModules,
  skills}` with `RegistryRefValidation`; the three onUpdate helpers now write
  the asset-ref columns so redeploying an onboarded repo no longer drops them.
- Agent loaders (registry.loader): mcp_server merges into `.mcp.json`;
  cedar_policy_module flows through PolicyEngine's unannotated `extra_policies`;
  skill prompt fragments append to the system prompt (build_skill_prompt_fragment).
- TaskOrchestrator IAM: read-only bedrock-agentcore registry access so the
  orchestrator can resolve refs.

Depends on the catalog PR (feat/246-registry-catalog): imports the RegistryClient
port, ref grammar, and resolver from that branch.
…l-closed load, cap (#246)

Blueprint / orchestrator:
- Validate each typed Blueprint field's ref kind at synth (reject a skill ref
  under assets.mcpServers, etc.) so a field typo can't silently activate a
  different asset class.
- REMOVE asset columns that go empty on update, so a redeploy that cleared the
  last mcp_server/cedar_policy_module/skill actually detaches the stale refs.
- Persist deprecation warnings: stamp them on resolved_assets and emit a durable
  registry_asset_warning TaskEvent (was a Lambda log only).

Agent loader:
- Use an injective MCP server key (drop hyphen->underscore collapse) so
  acme/foo-bar and acme/foo_bar don't clobber each other.
- Normalize the MCP runtime transport -> the SDK's discriminant type key when
  writing .mcp.json, so a published server the docs describe is actually loaded.
- Option C fail-closed: raise RegistryAssetLoadError on infrastructure failures
  (missing repo_dir, .mcp.json write error) so the task fails rather than
  running with a pinned-but-absent asset; warn+skip degraded-but-safe cases;
  return the loaded keys.

Policy:
- Count registry cedar_policy_module bytes (legacy extra_policies path) in the
  64 KiB aggregate cap so a large registry policy can't bypass the bound.

Docs: document the opt-in forkBlueprintRepo E2E hook in REGISTRY.md (+ mirror).
…or IAM (#246)

Third review pass (@scottschreckengaust) on the integration PR:

- B4 (P1 security): the resolved runtime may carry secrets (bearer headers,
  url tokens, --api-key args) and .mcp.json lives in the live git clone, so the
  post-hook `git add -u` → commit → push could exfiltrate it to the PR when the
  target repo tracks .mcp.json. apply_mcp_assets now marks the file
  skip-worktree (intent-to-add first if untracked), blocking both `git add -u`
  and an explicit add. Regression tests assert a secret-bearing asset cannot be
  staged (tracked + untracked cases).
- nit: scope the orchestrator's registry read grant to registry/{agentRegistryId}
  (+ record/* suffix) instead of registry/* — mirrors registry-api.ts; nag reason
  trimmed; test asserts no bare "*"/registry-id wildcard.
- nits: align the RegistryAssetLoadError docstring + pipeline comment with the
  fail-closed behavior (no more stale "warn+skip"); drop the now-dead
  `if not written` branch.
@Kalindi-Dev
Kalindi-Dev force-pushed the feat/246-registry-integration branch from 3aa1fe8 to ee4011f Compare August 12, 2026 18:38
@scottschreckengaust
scottschreckengaust self-requested a review August 12, 2026 18:43
@Kalindi-Dev
Kalindi-Dev enabled auto-merge August 12, 2026 18:56

@scottschreckengaust scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conditional approval with followup items in GitHub

@Kalindi-Dev
Kalindi-Dev added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 864f467 Aug 12, 2026
8 checks passed
@Kalindi-Dev
Kalindi-Dev deleted the feat/246-registry-integration branch August 12, 2026 19:09
euntaek-hong pushed a commit to wrongbutworks/sample-autonomous-cloud-coding-agents that referenced this pull request Aug 13, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants