Skip to content

Commit 1539958

Browse files
author
Oliver Gibbs
committed
refactor(backend): move agent registry domain to a dedicated stack
Relocates the agent registry domain — the 28-field agent-record resolver covering app lifecycle, API keys, access control, and metrics, plus agent import, registry sync, agent code, and fabricator request handling — 97 resources in all — into a new registry stack attached to the same GraphQL API. The backend stack drops from 375 to 278 resources, 44 percent below the CloudFormation ceiling. All seven restructuring gates pass across both relocated domains: template diffs are removals-only, all 39 stateful resources keep their logical ids and policies, every schema field stays attached exactly once, and relocated resolver templates byte-match their baselines. The agent-import role's Secrets Manager and STS statements are proven byte-identical to baseline by dedicated equality tests, not merely subset-equal. App publish and unpublish resolvers stay in the backend stack because they target a gateway-owned function; the agent-config resolver and gateway-registration handler are documented deferrals for a later batch.
1 parent 0dc152a commit 1539958

12 files changed

Lines changed: 2649 additions & 1489 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Service (AgentCore Runtime · Gateway · Knowledge Base)
9696
Gateway (per-app API Gateway · authorizer · metrics)
9797
```
9898

99-
Deployed as focused CDK stacks: `backend`, `projects`, `services`, `governance`, `arbiter`, `frontend`, `gateway`, and `telemetry`.
99+
Deployed as focused CDK stacks: `backend`, `projects`, `registry`, `services`, `governance`, `arbiter`, `frontend`, `gateway`, and `telemetry`.
100100

101101
## Getting started
102102

backend/bin/app.ts

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GatewayStack } from "../lib/gateway-stack";
1111
import { GovernanceStack } from "../lib/governance-stack";
1212
import { TelemetryStack } from "../lib/telemetry-stack";
1313
import { ProjectsStack } from "../lib/projects-stack";
14+
import { RegistryStack } from "../lib/registry-stack";
1415

1516
const app = new cdk.App();
1617

@@ -64,6 +65,39 @@ const projectsStack = new ProjectsStack(
6465
);
6566
projectsStack.addDependency(backendStack);
6667

68+
// Registry satellite stack — backend-stack-split phase 2 (decision 30e6d067).
69+
// Owns the registry/agent-import/fabricator-request/fabricator-queue/
70+
// fabrication-event/app-CRUD-and-api-key domain, moved out of BackendStack.
71+
// Depends on backend only, for the shared API/bus/tables/registry/pool/
72+
// codeBucket passed in as props; resolvers attach to BackendStack's AppSync
73+
// API via the same L1 cross-stack pattern as ProjectsStack. Instantiated
74+
// before ServicesStack/GovernanceStack — no dependency on either — and
75+
// before GatewayStack, which it does NOT depend on: the publishApp/
76+
// unpublishApp resolvers (targeting a GatewayStack-owned function) stay in
77+
// BackendStack (documented exclusion in registry-stack.ts), so no
78+
// RegistryStack -> GatewayStack edge is introduced and ordering vs
79+
// GatewayStack is a non-issue.
80+
const registryStack = new RegistryStack(
81+
app,
82+
`citadel-registry-${environment}`,
83+
{
84+
...stackProps,
85+
description: `Registry domain satellite for Citadel - ${environment}`,
86+
appSyncApi: backendStack.appSyncApi,
87+
agentEventBus: backendStack.agentEventBus,
88+
appsTable: backendStack.appsTable,
89+
workflowsTable: backendStack.workflowsTable,
90+
agentConfigTable: backendStack.agentConfigTable,
91+
modelCatalogTable: backendStack.modelCatalogTable,
92+
idempotencyTable: backendStack.idempotencyTable,
93+
userPool: backendStack.userPool,
94+
registryArn: backendStack.registryArn,
95+
registryId: backendStack.registryId,
96+
adrsTable: backendStack.adrsTable,
97+
},
98+
);
99+
registryStack.addDependency(backendStack);
100+
67101
// Services stack (depends on backend)
68102
const servicesStack = new ServicesStack(
69103
app,
@@ -361,6 +395,7 @@ if (app.node.tryGetContext("nag") !== "false") {
361395
for (const stack of [
362396
backendStack,
363397
projectsStack,
398+
registryStack,
364399
servicesStack,
365400
arbiterStack,
366401
frontendStack,
@@ -519,7 +554,7 @@ if (app.node.tryGetContext("nag") !== "false") {
519554
"RegistryProvisionerFunction/ServiceRole/DefaultPolicy/Resource",
520555
],
521556
[
522-
backendStack,
557+
registryStack,
523558
"RegistryAgentRecordResolverFunction/ServiceRole/DefaultPolicy/Resource",
524559
],
525560
];
@@ -634,8 +669,8 @@ if (app.node.tryGetContext("nag") !== "false") {
634669

635670
// SQS3 — RegistrySyncDLQ is itself a dead-letter queue; adding a DLQ to a DLQ is unnecessary.
636671
NagSuppressions.addResourceSuppressionsByPath(
637-
backendStack,
638-
`/${backendStack.stackName}/RegistrySyncDLQ/Resource`,
672+
registryStack,
673+
`/${registryStack.stackName}/RegistrySyncDLQ/Resource`,
639674
[
640675
{
641676
id: "AwsSolutions-SQS3",
@@ -673,13 +708,13 @@ if (app.node.tryGetContext("nag") !== "false") {
673708
},
674709
];
675710
const registryArnPaths: Array<[cdk.Stack, string]> = [
676-
[backendStack, "RegistrySyncLambda/ServiceRole/DefaultPolicy/Resource"],
711+
[registryStack, "RegistrySyncLambda/ServiceRole/DefaultPolicy/Resource"],
677712
[
678713
backendStack,
679714
"AgentConfigResolverFunction/ServiceRole/DefaultPolicy/Resource",
680715
],
681716
[
682-
backendStack,
717+
registryStack,
683718
"AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource",
684719
],
685720
[
@@ -691,7 +726,7 @@ if (app.node.tryGetContext("nag") !== "false") {
691726
[arbiterStack, "WorkerAgentWrapper/ServiceRole/DefaultPolicy/Resource"],
692727
[arbiterStack, "GovernanceUiResolverFn/ServiceRole/DefaultPolicy/Resource"],
693728
[
694-
backendStack,
729+
registryStack,
695730
"RegistryAgentRecordResolverFunction/ServiceRole/DefaultPolicy/Resource",
696731
],
697732
[
@@ -703,7 +738,7 @@ if (app.node.tryGetContext("nag") !== "false") {
703738
// (GetRegistryRecord + UpdateRegistryRecord), scoped to the
704739
// registry ARN + its records (<AgentCoreRegistry.RegistryArn>/*).
705740
[
706-
backendStack,
741+
registryStack,
707742
"AgentImportManifestResultHandler/ServiceRole/DefaultPolicy/Resource",
708743
],
709744
[
@@ -733,8 +768,8 @@ if (app.node.tryGetContext("nag") !== "false") {
733768
// Additive to the registry-ARN suppression already registered for
734769
// this role above.
735770
NagSuppressions.addResourceSuppressionsByPath(
736-
backendStack,
737-
`/${backendStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
771+
registryStack,
772+
`/${registryStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
738773
[
739774
{
740775
id: "AwsSolutions-IAM5",
@@ -753,8 +788,8 @@ if (app.node.tryGetContext("nag") !== "false") {
753788
// resource-level scoping, so they require Resource '*'. Additive to
754789
// the discovery suppression registered just above; read-only only.
755790
NagSuppressions.addResourceSuppressionsByPath(
756-
backendStack,
757-
`/${backendStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
791+
registryStack,
792+
`/${registryStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
758793
[
759794
{
760795
id: "AwsSolutions-IAM5",
@@ -773,8 +808,8 @@ if (app.node.tryGetContext("nag") !== "false") {
773808
// wildcards are already covered by the stack-level IAM5 suppression.
774809
// Additive to the discovery suppression registered just above.
775810
NagSuppressions.addResourceSuppressionsByPath(
776-
backendStack,
777-
`/${backendStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
811+
registryStack,
812+
`/${registryStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
778813
[
779814
{
780815
id: "AwsSolutions-IAM5",
@@ -804,8 +839,8 @@ if (app.node.tryGetContext("nag") !== "false") {
804839
// the discovery + test-invoke suppressions already registered for
805840
// this role above.
806841
NagSuppressions.addResourceSuppressionsByPath(
807-
backendStack,
808-
`/${backendStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
842+
registryStack,
843+
`/${registryStack.stackName}/AgentImportResolverFunction/ServiceRole/DefaultPolicy/Resource`,
809844
[
810845
{
811846
id: "AwsSolutions-IAM5",
@@ -816,4 +851,31 @@ if (app.node.tryGetContext("nag") !== "false") {
816851
},
817852
],
818853
);
854+
855+
// IAM5 — AgentCodeResolverFunction's S3 grant on the code bucket's agents/*
856+
// prefix (read/write/list agent code artifacts). The base bucket ARN grant
857+
// (list) plus the agents/* prefix grant (get/put/version) is the same
858+
// narrowing pattern the generic appLambdaSuppressions S3-prefix regex
859+
// (`Resource::arn:aws:s3:::.+\/\*$`) already covers for LITERAL ARN
860+
// strings — this one is a CDK token reference (`<CodeBucketFF4C7AD6.Arn>
861+
// /agents/*`), which the literal-string regex does not match. Same IAM5
862+
// category, same narrowing rationale (prefix-scoped, not bucket-wide *).
863+
NagSuppressions.addResourceSuppressionsByPath(
864+
registryStack,
865+
`/${registryStack.stackName}/AgentCodeResolverFunction/ServiceRole/DefaultPolicy/Resource`,
866+
[
867+
{
868+
id: "AwsSolutions-IAM5",
869+
reason:
870+
"S3 grant scoped to the code bucket's agents/* prefix (CDK token " +
871+
"resource, not a literal ARN string) for get/put/version/list of " +
872+
"agent code artifacts. Narrow prefix, not bucket-wide wildcard.",
873+
appliesTo: [
874+
{
875+
regex: "/^Resource::<CodeBucket[A-F0-9]+\\.Arn>\\/agents\\/\\*$/g",
876+
},
877+
],
878+
},
879+
],
880+
);
819881
}

0 commit comments

Comments
 (0)