Skip to content

Commit 10a7dcb

Browse files
committed
feat: add resource tagging support
Add `agentcore tag` command for managing AWS resource tags on agents, memories, and gateways. Supports project-level default tags (inherited by all resources) and per-resource tag overrides. - Add TagsSchema to project, agent, memory, and gateway schemas - Add `tag list|add|remove|set-defaults|remove-defaults` subcommands - Auto-tag new projects with `agentcore:created-by` and `agentcore:project-name` - Add unit tests for schema validation and tag actions - Add integration tests for tag command round-trip
1 parent 5c8d1b4 commit 10a7dcb

21 files changed

Lines changed: 767 additions & 6 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Note: CDK L3 constructs are in a separate package `@aws/agentcore-cdk`.
3131
- `dev` - Local development server (CodeZip: uvicorn with hot-reload; Container: Docker build + run with volume mount)
3232
- `invoke` - Invoke agents (local or deployed)
3333
- `package` - Package agent artifacts without deploying (zip for CodeZip, container image build for Container)
34+
- `tag` - Manage resource tags (list, add, remove, set-defaults, remove-defaults)
3435
- `validate` - Validate configuration files
3536
- `update` - Check for CLI updates
3637
- `help` - Display help information

docs/commands.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,46 @@ agentcore remove all --dry-run # Preview
333333

334334
---
335335

336+
## Tagging
337+
338+
### tag
339+
340+
Manage AWS resource tags on your AgentCore project. Tags are applied to deployed CloudFormation resources (agents,
341+
memories, gateways). Credentials are not taggable since they're deployed via the AgentCore Identity API.
342+
343+
```bash
344+
# List all tags (project defaults + per-resource)
345+
agentcore tag list
346+
agentcore tag list --json
347+
agentcore tag list --resource agent:MyAgent
348+
349+
# Add a tag to a specific resource
350+
agentcore tag add --resource agent:MyAgent --key environment --value prod
351+
352+
# Remove a tag from a resource
353+
agentcore tag remove --resource agent:MyAgent --key environment
354+
355+
# Set a project-level default tag (inherited by all resources)
356+
agentcore tag set-defaults --key team --value platform
357+
358+
# Remove a project-level default tag
359+
agentcore tag remove-defaults --key team
360+
```
361+
362+
Resource references use `type:name` format. Taggable types: `agent`, `memory`, `gateway`.
363+
364+
Per-resource tags override project-level defaults when keys conflict. Projects created with the CLI include
365+
`agentcore:created-by` and `agentcore:project-name` as defaults.
366+
367+
| Flag | Description |
368+
| ------------------ | ------------------------------ |
369+
| `--resource <ref>` | Resource reference (type:name) |
370+
| `--key <key>` | Tag key (max 128 chars) |
371+
| `--value <value>` | Tag value (max 256 chars) |
372+
| `--json` | JSON output |
373+
374+
---
375+
336376
## Development
337377

338378
### dev

docs/configuration.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ Main project configuration using a **flat resource model**. Agents, memories, an
2222
{
2323
"name": "MyProject",
2424
"version": 1,
25+
"tags": {
26+
"agentcore:created-by": "agentcore-cli",
27+
"agentcore:project-name": "MyProject",
28+
"environment": "dev"
29+
},
2530
"agents": [
2631
{
2732
"type": "AgentCoreRuntime",
@@ -61,6 +66,7 @@ Main project configuration using a **flat resource model**. Agents, memories, an
6166
| ------------- | -------- | ----------------------------------------------------------- |
6267
| `name` | Yes | Project name (1-23 chars, alphanumeric, starts with letter) |
6368
| `version` | Yes | Schema version (integer, currently `1`) |
69+
| `tags` | No | Project-level default tags (inherited by all resources) |
6470
| `agents` | Yes | Array of agent specifications |
6571
| `memories` | Yes | Array of memory resources |
6672
| `credentials` | Yes | Array of credential providers (API key or OAuth) |
@@ -98,6 +104,7 @@ Main project configuration using a **flat resource model**. Agents, memories, an
98104
| `networkMode` | No | `"PUBLIC"` (default) or `"PRIVATE"` |
99105
| `envVars` | No | Custom environment variables |
100106
| `instrumentation` | No | OpenTelemetry settings |
107+
| `tags` | No | Per-agent tags (override project defaults) |
101108

102109
### Runtime Versions
103110

@@ -121,12 +128,13 @@ Main project configuration using a **flat resource model**. Agents, memories, an
121128
}
122129
```
123130

124-
| Field | Required | Description |
125-
| --------------------- | -------- | --------------------------------------- |
126-
| `type` | Yes | Always `"AgentCoreMemory"` |
127-
| `name` | Yes | Memory name (1-48 chars) |
128-
| `eventExpiryDuration` | Yes | Days until events expire (7-365) |
129-
| `strategies` | Yes | Array of memory strategies (at least 1) |
131+
| Field | Required | Description |
132+
| --------------------- | -------- | ------------------------------------------- |
133+
| `type` | Yes | Always `"AgentCoreMemory"` |
134+
| `name` | Yes | Memory name (1-48 chars) |
135+
| `eventExpiryDuration` | Yes | Days until events expire (7-365) |
136+
| `strategies` | Yes | Array of memory strategies (at least 1) |
137+
| `tags` | No | Per-memory tags (override project defaults) |
130138

131139
### Memory Strategies
132140

@@ -231,6 +239,7 @@ Gateway and MCP tool configuration. Gateways, their targets, and standalone MCP
231239
| `targets` | Yes | Array of gateway targets |
232240
| `authorizerType` | No | `"NONE"` (default), `"AWS_IAM"`, or `"CUSTOM_JWT"` |
233241
| `authorizerConfiguration` | No | Required when `authorizerType` is `"CUSTOM_JWT"` (see below) |
242+
| `tags` | No | Per-gateway tags (override project defaults) |
234243

235244
### CUSTOM_JWT Authorizer Configuration
236245

integ-tests/tag.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createTestProject, runCLI } from '../src/test-utils/index.js';
2+
import type { TestProject } from '../src/test-utils/index.js';
3+
import { readFile } from 'node:fs/promises';
4+
import { join } from 'node:path';
5+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
6+
7+
describe('integration: tag command', () => {
8+
let project: TestProject;
9+
10+
beforeAll(async () => {
11+
project = await createTestProject({
12+
language: 'Python',
13+
framework: 'Strands',
14+
modelProvider: 'Bedrock',
15+
memory: 'none',
16+
});
17+
});
18+
19+
afterAll(async () => {
20+
await project.cleanup();
21+
});
22+
23+
it('creates project with auto-tags in agentcore.json', async () => {
24+
const specPath = join(project.projectPath, 'agentcore', 'agentcore.json');
25+
const spec = JSON.parse(await readFile(specPath, 'utf-8'));
26+
expect(spec.tags).toEqual({
27+
'agentcore:created-by': 'agentcore-cli',
28+
'agentcore:project-name': expect.any(String),
29+
});
30+
});
31+
32+
it('set-defaults adds a project-level tag', async () => {
33+
const result = await runCLI(
34+
['tag', 'set-defaults', '--key', 'environment', '--value', 'dev', '--json'],
35+
project.projectPath
36+
);
37+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
38+
const output = JSON.parse(result.stdout);
39+
expect(output.success).toBe(true);
40+
41+
// Verify in agentcore.json
42+
const specPath = join(project.projectPath, 'agentcore', 'agentcore.json');
43+
const spec = JSON.parse(await readFile(specPath, 'utf-8'));
44+
expect(spec.tags.environment).toBe('dev');
45+
});
46+
47+
it('tag add sets a per-resource tag', async () => {
48+
// Get the agent name from spec
49+
const specPath = join(project.projectPath, 'agentcore', 'agentcore.json');
50+
const spec = JSON.parse(await readFile(specPath, 'utf-8'));
51+
const agentName = spec.agents[0]?.name;
52+
if (!agentName) return; // Skip if no agent
53+
54+
const result = await runCLI(
55+
['tag', 'add', '--resource', `agent:${agentName}`, '--key', 'cost-center', '--value', '12345', '--json'],
56+
project.projectPath
57+
);
58+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
59+
60+
// Verify in agentcore.json
61+
const updatedSpec = JSON.parse(await readFile(specPath, 'utf-8'));
62+
expect(updatedSpec.agents[0].tags).toEqual({ 'cost-center': '12345' });
63+
});
64+
65+
it('tag list returns JSON output', async () => {
66+
const result = await runCLI(['tag', 'list', '--json'], project.projectPath);
67+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
68+
const output = JSON.parse(result.stdout);
69+
expect(output.projectDefaults).toBeDefined();
70+
expect(output.resources).toBeInstanceOf(Array);
71+
});
72+
73+
it('tag remove removes a per-resource tag', async () => {
74+
const specPath = join(project.projectPath, 'agentcore', 'agentcore.json');
75+
const spec = JSON.parse(await readFile(specPath, 'utf-8'));
76+
const agentName = spec.agents[0]?.name;
77+
if (!agentName || !spec.agents[0].tags?.['cost-center']) return;
78+
79+
const result = await runCLI(
80+
['tag', 'remove', '--resource', `agent:${agentName}`, '--key', 'cost-center', '--json'],
81+
project.projectPath
82+
);
83+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
84+
85+
const updatedSpec = JSON.parse(await readFile(specPath, 'utf-8'));
86+
expect(updatedSpec.agents[0].tags).toBeUndefined();
87+
});
88+
89+
it('remove-defaults removes a project-level tag', async () => {
90+
const result = await runCLI(['tag', 'remove-defaults', '--key', 'environment', '--json'], project.projectPath);
91+
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
92+
93+
const specPath = join(project.projectPath, 'agentcore', 'agentcore.json');
94+
const spec = JSON.parse(await readFile(specPath, 'utf-8'));
95+
expect(spec.tags.environment).toBeUndefined();
96+
});
97+
});

src/cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { registerLogs } from './commands/logs';
88
import { registerPackage } from './commands/package';
99
import { registerRemove } from './commands/remove';
1010
import { registerStatus } from './commands/status';
11+
import { registerTag } from './commands/tag';
1112
import { registerTraces } from './commands/traces';
1213
import { registerUpdate } from './commands/update';
1314
import { registerValidate } from './commands/validate';
@@ -136,6 +137,7 @@ export function registerCommands(program: Command) {
136137
registerPackage(program);
137138
const removeCmd = registerRemove(program);
138139
registerStatus(program);
140+
registerTag(program);
139141
registerTraces(program);
140142
registerUpdate(program);
141143
registerValidate(program);

src/cli/commands/create/action.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
2727
return {
2828
name: projectName,
2929
version: 1,
30+
tags: {
31+
'agentcore:created-by': 'agentcore-cli',
32+
'agentcore:project-name': projectName,
33+
},
3034
agents: [],
3135
memories: [],
3236
credentials: [],

0 commit comments

Comments
 (0)