Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .claude/skills/cdk-infrastructure/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ new AlbConstruct(this, 'Alb', { config, vpc: network.vpc });

SSM parameters are published **only for runtime consumption** by ECS tasks and Lambdas — never for CDK-to-CDK references within the same stack.

### Wire a resource's name to every compute that reads it

When a construct exposes a table/bucket that backend code reads via
`os.environ.get("X_NAME", "default")`, you must set `X_NAME` in the container
environment of **every** compute that runs that code — thread the typed ref
through that compute's env builder (e.g. `buildAppApiEnvironment` for app-api,
the inference-agentcore construct's `environment` for inference-api). Wiring one
does **not** wire the other.

**Why this bites (silent 502):** the backend's default fallback hides the
omission. If the env var is missing, the code queries the *default* name
(e.g. `"memory-spaces"` instead of `{prefix}-memory-spaces`), the resource
isn't found, boto3 raises `ResourceNotFoundException`, and the centralized
handler (`apis/shared/security/error_handler.py`) maps it to a generic
**502 `{"detail":"Upstream service error."}`**. Nothing in `cdk synth` or CI
catches it — the stack is valid, the IAM grant may even exist; only a runtime
read fails. (Real instance: PR #588 — memory-spaces names were wired to
inference-api but not app-api, which owns the CRUD routes.)

**Guard it:** add an env-map unit test asserting the key is emitted (see
`test/app-api-environment.test.ts`). **Mind the boundary:** app-api owns
user-facing CRUD; granting IAM or wiring inference-api does not cover it.

## DynamoDB Tables

- Always use PK + SK for flexibility
Expand Down
13 changes: 13 additions & 0 deletions infrastructure/lib/constructs/app-api/app-api-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export interface AppApiSsmParams {
sharedConversationsTableName: string;
sharedConversationsTableArn: string;
memoryId: string;
// Memory Spaces
memorySpacesTableName: string;
memorySpacesBucketName: string;
// Workload identity
workloadIdentityName: string;
}
Expand Down Expand Up @@ -204,6 +207,9 @@ export function resolveAppApiParams(
sharedConversationsTableName: refs.sharedConversationsTable.tableName,
sharedConversationsTableArn: refs.sharedConversationsTable.tableArn,
memoryId: overrides.memoryId,
// Memory Spaces
memorySpacesTableName: refs.memorySpacesTable.tableName,
memorySpacesBucketName: refs.memorySpacesBucket.bucketName,
// Workload identity
workloadIdentityName: refs.platformWorkloadIdentity.name,
};
Expand Down Expand Up @@ -278,7 +284,14 @@ export function buildAppApiEnvironment(
// capability); this only gates feature existence per environment.
SCHEDULED_RUNS_ENABLED: config.scheduledRuns.enabled ? 'true' : 'false',
// Kill switch for the Memory Spaces feature (default off per env).
// The table/bucket names are always wired (the service reads them lazily);
// only MEMORY_SPACES_ENABLED gates whether the routes are mounted. Without
// these two, app-api falls back to the default "memory-spaces" name and
// every read 502s (ResourceNotFoundException). inference-api already sets
// the identical trio — app-api owns the CRUD surface, so it needs them too.
MEMORY_SPACES_ENABLED: config.memorySpaces.enabled ? 'true' : 'false',
DYNAMODB_MEMORY_SPACES_TABLE_NAME: params.memorySpacesTableName,
S3_MEMORY_SPACES_BUCKET_NAME: params.memorySpacesBucketName,
VOICE_TICKET_REPLAY_TABLE_NAME: params.voiceTicketReplayTableName,
VOICE_TICKET_SIGNING_SECRET_ARN: params.voiceTicketSigningSecretArn,
};
Expand Down
43 changes: 43 additions & 0 deletions infrastructure/test/app-api-environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { buildAppApiEnvironment, AppApiSsmParams } from '../lib/constructs/app-api/app-api-environment';
import { createMockConfig } from './helpers/mock-config';

/**
* Guards the Memory Spaces env wiring on app-api. The service reads the table
* and bucket names from `DYNAMODB_MEMORY_SPACES_TABLE_NAME` /
* `S3_MEMORY_SPACES_BUCKET_NAME`; without them app-api falls back to the
* default "memory-spaces" name and every read 502s (ResourceNotFoundException).
* inference-api already sets the identical trio — app-api owns the CRUD
* surface, so it must too.
*/
function stubParams(overrides: Partial<AppApiSsmParams> = {}): AppApiSsmParams {
// Only the fields the env builder reads need real values; the rest are
// filled with placeholders so the type is satisfied.
return {
memorySpacesTableName: 'test-project-memory-spaces',
memorySpacesBucketName: 'test-project-memory-spaces',
...overrides,
} as AppApiSsmParams;
}

describe('buildAppApiEnvironment — Memory Spaces', () => {
it('wires the table and bucket names the service reads', () => {
const env = buildAppApiEnvironment(createMockConfig(), stubParams());

expect(env.DYNAMODB_MEMORY_SPACES_TABLE_NAME).toBe('test-project-memory-spaces');
expect(env.S3_MEMORY_SPACES_BUCKET_NAME).toBe('test-project-memory-spaces');
});

it('gates only feature existence on the flag, always wiring the names', () => {
const off = buildAppApiEnvironment(createMockConfig(), stubParams());
expect(off.MEMORY_SPACES_ENABLED).toBe('false');
// Names are present even with the kill switch off — the service reads them
// lazily, so a later flip to enabled needs no env change.
expect(off.DYNAMODB_MEMORY_SPACES_TABLE_NAME).toBe('test-project-memory-spaces');

const on = buildAppApiEnvironment(
createMockConfig({ memorySpaces: { enabled: true } }),
stubParams(),
);
expect(on.MEMORY_SPACES_ENABLED).toBe('true');
});
});