Skip to content

Commit b3cdf69

Browse files
committed
fix: inject $schema only on project creation, not on every write
- Move $schema injection from writeProjectSpec to createDefaultProjectSpec in both CLI and TUI create flows - Existing projects are never modified — no git churn on upgrade - Custom $schema values are preserved through read-modify-write cycles - Add integ tests verifying injection, preservation, and non-injection
1 parent dad35a2 commit b3cdf69

7 files changed

Lines changed: 112 additions & 59 deletions

File tree

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+
});

src/cli/commands/create/action.ts

Lines changed: 1 addition & 19 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,29 +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-
agents: [],
32-
memories: [],
33-
credentials: [],
34-
evaluators: [],
35-
onlineEvalConfigs: [],
36-
agentCoreGateways: [],
37-
policyEngines: [],
38-
tags: {
39-
'agentcore:created-by': 'agentcore-cli',
40-
'agentcore:project-name': projectName,
41-
},
42-
};
43-
}
44-
4527
function createDefaultDeployedState(): DeployedState {
4628
return { targets: {} };
4729
}

src/cli/project.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
agents: [],
14+
memories: [],
15+
credentials: [],
16+
evaluators: [],
17+
onlineEvalConfigs: [],
18+
agentCoreGateways: [],
19+
policyEngines: [],
20+
tags: {
21+
'agentcore:created-by': 'agentcore-cli',
22+
'agentcore:project-name': projectName,
23+
},
24+
};
25+
}

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

Lines changed: 2 additions & 19 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,24 +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-
tags: {
77-
'agentcore:created-by': 'agentcore-cli',
78-
'agentcore:project-name': projectName,
79-
},
80-
agents: [],
81-
memories: [],
82-
credentials: [],
83-
evaluators: [],
84-
onlineEvalConfigs: [],
85-
agentCoreGateways: [],
86-
policyEngines: [],
87-
};
88-
}
89-
9073
function createDefaultDeployedState(): DeployedState {
9174
return {
9275
targets: {},

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

Lines changed: 1 addition & 15 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,20 +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-
agents: [],
35-
memories: [],
36-
credentials: [],
37-
evaluators: [],
38-
onlineEvalConfigs: [],
39-
agentCoreGateways: [],
40-
policyEngines: [],
41-
};
42-
}
43-
4430
export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowState {
4531
const [phase, setPhase] = useState<RemovePhase>('checking');
4632
const [steps, setSteps] = useState<Step[]>([]);

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ 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+
export function getSchemaUrlForVersion(version: number): string {
25+
return `https://schema.agentcore.aws.dev/v${version}/agentcore.json`;
26+
}
2527

2628
/**
2729
* Manages reading, writing, and validation of AgentCore configuration files
@@ -105,10 +107,7 @@ export class ConfigIO {
105107
*/
106108
async writeProjectSpec(data: AgentCoreProjectSpec): Promise<void> {
107109
const filePath = this.pathResolver.getAgentConfigPath();
108-
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, {
109-
...data,
110-
$schema: SCHEMA_URL,
111-
});
110+
await this.validateAndWrite(filePath, 'AgentCore Project Config', AgentCoreProjectSpecSchema, data);
112111
}
113112

114113
/**

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)