Skip to content

Commit a2c7014

Browse files
committed
fix: make JSON schema default-field filter recursive for nested objects
1 parent 35ef27d commit a2c7014

File tree

9 files changed

+164
-83
lines changed

9 files changed

+164
-83
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { createTestProject, runCLI } from '../src/test-utils/index.js';
2+
import type { TestProject } from '../src/test-utils/index.js';
3+
import { readFile, writeFile } from 'node:fs/promises';
4+
import { join } from 'node:path';
5+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
6+
7+
const SCHEMA_URL_PATTERN = /^https:\/\/schema\.agentcore\.aws\.dev\/.+\.json$/;
8+
async function readRawConfig(projectPath: string): Promise<Record<string, unknown>> {
9+
const raw = await readFile(join(projectPath, 'agentcore', 'agentcore.json'), 'utf-8');
10+
return JSON.parse(raw) as Record<string, unknown>;
11+
}
12+
13+
describe('integration: $schema injection in agentcore.json', () => {
14+
let project: TestProject;
15+
16+
beforeAll(async () => {
17+
project = await createTestProject({
18+
language: 'Python',
19+
framework: 'Strands',
20+
modelProvider: 'Bedrock',
21+
memory: 'none',
22+
});
23+
});
24+
25+
afterAll(async () => {
26+
await project.cleanup();
27+
});
28+
29+
it('new project has $schema set to the official URL as the first key', async () => {
30+
const config = await readRawConfig(project.projectPath);
31+
expect(config.$schema).toMatch(SCHEMA_URL_PATTERN);
32+
expect(Object.keys(config)[0]).toBe('$schema');
33+
});
34+
35+
it('$schema persists after adding a resource', async () => {
36+
const memName = `SchemaMem${Date.now().toString().slice(-6)}`;
37+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
38+
39+
const config = await readRawConfig(project.projectPath);
40+
expect(config.$schema).toMatch(SCHEMA_URL_PATTERN);
41+
42+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
43+
});
44+
45+
it('does not overwrite a custom $schema value', async () => {
46+
const configPath = join(project.projectPath, 'agentcore', 'agentcore.json');
47+
const config = await readRawConfig(project.projectPath);
48+
const customUrl = 'https://example.com/custom-schema.json';
49+
config.$schema = customUrl;
50+
await writeFile(configPath, JSON.stringify(config, null, 2));
51+
52+
const memName = `CustomMem${Date.now().toString().slice(-6)}`;
53+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
54+
55+
const updated = await readRawConfig(project.projectPath);
56+
expect(updated.$schema).toBe(customUrl);
57+
58+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
59+
});
60+
61+
it('does not inject $schema into a pre-existing project that lacks one', async () => {
62+
const configPath = join(project.projectPath, 'agentcore', 'agentcore.json');
63+
const config = await readRawConfig(project.projectPath);
64+
65+
// Simulate an old project by stripping $schema
66+
delete config.$schema;
67+
await writeFile(configPath, JSON.stringify(config, null, 2));
68+
69+
// Trigger a write
70+
const memName = `OldProj${Date.now().toString().slice(-6)}`;
71+
await runCLI(['add', 'memory', '--name', memName, '--json'], project.projectPath);
72+
73+
const updated = await readRawConfig(project.projectPath);
74+
expect(updated.$schema).toBeUndefined();
75+
76+
await runCLI(['remove', 'memory', '--name', memName, '--json'], project.projectPath);
77+
});
78+
});

schemas/agentcore.schema.v1.json

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
"minimum": 1,
1717
"maximum": 9007199254740991
1818
},
19+
"managedBy": {
20+
"default": "CDK",
21+
"type": "string",
22+
"enum": ["CDK"]
23+
},
1924
"tags": {
2025
"type": "object",
2126
"propertyNames": {
@@ -127,7 +132,6 @@
127132
"type": "boolean"
128133
}
129134
},
130-
"required": ["enableOtel"],
131135
"additionalProperties": false
132136
},
133137
"modelProvider": {
@@ -349,7 +353,7 @@
349353
}
350354
}
351355
},
352-
"required": ["type", "name", "eventExpiryDuration", "strategies"],
356+
"required": ["type", "name", "eventExpiryDuration"],
353357
"additionalProperties": false
354358
}
355359
},
@@ -410,7 +414,7 @@
410414
"enum": ["inbound", "outbound"]
411415
}
412416
},
413-
"required": ["type", "name", "vendor"],
417+
"required": ["type", "name"],
414418
"additionalProperties": false
415419
}
416420
]
@@ -767,7 +771,6 @@
767771
"type": "boolean"
768772
}
769773
},
770-
"required": ["enableOtel"],
771774
"additionalProperties": false
772775
},
773776
"networkMode": {
@@ -779,14 +782,7 @@
779782
"type": "string"
780783
}
781784
},
782-
"required": [
783-
"artifact",
784-
"pythonVersion",
785-
"name",
786-
"entrypoint",
787-
"codeLocation",
788-
"networkMode"
789-
],
785+
"required": ["artifact", "pythonVersion", "name", "entrypoint", "codeLocation"],
790786
"additionalProperties": false
791787
},
792788
"iamPolicy": {
@@ -832,7 +828,6 @@
832828
}
833829
}
834830
},
835-
"required": ["type"],
836831
"additionalProperties": false
837832
},
838833
"apiGateway": {
@@ -1107,7 +1102,7 @@
11071102
}
11081103
}
11091104
},
1110-
"required": ["name", "targets", "authorizerType", "enableSemanticSearch", "exceptionLevel"],
1105+
"required": ["name", "targets"],
11111106
"additionalProperties": false
11121107
}
11131108
},
@@ -1206,7 +1201,6 @@
12061201
"type": "boolean"
12071202
}
12081203
},
1209-
"required": ["enableOtel"],
12101204
"additionalProperties": false
12111205
},
12121206
"networkMode": {
@@ -1218,7 +1212,7 @@
12181212
"type": "string"
12191213
}
12201214
},
1221-
"required": ["artifact", "pythonVersion", "name", "entrypoint", "codeLocation", "networkMode"],
1215+
"required": ["artifact", "pythonVersion", "name", "entrypoint", "codeLocation"],
12221216
"additionalProperties": false
12231217
},
12241218
"iamPolicy": {
@@ -1430,7 +1424,6 @@
14301424
"type": "boolean"
14311425
}
14321426
},
1433-
"required": ["enableOtel"],
14341427
"additionalProperties": false
14351428
},
14361429
"networkMode": {
@@ -1442,7 +1435,7 @@
14421435
"type": "string"
14431436
}
14441437
},
1445-
"required": ["artifact", "pythonVersion", "name", "entrypoint", "codeLocation", "networkMode"],
1438+
"required": ["artifact", "pythonVersion", "name", "entrypoint", "codeLocation"],
14461439
"additionalProperties": false
14471440
},
14481441
"iamPolicy": {
@@ -1488,7 +1481,6 @@
14881481
}
14891482
}
14901483
},
1491-
"required": ["type"],
14921484
"additionalProperties": false
14931485
},
14941486
"apiGateway": {
@@ -1690,12 +1682,12 @@
16901682
"enum": ["FAIL_ON_ANY_FINDINGS", "IGNORE_ALL_FINDINGS"]
16911683
}
16921684
},
1693-
"required": ["name", "statement", "validationMode"],
1685+
"required": ["name", "statement"],
16941686
"additionalProperties": false
16951687
}
16961688
}
16971689
},
1698-
"required": ["name", "policies"],
1690+
"required": ["name"],
16991691
"additionalProperties": false
17001692
}
17011693
}

src/cli/commands/create/action.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { APP_DIR, CONFIG_DIR, ConfigIO, setEnvVar, setSessionProjectRoot } from '../../../lib';
22
import type {
3-
AgentCoreProjectSpec,
43
BuildType,
54
DeployedState,
65
ModelProvider,
@@ -19,30 +18,12 @@ import {
1918
} from '../../operations/agent/generate';
2019
import { executeImportAgent } from '../../operations/agent/import';
2120
import { credentialPrimitive } from '../../primitives/registry';
21+
import { createDefaultProjectSpec } from '../../project';
2222
import { CDKRenderer, createRenderer } from '../../templates';
2323
import type { CreateResult } from './types';
2424
import { mkdir } from 'fs/promises';
2525
import { join } from 'path';
2626

27-
function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
28-
return {
29-
name: projectName,
30-
version: 1,
31-
managedBy: 'CDK' as const,
32-
agents: [],
33-
memories: [],
34-
credentials: [],
35-
evaluators: [],
36-
onlineEvalConfigs: [],
37-
agentCoreGateways: [],
38-
policyEngines: [],
39-
tags: {
40-
'agentcore:created-by': 'agentcore-cli',
41-
'agentcore:project-name': projectName,
42-
},
43-
};
44-
}
45-
4627
function createDefaultDeployedState(): DeployedState {
4728
return { targets: {} };
4829
}

src/cli/project.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getSchemaUrlForVersion } from '../lib';
2+
import type { AgentCoreProjectSpec } from '../schema';
3+
import { SCHEMA_VERSION } from './constants';
4+
5+
/**
6+
* Create a default AgentCore project spec with standard defaults.
7+
*/
8+
export function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
9+
return {
10+
$schema: getSchemaUrlForVersion(SCHEMA_VERSION),
11+
name: projectName,
12+
version: SCHEMA_VERSION,
13+
managedBy: 'CDK' as const,
14+
agents: [],
15+
memories: [],
16+
credentials: [],
17+
evaluators: [],
18+
onlineEvalConfigs: [],
19+
agentCoreGateways: [],
20+
policyEngines: [],
21+
tags: {
22+
'agentcore:created-by': 'agentcore-cli',
23+
'agentcore:project-name': projectName,
24+
},
25+
};
26+
}

src/cli/tui/screens/create/useCreateFlow.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { APP_DIR, CONFIG_DIR, ConfigIO, findConfigRoot, setEnvVar, setSessionProjectRoot } from '../../../../lib';
2-
import type { AgentCoreProjectSpec, DeployedState } from '../../../../schema';
2+
import type { DeployedState } from '../../../../schema';
33
import { getErrorMessage } from '../../../errors';
44
import { CreateLogger } from '../../../logging';
55
import { initGitRepo, setupPythonProject, writeEnvFile, writeGitignore } from '../../../operations';
@@ -13,6 +13,7 @@ import { executeImportAgent } from '../../../operations/agent/import';
1313
import { createManagedOAuthCredential } from '../../../primitives/auth-utils';
1414
import { computeDefaultCredentialEnvVarName } from '../../../primitives/credential-utils';
1515
import { credentialPrimitive } from '../../../primitives/registry';
16+
import { createDefaultProjectSpec } from '../../../project';
1617
import { CDKRenderer, createRenderer } from '../../../templates';
1718
import { type Step, areStepsComplete, hasStepError } from '../../components';
1819
import { withMinDuration } from '../../utils';
@@ -69,25 +70,6 @@ function getCreateSteps(projectName: string, agentConfig: AddAgentConfig | null)
6970
return steps;
7071
}
7172

72-
function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
73-
return {
74-
name: projectName,
75-
version: 1,
76-
managedBy: 'CDK' as const,
77-
tags: {
78-
'agentcore:created-by': 'agentcore-cli',
79-
'agentcore:project-name': projectName,
80-
},
81-
agents: [],
82-
memories: [],
83-
credentials: [],
84-
evaluators: [],
85-
onlineEvalConfigs: [],
86-
agentCoreGateways: [],
87-
policyEngines: [],
88-
};
89-
}
90-
9173
function createDefaultDeployedState(): DeployedState {
9274
return {
9375
targets: {},

src/cli/tui/screens/remove/useRemoveFlow.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ConfigIO, getWorkingDirectory } from '../../../../lib';
2-
import type { AgentCoreProjectSpec } from '../../../../schema';
32
import { findStack } from '../../../cloudformation/stack-discovery';
43
import { getErrorMessage } from '../../../errors';
4+
import { createDefaultProjectSpec } from '../../../project';
55
import { type Step, areStepsComplete, hasStepError } from '../../components';
66
import { withMinDuration } from '../../utils';
77
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -27,21 +27,6 @@ function getRemoveSteps(): Step[] {
2727
return [{ label: 'Reset project schemas', status: 'pending' }];
2828
}
2929

30-
function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
31-
return {
32-
name: projectName,
33-
version: 1,
34-
managedBy: 'CDK' as const,
35-
agents: [],
36-
memories: [],
37-
credentials: [],
38-
evaluators: [],
39-
onlineEvalConfigs: [],
40-
agentCoreGateways: [],
41-
policyEngines: [],
42-
};
43-
}
44-
4530
export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowState {
4631
const [phase, setPhase] = useState<RemovePhase>('checking');
4732
const [steps, setSteps] = useState<Step[]>([]);

src/lib/schemas/io/config-io.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import { mkdir, readFile, writeFile } from 'fs/promises';
2121
import { dirname } from 'path';
2222
import { type ZodType } from 'zod';
2323

24-
const SCHEMA_URL = 'https://schema.agentcore.aws.dev/v1/agentcore.json';
24+
/** Supported schema versions. Extend this union as new versions are published. */
25+
type SchemaVersion = 1;
26+
27+
export function getSchemaUrlForVersion(version: SchemaVersion): string {
28+
return `https://schema.agentcore.aws.dev/v${version}/agentcore.json`;
29+
}
2530

2631
/**
2732
* Manages reading, writing, and validation of AgentCore configuration files
@@ -105,10 +110,7 @@ export class ConfigIO {
105110
*/
106111
async writeProjectSpec(data: AgentCoreProjectSpec): Promise<void> {
107112
const filePath = this.pathResolver.getAgentConfigPath();
108-
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, {
109-
...data,
110-
$schema: SCHEMA_URL,
111-
});
113+
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, data);
112114
}
113115

114116
/**

src/lib/schemas/io/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ export {
1010
NoProjectError,
1111
type PathConfig,
1212
} from './path-resolver';
13-
export { ConfigIO, createConfigIO } from './config-io';
13+
export { ConfigIO, createConfigIO, getSchemaUrlForVersion } from './config-io';
1414
export { readCliConfig, type CliConfig } from './cli-config';

0 commit comments

Comments
 (0)