@intentius/chant-lexicon-aws 0.44.13 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentcore/trace-fetch.d.ts +4 -1
- package/dist/agentcore/trace-fetch.d.ts.map +1 -1
- package/dist/api/read-client.d.ts +31 -1
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts +9 -0
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +33 -19
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/index.d.ts +1 -1
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +21 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/aws-apply.d.ts +14 -4
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/properties.d.ts +4 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/spec/fetch.d.ts +12 -1
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.test.ts +17 -0
- package/src/agentcore/trace-fetch.ts +7 -2
- package/src/api/read-client.test.ts +87 -0
- package/src/api/read-client.ts +57 -1
- package/src/codegen/docs-links.test.ts +44 -30
- package/src/codegen/docs.ts +2 -1034
- package/src/components/capability-plugin.ts +5 -2
- package/src/components/cloud-executor.test.ts +29 -1
- package/src/components/cloud-executor.ts +23 -5
- package/src/composites/agentcore-agent.test.ts +32 -12
- package/src/composites/agentcore-agent.ts +43 -23
- package/src/composites/index.ts +1 -1
- package/src/composites/microvm-app.test.ts +2 -2
- package/src/deep-observe.test.ts +94 -0
- package/src/deep-observe.ts +58 -2
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +3 -1
- package/src/lifecycle-integration.test.ts +89 -0
- package/src/op/activities/aws-apply.test.ts +40 -4
- package/src/op/activities/aws-apply.ts +23 -7
- package/src/plugin.ts +34 -35
- package/src/properties.test.ts +5 -5
- package/src/properties.ts +6 -5
- package/src/serializer.test.ts +73 -33
- package/src/spec/fetch.test.ts +40 -0
- package/src/spec/fetch.ts +24 -3
|
@@ -69,8 +69,11 @@ export const awsCapabilityPlugin: CapabilityPlugin = {
|
|
|
69
69
|
name: "aws",
|
|
70
70
|
// The lexicon package's own version (#1505) — the "1.0.0" this shipped with
|
|
71
71
|
// was the starter plugin's literal, copied along in the #681 extraction and
|
|
72
|
-
// never a real package version.
|
|
73
|
-
|
|
72
|
+
// never a real package version. A getter, so the package.json read happens
|
|
73
|
+
// on first access rather than at import time.
|
|
74
|
+
get version(): string {
|
|
75
|
+
return ownPackageVersion(import.meta.url);
|
|
76
|
+
},
|
|
74
77
|
capabilities: awsCapabilities,
|
|
75
78
|
families: () => AWS_VERB_FAMILIES,
|
|
76
79
|
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyAwsEndpoint,
|
|
4
|
+
applyAwsEndpointArgv,
|
|
5
|
+
awsDeployCapabilities,
|
|
6
|
+
awsDeployCapabilitiesForBody,
|
|
7
|
+
awsDeployCapabilityList,
|
|
8
|
+
ecsDeploymentId,
|
|
9
|
+
ecsServiceStable,
|
|
10
|
+
} from "./cloud-executor";
|
|
3
11
|
|
|
4
12
|
describe("ecsDeploymentId / ecsServiceStable — tolerate Floci's missing deployments (#937)", () => {
|
|
5
13
|
test("deployment id: real AWS shape", () => {
|
|
@@ -74,4 +82,24 @@ describe("awsDeployCapabilities — CAPABILITY_AUTO_EXPAND for Transform macros"
|
|
|
74
82
|
);
|
|
75
83
|
expect(awsDeployCapabilities({ Transform: ["AWS::LanguageExtensions"] })).toContain("CAPABILITY_AUTO_EXPAND");
|
|
76
84
|
});
|
|
85
|
+
test("list form mirrors the string form", () => {
|
|
86
|
+
expect(awsDeployCapabilityList({})).toEqual(["CAPABILITY_NAMED_IAM"]);
|
|
87
|
+
expect(awsDeployCapabilityList({ Transform: "AWS::Serverless-2016-10-31" })).toEqual([
|
|
88
|
+
"CAPABILITY_NAMED_IAM",
|
|
89
|
+
"CAPABILITY_AUTO_EXPAND",
|
|
90
|
+
]);
|
|
91
|
+
expect(awsDeployCapabilityList({ Transform: ["AWS::LanguageExtensions", "AWS::Serverless-2016-10-31"] })).toEqual([
|
|
92
|
+
"CAPABILITY_NAMED_IAM",
|
|
93
|
+
"CAPABILITY_AUTO_EXPAND",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
test("raw body: parses JSON, falls back to NAMED_IAM for non-JSON or empty input", () => {
|
|
97
|
+
expect(awsDeployCapabilitiesForBody(JSON.stringify({ Transform: "AWS::SecretsManager-2020-07-23", Resources: {} }))).toEqual([
|
|
98
|
+
"CAPABILITY_NAMED_IAM",
|
|
99
|
+
"CAPABILITY_AUTO_EXPAND",
|
|
100
|
+
]);
|
|
101
|
+
expect(awsDeployCapabilitiesForBody(JSON.stringify({ Resources: {} }))).toEqual(["CAPABILITY_NAMED_IAM"]);
|
|
102
|
+
expect(awsDeployCapabilitiesForBody("Resources:\n B:\n Type: AWS::S3::Bucket\n")).toEqual(["CAPABILITY_NAMED_IAM"]);
|
|
103
|
+
expect(awsDeployCapabilitiesForBody("")).toEqual(["CAPABILITY_NAMED_IAM"]);
|
|
104
|
+
});
|
|
77
105
|
});
|
|
@@ -393,9 +393,26 @@ async function cfnChangeSetType(stackName: string): Promise<"CREATE" | "UPDATE">
|
|
|
393
393
|
* CloudFormation refuses without the acknowledgement.
|
|
394
394
|
*/
|
|
395
395
|
export function awsDeployCapabilities(template: { Transform?: unknown }): string {
|
|
396
|
+
return awsDeployCapabilityList(template).join(" ");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** {@link awsDeployCapabilities} as a list, for the CFN API's `Capabilities.member.N` params. */
|
|
400
|
+
export function awsDeployCapabilityList(template: { Transform?: unknown }): string[] {
|
|
396
401
|
return template.Transform !== undefined
|
|
397
|
-
? "CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND"
|
|
398
|
-
: "CAPABILITY_NAMED_IAM";
|
|
402
|
+
? ["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
403
|
+
: ["CAPABILITY_NAMED_IAM"];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* {@link awsDeployCapabilityList} for a raw template body. A body that isn't
|
|
408
|
+
* JSON gets the default list; the deploy itself reports the real problem.
|
|
409
|
+
*/
|
|
410
|
+
export function awsDeployCapabilitiesForBody(body: string): string[] {
|
|
411
|
+
try {
|
|
412
|
+
return awsDeployCapabilityList(JSON.parse(body) as { Transform?: unknown });
|
|
413
|
+
} catch {
|
|
414
|
+
return ["CAPABILITY_NAMED_IAM"];
|
|
415
|
+
}
|
|
399
416
|
}
|
|
400
417
|
|
|
401
418
|
const realCloudFormation: CloudFormationClient = {
|
|
@@ -410,10 +427,11 @@ const realCloudFormation: CloudFormationClient = {
|
|
|
410
427
|
// for one still in REVIEW_IN_PROGRESS (a prior CREATE change set never
|
|
411
428
|
// executed), mirroring what `aws cloudformation deploy` does under the hood.
|
|
412
429
|
const changeSetType = await cfnChangeSetType(args.stackName);
|
|
413
|
-
let
|
|
430
|
+
let body = "";
|
|
414
431
|
try {
|
|
415
|
-
|
|
416
|
-
} catch { /* unreadable
|
|
432
|
+
body = readFileSync(args.templatePath, "utf8");
|
|
433
|
+
} catch { /* unreadable template — create-change-set reports it */ }
|
|
434
|
+
const capabilities = awsDeployCapabilitiesForBody(body).join(" ");
|
|
417
435
|
await run(
|
|
418
436
|
`aws cloudformation create-change-set --stack-name ${q(args.stackName)} --change-set-name ${q(changeSetName)} ` +
|
|
419
437
|
`--change-set-type ${changeSetType} ` +
|
|
@@ -6,7 +6,7 @@ import { AttrRef } from "@intentius/chant/attrref";
|
|
|
6
6
|
// reaching into core directly here mirrors nested-stack-integration.test.ts's
|
|
7
7
|
// precedent for aws-lexicon integration-style tests.
|
|
8
8
|
import { resolveAttrRefs } from "../../../../packages/core/src/discovery/resolve";
|
|
9
|
-
import { AgentCoreAgent } from "./agentcore-agent";
|
|
9
|
+
import { AgentCoreAgent, agentCoreDefaultEndpointArn } from "./agentcore-agent";
|
|
10
10
|
import { Split, Ref } from "../intrinsics";
|
|
11
11
|
import { awsSerializer } from "../serializer";
|
|
12
12
|
|
|
@@ -15,12 +15,12 @@ const baseProps = {
|
|
|
15
15
|
containerUri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/support-agent:latest",
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const withEndpoint = { ...baseProps,
|
|
18
|
+
// An explicit endpoint exists only for a non-DEFAULT alias (#978): AgentCore owns DEFAULT,
|
|
19
|
+
// and declaring it races the Runtime's async version-READY. Endpoint tests use this variant.
|
|
20
|
+
const withEndpoint = { ...baseProps, endpointName: "PROD" };
|
|
21
21
|
|
|
22
22
|
describe("AgentCoreAgent", () => {
|
|
23
|
-
test("returns 7 members by default;
|
|
23
|
+
test("returns 7 members by default; no RuntimeEndpoint (#978)", () => {
|
|
24
24
|
const instance = AgentCoreAgent(baseProps);
|
|
25
25
|
expect(Object.keys(instance.members)).toEqual([
|
|
26
26
|
"role", "gatewayRole", "runtime", "memory",
|
|
@@ -29,7 +29,7 @@ describe("AgentCoreAgent", () => {
|
|
|
29
29
|
expect((instance as any).endpoint).toBeUndefined();
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test("
|
|
32
|
+
test("endpointName adds an explicit endpoint as an 8th member", () => {
|
|
33
33
|
const instance = AgentCoreAgent(withEndpoint);
|
|
34
34
|
expect(Object.keys(instance.members)).toContain("endpoint");
|
|
35
35
|
expect(Object.keys(instance.members)).toHaveLength(8);
|
|
@@ -40,13 +40,13 @@ describe("AgentCoreAgent", () => {
|
|
|
40
40
|
expect(expanded.has("agentRole")).toBe(true);
|
|
41
41
|
expect(expanded.has("agentGatewayRole")).toBe(true);
|
|
42
42
|
expect(expanded.has("agentRuntime")).toBe(true);
|
|
43
|
-
expect(expanded.has("agentEndpoint")).toBe(false); //
|
|
43
|
+
expect(expanded.has("agentEndpoint")).toBe(false); // managed DEFAULT, not declared (#978)
|
|
44
44
|
expect(expanded.has("agentMemory")).toBe(true);
|
|
45
45
|
expect(expanded.has("agentWorkloadIdentity")).toBe(true);
|
|
46
46
|
expect(expanded.has("agentGateway")).toBe(true);
|
|
47
47
|
expect(expanded.has("agentGatewayTarget")).toBe(true);
|
|
48
48
|
expect(expanded.size).toBe(7);
|
|
49
|
-
// With
|
|
49
|
+
// With an explicit non-DEFAULT endpoint, it appears as agentEndpoint.
|
|
50
50
|
expect(expandComposite("agent", AgentCoreAgent(withEndpoint)).has("agentEndpoint")).toBe(true);
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -69,7 +69,7 @@ describe("AgentCoreAgent", () => {
|
|
|
69
69
|
const memoryProps = (instance.memory as any).props;
|
|
70
70
|
expect(runtimeProps.AgentRuntimeName).toBe("support_agent");
|
|
71
71
|
expect(runtimeProps.AgentRuntimeName).toMatch(/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/);
|
|
72
|
-
expect(endpointProps.Name).toBe("
|
|
72
|
+
expect(endpointProps.Name).toBe("PROD");
|
|
73
73
|
expect(memoryProps.Name).toBe("support_agentMemory");
|
|
74
74
|
expect(memoryProps.Name).toMatch(/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/);
|
|
75
75
|
});
|
|
@@ -144,7 +144,7 @@ describe("AgentCoreAgent", () => {
|
|
|
144
144
|
code: { s3Bucket: "loom-artifacts", s3Prefix: "agents/assistant.zip", runtime: "PYTHON_3_12", entryPoint: ["app.py"] },
|
|
145
145
|
}));
|
|
146
146
|
resolveAttrRefs(expanded);
|
|
147
|
-
const template = JSON.parse(awsSerializer.serialize(expanded));
|
|
147
|
+
const template = JSON.parse(awsSerializer.serialize(expanded) as string);
|
|
148
148
|
const artifact = template.Resources.agentRuntime.Properties.AgentRuntimeArtifact;
|
|
149
149
|
expect(artifact.ContainerConfiguration).toBeUndefined();
|
|
150
150
|
expect(artifact.CodeConfiguration).toEqual({
|
|
@@ -195,7 +195,27 @@ describe("AgentCoreAgent", () => {
|
|
|
195
195
|
);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
test("
|
|
198
|
+
test("endpointName \"DEFAULT\" is rejected — AgentCore provisions the managed DEFAULT endpoint (#978)", () => {
|
|
199
|
+
expect(() => AgentCoreAgent({ ...baseProps, endpointName: "DEFAULT" })).toThrow(
|
|
200
|
+
"AgentCoreAgent endpointName must not be \"DEFAULT\"",
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("explicit endpoint name is sanitized to the Runtime identifier pattern", () => {
|
|
205
|
+
const instance = AgentCoreAgent({ ...baseProps, endpointName: "blue-green" });
|
|
206
|
+
expect((instance.endpoint as any).props.Name).toBe("blue_green");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("agentCoreDefaultEndpointArn derives the managed DEFAULT endpoint ARN from the Runtime ARN", () => {
|
|
210
|
+
const instance = AgentCoreAgent(baseProps);
|
|
211
|
+
const expanded = expandComposite("agent", instance);
|
|
212
|
+
resolveAttrRefs(expanded);
|
|
213
|
+
expect(JSON.parse(JSON.stringify(agentCoreDefaultEndpointArn(instance.runtime)))).toEqual({
|
|
214
|
+
"Fn::Sub": "${agentRuntime.AgentRuntimeArn}/runtime-endpoint/DEFAULT",
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("RuntimeEndpoint references runtime.AgentRuntimeId (when an explicit endpoint is named)", () => {
|
|
199
219
|
const instance = AgentCoreAgent(withEndpoint);
|
|
200
220
|
const endpointProps = (instance.endpoint as any).props;
|
|
201
221
|
expect(endpointProps.AgentRuntimeId).toBeInstanceOf(AttrRef);
|
|
@@ -245,7 +265,7 @@ describe("AgentCoreAgent", () => {
|
|
|
245
265
|
test("serializes to a valid CloudFormation template with the expected resource types", () => {
|
|
246
266
|
const expanded = expandComposite("agent", AgentCoreAgent(withEndpoint));
|
|
247
267
|
resolveAttrRefs(expanded);
|
|
248
|
-
const output = awsSerializer.serialize(expanded);
|
|
268
|
+
const output = awsSerializer.serialize(expanded) as string;
|
|
249
269
|
const template = JSON.parse(output);
|
|
250
270
|
|
|
251
271
|
expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
|
|
@@ -30,6 +30,19 @@ export type AgentManagedRuntime =
|
|
|
30
30
|
| "PYTHON_3_13"
|
|
31
31
|
| "PYTHON_3_14";
|
|
32
32
|
import { agentCoreTrustPolicy } from "./agentcore-trust-policy";
|
|
33
|
+
import { Sub, type SubIntrinsic } from "../intrinsics";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ARN of the managed `DEFAULT` endpoint AgentCore provisions with a Runtime,
|
|
37
|
+
* as an `Fn::Sub` over the Runtime's ARN. No CloudFormation attribute carries
|
|
38
|
+
* it (the Runtime schema exposes only `AgentRuntimeArn`/`AgentRuntimeId`/
|
|
39
|
+
* `AgentRuntimeVersion`/`Status`), but the format is fixed by the
|
|
40
|
+
* CreateAgentRuntimeEndpoint API:
|
|
41
|
+
* `arn:aws:bedrock-agentcore:<region>:<account>:runtime/<id>/runtime-endpoint/<name>`.
|
|
42
|
+
*/
|
|
43
|
+
export function agentCoreDefaultEndpointArn(runtime: InstanceType<typeof Runtime>): SubIntrinsic {
|
|
44
|
+
return Sub`${runtime.AgentRuntimeArn}/runtime-endpoint/DEFAULT`;
|
|
45
|
+
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
35
48
|
* AgentCore's `Runtime`/`RuntimeEndpoint`/`Memory` `Name`/`AgentRuntimeName`
|
|
@@ -70,7 +83,7 @@ export interface AgentCoreCodeArtifact {
|
|
|
70
83
|
export interface AgentCoreAgentProps {
|
|
71
84
|
/**
|
|
72
85
|
* Base name for the agent's resources. `toRuntimeIdentifier(name)` derives
|
|
73
|
-
* the Runtime/
|
|
86
|
+
* the Runtime/Memory (and any explicit endpoint) names; Gateway/GatewayTarget/
|
|
74
87
|
* WorkloadIdentity use `name` as-is (hyphens are valid there).
|
|
75
88
|
*/
|
|
76
89
|
name: string;
|
|
@@ -94,20 +107,16 @@ export interface AgentCoreAgentProps {
|
|
|
94
107
|
protocolConfiguration?: "A2A" | "AGUI" | "HTTP" | "MCP";
|
|
95
108
|
/** Environment variables passed to the Runtime container. */
|
|
96
109
|
environmentVariables?: Record<string, string>;
|
|
97
|
-
/** RuntimeEndpoint name — the alias a version-promotion capability would repoint (deferred, see #882). Default: "DEFAULT". */
|
|
98
|
-
endpointName?: string;
|
|
99
110
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* AgentCore's own tooling (and Loom's app) does. Opt in only when you know the
|
|
108
|
-
* Runtime will already be READY (e.g. a version-promotion flow on an existing runtime).
|
|
111
|
+
* Name of an explicit, non-`DEFAULT` `RuntimeEndpoint` to create alongside
|
|
112
|
+
* the Runtime (e.g. `"PROD"`), the alias a version-promotion flow would
|
|
113
|
+
* later repoint (deferred, see #882). Omit it and no endpoint resource is
|
|
114
|
+
* created: AgentCore provisions a managed `DEFAULT` endpoint with every
|
|
115
|
+
* Runtime, and it tracks the latest version on its own. `"DEFAULT"` is
|
|
116
|
+
* rejected, since a CloudFormation endpoint of that name duplicates the
|
|
117
|
+
* managed one and fails on a real apply (#978, see the composite doc).
|
|
109
118
|
*/
|
|
110
|
-
|
|
119
|
+
endpointName?: string;
|
|
111
120
|
/** Memory event retention, in days. CFN bounds: 3-365. Default: 30. */
|
|
112
121
|
memoryEventExpiryDays?: number;
|
|
113
122
|
/** Gateway authorizer. Mirrors the generated `BedrockAgentCoreGateway_AuthorizerType` CFN enum. Default: "AWS_IAM". */
|
|
@@ -134,7 +143,7 @@ export type AgentCoreAgentResult = {
|
|
|
134
143
|
role: InstanceType<typeof Role>;
|
|
135
144
|
gatewayRole: InstanceType<typeof Role>;
|
|
136
145
|
runtime: InstanceType<typeof Runtime>;
|
|
137
|
-
/** Present only when `
|
|
146
|
+
/** Present only when `endpointName` names an explicit non-DEFAULT endpoint (#978). */
|
|
138
147
|
endpoint?: InstanceType<typeof RuntimeEndpoint>;
|
|
139
148
|
memory: InstanceType<typeof Memory>;
|
|
140
149
|
workloadIdentity: InstanceType<typeof WorkloadIdentity>;
|
|
@@ -144,7 +153,7 @@ export type AgentCoreAgentResult = {
|
|
|
144
153
|
|
|
145
154
|
/**
|
|
146
155
|
* A Bedrock AgentCore agent as one CloudFormation-serializable bundle — the
|
|
147
|
-
* composite/base path from #882: `Runtime` + `
|
|
156
|
+
* composite/base path from #882: `Runtime` + `Memory` +
|
|
148
157
|
* `Gateway`/`GatewayTarget` + `WorkloadIdentity` + IAM, deployable with
|
|
149
158
|
* `cfn-deploy` + `wait-for-stack` and no bespoke verb.
|
|
150
159
|
*
|
|
@@ -156,10 +165,18 @@ export type AgentCoreAgentResult = {
|
|
|
156
165
|
* for workflows (e.g. a future credential-provider capability) that need an
|
|
157
166
|
* explicit workload identity of their own.
|
|
158
167
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
168
|
+
* There is no `RuntimeEndpoint` in the bundle by default. AgentCore creates a
|
|
169
|
+
* managed `DEFAULT` endpoint with every Runtime and repoints it at each new
|
|
170
|
+
* version on its own, so a CloudFormation `DEFAULT` endpoint is redundant. It
|
|
171
|
+
* is also the race that sank a live deploy (#978): the Runtime resource's
|
|
172
|
+
* `CREATE_COMPLETE` fires while AgentCore is still turning the artifact into
|
|
173
|
+
* a READY agent version, and the endpoint's CREATE then fails with "Agent
|
|
174
|
+
* version 1 must be in READY status". Invoking the Runtime with no qualifier
|
|
175
|
+
* hits the managed DEFAULT endpoint; {@link agentCoreDefaultEndpointArn}
|
|
176
|
+
* builds its ARN from the Runtime's. `endpointName` adds an explicit
|
|
177
|
+
* non-DEFAULT endpoint for the `agentcore-deploy` version-promotion flow,
|
|
178
|
+
* which is deferred (GA-gated, #882) — this composite only wires the
|
|
179
|
+
* CloudFormation shape it would eventually apply against.
|
|
163
180
|
*/
|
|
164
181
|
export const AgentCoreAgent = Composite<AgentCoreAgentProps, AgentCoreAgentResult>((props) => {
|
|
165
182
|
const { defaults } = props;
|
|
@@ -241,12 +258,15 @@ export const AgentCoreAgent = Composite<AgentCoreAgentProps, AgentCoreAgentResul
|
|
|
241
258
|
EnvironmentVariables: props.environmentVariables,
|
|
242
259
|
}, defaults?.runtime));
|
|
243
260
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
261
|
+
// Explicit endpoints only for a non-DEFAULT alias (#978): AgentCore owns
|
|
262
|
+
// DEFAULT, and creating it here races the Runtime's async version-READY.
|
|
263
|
+
if (props.endpointName === "DEFAULT") {
|
|
264
|
+
throw new Error("AgentCoreAgent endpointName must not be \"DEFAULT\": AgentCore provisions the managed DEFAULT endpoint itself");
|
|
265
|
+
}
|
|
266
|
+
const endpoint = props.endpointName !== undefined
|
|
247
267
|
? new RuntimeEndpoint(mergeDefaults({
|
|
248
268
|
AgentRuntimeId: runtime.AgentRuntimeId,
|
|
249
|
-
Name: toRuntimeIdentifier(props.endpointName
|
|
269
|
+
Name: toRuntimeIdentifier(props.endpointName),
|
|
250
270
|
}, defaults?.endpoint))
|
|
251
271
|
: undefined;
|
|
252
272
|
|
package/src/composites/index.ts
CHANGED
|
@@ -43,5 +43,5 @@ export { EksCluster } from "./eks-cluster";
|
|
|
43
43
|
export type { EksClusterProps } from "./eks-cluster";
|
|
44
44
|
export { MicrovmApp, MICROVM_LIMITS } from "./microvm-app";
|
|
45
45
|
export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB } from "./microvm-app";
|
|
46
|
-
export { AgentCoreAgent } from "./agentcore-agent";
|
|
46
|
+
export { AgentCoreAgent, agentCoreDefaultEndpointArn } from "./agentcore-agent";
|
|
47
47
|
export type { AgentCoreAgentProps, AgentCoreAgentResult } from "./agentcore-agent";
|
|
@@ -149,7 +149,7 @@ describe("MicrovmApp", () => {
|
|
|
149
149
|
const instance = MicrovmApp(baseProps);
|
|
150
150
|
const entities = expandComposite("worker", instance);
|
|
151
151
|
resolveAttrRefs(entities);
|
|
152
|
-
const output = awsSerializer.serialize(entities);
|
|
152
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
153
153
|
const template = JSON.parse(output);
|
|
154
154
|
|
|
155
155
|
expect(template.AWSTemplateFormatVersion).toBe("2010-09-09");
|
|
@@ -204,7 +204,7 @@ describe("MicrovmApp", () => {
|
|
|
204
204
|
});
|
|
205
205
|
const entities = expandComposite("worker", instance);
|
|
206
206
|
resolveAttrRefs(entities);
|
|
207
|
-
const output = awsSerializer.serialize(entities);
|
|
207
|
+
const output = awsSerializer.serialize(entities) as string;
|
|
208
208
|
const template = JSON.parse(output);
|
|
209
209
|
|
|
210
210
|
const connector = template.Resources.workerConnector;
|
package/src/deep-observe.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
observeResourcesDeepAws,
|
|
25
25
|
awsDeepNormalizationHooks,
|
|
26
26
|
hasOwnershipMarker,
|
|
27
|
+
schemaReadOnlyPatterns,
|
|
27
28
|
} = await import("./deep-observe");
|
|
28
29
|
const { parseResourceDescription } = await import("./api/read-client");
|
|
29
30
|
const { deepDiffForLexicon } = await import("@intentius/chant/lifecycle/deep-observe");
|
|
@@ -252,6 +253,99 @@ describe("the aws noise rules", () => {
|
|
|
252
253
|
});
|
|
253
254
|
});
|
|
254
255
|
|
|
256
|
+
// A property the schema marks read-only is a GetAtt attribute, never a
|
|
257
|
+
// declared input. The live read still reports it, so without the schema-driven
|
|
258
|
+
// rule every clean apply shows `<undeclared> -> value` for it.
|
|
259
|
+
describe("schema read-only properties are attributes, not drift (#1641)", () => {
|
|
260
|
+
test("the registry is read off the schema's readOnlyProperties, arrays spelled as patterns", () => {
|
|
261
|
+
expect([...schemaReadOnlyPatterns("AWS::IAM::ManagedPolicy")]).toEqual(
|
|
262
|
+
expect.arrayContaining(["PolicyArn", "PolicyId", "AttachmentCount", "DefaultVersionId"]),
|
|
263
|
+
);
|
|
264
|
+
expect([...schemaReadOnlyPatterns("AWS::RDS::DBInstance")]).toEqual(
|
|
265
|
+
expect.arrayContaining(["Endpoint.Address", "Endpoint.Port", "DbiResourceId", "DBInstanceStatus"]),
|
|
266
|
+
);
|
|
267
|
+
expect([...schemaReadOnlyPatterns("AWS::CE::AnomalySubscription")]).toContain("Subscribers[].Status");
|
|
268
|
+
expect(schemaReadOnlyPatterns("AWS::Made::Up").size).toBe(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("ManagedPolicy: a live read carrying PolicyArn is not property drift", async () => {
|
|
272
|
+
vi.spyOn(globalThis, "fetch").mockImplementation((async (_url: string, init: { headers: Record<string, string>; body: string }) => {
|
|
273
|
+
const target = init.headers["x-amz-target"];
|
|
274
|
+
if (!target) return { status: 200, text: () => Promise.resolve(stackResources([["ReadOnly", "AWS::IAM::ManagedPolicy", "arn:aws:iam::000000000000:policy/S3VectorsReadOnlyAccess"]]).text) };
|
|
275
|
+
const r = cloudControl("arn:aws:iam::000000000000:policy/S3VectorsReadOnlyAccess", {
|
|
276
|
+
ManagedPolicyName: "S3VectorsReadOnlyAccess",
|
|
277
|
+
Path: "/",
|
|
278
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Action: ["s3vectors:Get*"], Resource: "*" }] },
|
|
279
|
+
// Every readOnlyProperties entry for the type, as real AWS and the
|
|
280
|
+
// emulator return them. PolicyArn is also the primary identifier.
|
|
281
|
+
PolicyArn: "arn:aws:iam::000000000000:policy/S3VectorsReadOnlyAccess",
|
|
282
|
+
PolicyId: "ANPA000000000000EXAMPLE",
|
|
283
|
+
AttachmentCount: 1,
|
|
284
|
+
DefaultVersionId: "v1",
|
|
285
|
+
IsAttachable: true,
|
|
286
|
+
PermissionsBoundaryUsageCount: 0,
|
|
287
|
+
CreateDate: "2026-01-01T00:00:00Z",
|
|
288
|
+
UpdateDate: "2026-01-01T00:00:00Z",
|
|
289
|
+
});
|
|
290
|
+
return { status: r.status, text: () => Promise.resolve(r.text) };
|
|
291
|
+
}) as unknown as typeof fetch);
|
|
292
|
+
try {
|
|
293
|
+
const result = await deepDiffForLexicon(awsPlugin, {
|
|
294
|
+
environment: "prod",
|
|
295
|
+
buildOutput: "",
|
|
296
|
+
entities: entities({
|
|
297
|
+
ReadOnly: {
|
|
298
|
+
entityType: "AWS::IAM::ManagedPolicy",
|
|
299
|
+
props: {
|
|
300
|
+
ManagedPolicyName: "S3VectorsReadOnlyAccess",
|
|
301
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Action: ["s3vectors:Get*"], Resource: "*" }] },
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
expect(result.drifted).toEqual([]);
|
|
307
|
+
expect(result.unchanged).toEqual(["ReadOnly"]);
|
|
308
|
+
} finally {
|
|
309
|
+
vi.restoreAllMocks();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a second type with nested read-only paths: RDS DBInstance's endpoint and status are pruned, inputs are kept", () => {
|
|
314
|
+
const out = normalizeDeepProperties(
|
|
315
|
+
{
|
|
316
|
+
DBInstanceIdentifier: "db-1",
|
|
317
|
+
DBInstanceClass: "db.t4g.micro",
|
|
318
|
+
Endpoint: { Address: "db-1.abc.us-east-1.rds.amazonaws.com", Port: "5432", HostedZoneId: "Z1" },
|
|
319
|
+
DbiResourceId: "db-ABCDEF",
|
|
320
|
+
DBInstanceStatus: "available",
|
|
321
|
+
InstanceCreateTime: "2026-01-01T00:00:00Z",
|
|
322
|
+
CertificateDetails: { CAIdentifier: "rds-ca-rsa2048-g1", ValidTill: "2027-01-01T00:00:00Z" },
|
|
323
|
+
ProcessorFeatures: [{ Name: "coreCount", Value: "2" }],
|
|
324
|
+
},
|
|
325
|
+
{ entityType: "AWS::RDS::DBInstance", side: "live", hooks: awsDeepNormalizationHooks },
|
|
326
|
+
);
|
|
327
|
+
expect(out).toEqual({
|
|
328
|
+
DBInstanceIdentifier: "db-1",
|
|
329
|
+
DBInstanceClass: "db.t4g.micro",
|
|
330
|
+
ProcessorFeatures: [{ Name: "coreCount", Value: "2" }],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("an array element path from the schema prunes inside the array", () => {
|
|
335
|
+
const out = normalizeDeepProperties(
|
|
336
|
+
{
|
|
337
|
+
SubscriptionName: "spend",
|
|
338
|
+
Subscribers: [{ Address: "a@example.com", Type: "EMAIL", Status: "CONFIRMED" }],
|
|
339
|
+
},
|
|
340
|
+
{ entityType: "AWS::CE::AnomalySubscription", side: "live", hooks: awsDeepNormalizationHooks },
|
|
341
|
+
);
|
|
342
|
+
expect(out).toEqual({
|
|
343
|
+
SubscriptionName: "spend",
|
|
344
|
+
Subscribers: [{ Address: "a@example.com", Type: "EMAIL" }],
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
255
349
|
describe("hasOwnershipMarker", () => {
|
|
256
350
|
test("reads chant's tag out of the live tree", () => {
|
|
257
351
|
expect(hasOwnershipMarker({ Tags: [{ Key: "chant:managed-by", Value: "chant" }] })).toBe(true);
|
package/src/deep-observe.ts
CHANGED
|
@@ -62,6 +62,9 @@ import {
|
|
|
62
62
|
import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
|
|
63
63
|
import { applyAwsEndpointArgv } from "./components/cloud-executor";
|
|
64
64
|
import { toIngressRules } from "./dependencies";
|
|
65
|
+
import { createRequire } from "node:module";
|
|
66
|
+
|
|
67
|
+
const require = createRequire(import.meta.url);
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* Where each type's live model comes from.
|
|
@@ -226,6 +229,58 @@ function canonicalJson(value: unknown): string {
|
|
|
226
229
|
) ?? "";
|
|
227
230
|
}
|
|
228
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The read-only properties of one type, as index-erased patterns, straight from
|
|
234
|
+
* the CloudFormation schema's `readOnlyProperties` (#1641).
|
|
235
|
+
*
|
|
236
|
+
* The codegen already turns that list into each class's GetAtt attributes
|
|
237
|
+
* (`attrs` in `lexicon-aws.json`), so this reads the same registry from the
|
|
238
|
+
* other side: a path the schema says only the service can write is an
|
|
239
|
+
* attribute, and an attribute the live read reports is not property drift. No
|
|
240
|
+
* declaration can contain one, so `<undeclared> -> value` is the shape every
|
|
241
|
+
* clean apply would otherwise produce. `AWS::IAM::ManagedPolicy.PolicyArn`,
|
|
242
|
+
* which is also the type's Cloud Control primary identifier, is the case that
|
|
243
|
+
* surfaced it.
|
|
244
|
+
*
|
|
245
|
+
* The manifest spells an array element `Subscribers.*.Status`; the
|
|
246
|
+
* normalization pass spells the same thing `Subscribers[].Status`.
|
|
247
|
+
*
|
|
248
|
+
* Complements {@link AWS_READ_ONLY_NAMES} rather than replacing it: the
|
|
249
|
+
* name-based list also covers translated models (an EC2 row mapped onto the
|
|
250
|
+
* CloudFormation shape) and nested documents the schema never enumerates.
|
|
251
|
+
*/
|
|
252
|
+
export function schemaReadOnlyPatterns(entityType: string): ReadonlySet<string> {
|
|
253
|
+
const cached = readOnlyByType.get(entityType);
|
|
254
|
+
if (cached) return cached;
|
|
255
|
+
const entry = manifestByType().get(entityType);
|
|
256
|
+
const patterns = new Set<string>();
|
|
257
|
+
for (const attr of Object.values(entry?.attrs ?? {})) {
|
|
258
|
+
patterns.add(attr.replace(/\.\*(?=\.|$)/g, "[]"));
|
|
259
|
+
}
|
|
260
|
+
readOnlyByType.set(entityType, patterns);
|
|
261
|
+
return patterns;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface ManifestEntry {
|
|
265
|
+
resourceType: string;
|
|
266
|
+
kind: string;
|
|
267
|
+
attrs?: Record<string, string>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const readOnlyByType = new Map<string, ReadonlySet<string>>();
|
|
271
|
+
let manifestIndex: Map<string, ManifestEntry> | undefined;
|
|
272
|
+
function manifestByType(): Map<string, ManifestEntry> {
|
|
273
|
+
if (!manifestIndex) {
|
|
274
|
+
const manifest = require("./generated/lexicon-aws.json") as Record<string, ManifestEntry>;
|
|
275
|
+
manifestIndex = new Map(
|
|
276
|
+
Object.values(manifest)
|
|
277
|
+
.filter((e) => e.kind === "resource")
|
|
278
|
+
.map((e) => [e.resourceType, e]),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return manifestIndex;
|
|
282
|
+
}
|
|
283
|
+
|
|
229
284
|
/** The final segment of an index-erased pattern (`Policies[].PolicyName` → `PolicyName`). */
|
|
230
285
|
function lastSegment(pattern: string): string {
|
|
231
286
|
const withoutIndex = pattern.replace(/\[\]$/, "");
|
|
@@ -243,7 +298,9 @@ export const awsDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
|
243
298
|
prune(node: DeepNode): boolean {
|
|
244
299
|
// Read-only / server-populated. Pruned on both sides: if source somehow
|
|
245
300
|
// declares an arn-shaped output, comparing it to the live one is still
|
|
246
|
-
// meaningless.
|
|
301
|
+
// meaningless. The schema's own `readOnlyProperties` first (#1641), then
|
|
302
|
+
// the name-based list for what the schema does not enumerate.
|
|
303
|
+
if (schemaReadOnlyPatterns(node.entityType).has(node.pattern)) return true;
|
|
247
304
|
if (AWS_READ_ONLY_NAMES.has(lastSegment(node.pattern))) return true;
|
|
248
305
|
|
|
249
306
|
// Provider defaults, on the live side only, and only where source is silent
|
|
@@ -424,7 +481,6 @@ export async function observeResourcesDeepAws(
|
|
|
424
481
|
|
|
425
482
|
const stackName = options.stack ?? options.environment;
|
|
426
483
|
const client: AwsReadClientOptions = {
|
|
427
|
-
...(process.env.AWS_ENDPOINT_URL ? { endpoint: process.env.AWS_ENDPOINT_URL } : {}),
|
|
428
484
|
...(options.region ? { region: options.region } : {}),
|
|
429
485
|
...(options.http ? { http: options.http } : {}),
|
|
430
486
|
};
|
|
@@ -87,7 +87,7 @@ describe("parameters.json build roundtrip", () => {
|
|
|
87
87
|
const result = await build(srcDir, [awsSerializer]);
|
|
88
88
|
expect(result.errors).toHaveLength(0);
|
|
89
89
|
|
|
90
|
-
const template = JSON.parse(result.outputs.get("aws")
|
|
90
|
+
const template = JSON.parse(result.outputs.get("aws") as string);
|
|
91
91
|
|
|
92
92
|
// Verify parameters round-tripped correctly (names preserved as-is)
|
|
93
93
|
for (const [name, param] of Object.entries(source.Parameters ?? {})) {
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,8 @@ export {
|
|
|
47
47
|
getResource,
|
|
48
48
|
listResources,
|
|
49
49
|
parseResourceDescription,
|
|
50
|
+
resolveEndpointOverride,
|
|
51
|
+
serviceEndpointEnvVar,
|
|
50
52
|
AwsReadError,
|
|
51
53
|
type AwsReadHttp,
|
|
52
54
|
type AwsReadClientOptions,
|
|
@@ -126,7 +128,7 @@ export {
|
|
|
126
128
|
Ec2InstanceRole, MinimalVpc, EksCluster,
|
|
127
129
|
SolrFargateService,
|
|
128
130
|
MicrovmApp, MICROVM_LIMITS,
|
|
129
|
-
AgentCoreAgent,
|
|
131
|
+
AgentCoreAgent, agentCoreDefaultEndpointArn,
|
|
130
132
|
OrganizationRoot, GovernanceFoundation, RegionRestriction, OrganizationTrail,
|
|
131
133
|
} from "./composites/index";
|
|
132
134
|
export type {
|