@intentius/chant-lexicon-cedar 0.124.31 → 0.124.33
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/embed.js +250 -0
- package/dist/agentcore/enforcement.js +81 -0
- package/dist/agentcore/scan.js +93 -0
- package/dist/avp/ambient.js +100 -0
- package/dist/avp/client.js +318 -0
- package/dist/avp/describe-resources.js +144 -0
- package/dist/avp/embed.js +125 -0
- package/dist/avp/live-export.js +151 -0
- package/dist/avp/ownership.js +144 -0
- package/dist/avp/statement.js +45 -0
- package/dist/avp/store.js +105 -0
- package/dist/avp/testdata/mock-transport.js +100 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +133 -0
- package/dist/codegen/emit.js +331 -0
- package/dist/codegen/generate-cli.js +18 -0
- package/dist/codegen/generate.js +122 -0
- package/dist/codegen/naming.js +90 -0
- package/dist/codegen/package.js +33 -0
- package/dist/commands.js +86 -0
- package/dist/composites/deny-by-default-set.js +51 -0
- package/dist/composites/index.js +9 -0
- package/dist/composites/owner-can-manage.js +48 -0
- package/dist/config.js +159 -0
- package/dist/coverage.js +112 -0
- package/dist/detect.js +61 -0
- package/dist/dogwood/cli.js +477 -0
- package/dist/dogwood/event-schema.js +219 -0
- package/dist/dogwood/index.js +32 -0
- package/dist/dogwood/macros.js +175 -0
- package/dist/dogwood/policy.js +58 -0
- package/dist/dogwood/replay-activity.js +313 -0
- package/dist/dogwood/replay-op.js +134 -0
- package/dist/dogwood/scan.js +234 -0
- package/dist/dogwood/serialize.js +199 -0
- package/dist/dogwood/temporal.js +354 -0
- package/dist/dogwood/trace.js +269 -0
- package/dist/dogwood/upstream.js +40 -0
- package/dist/dogwood/window.js +82 -0
- package/dist/gate-policy-pack.js +189 -0
- package/dist/gate-policy.js +168 -0
- package/dist/gate-schema.js +122 -0
- package/dist/generated/index.js +94 -0
- package/dist/generated/runtime.js +2 -0
- package/dist/import/adapter.js +54 -0
- package/dist/import/clause-text.js +166 -0
- package/dist/import/generator.js +178 -0
- package/dist/import/parser.js +273 -0
- package/dist/index.js +79 -0
- package/dist/init-templates.js +330 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +65 -0
- package/dist/lint/audit-lineage.js +23 -0
- package/dist/lint/post-synth/cedar-helpers.js +123 -0
- package/dist/lint/post-synth/cedc010.js +59 -0
- package/dist/lint/post-synth/cedc011.js +29 -0
- package/dist/lint/post-synth/cedc012.js +34 -0
- package/dist/lint/post-synth/cedc013.js +45 -0
- package/dist/lint/post-synth/cedc014.js +54 -0
- package/dist/lint/post-synth/cede010.js +51 -0
- package/dist/lint/post-synth/cede011.js +36 -0
- package/dist/lint/post-synth/ceds010.js +31 -0
- package/dist/lint/post-synth/ceds011.js +22 -0
- package/dist/lint/post-synth/ceds012.js +31 -0
- package/dist/lint/post-synth/dogwood-helpers.js +94 -0
- package/dist/lint/post-synth/dwdc010.js +38 -0
- package/dist/lint/post-synth/dwdc011.js +34 -0
- package/dist/lint/post-synth/dwdc012.js +24 -0
- package/dist/lint/post-synth/dwdc013.js +29 -0
- package/dist/lint/post-synth/dwde010.js +84 -0
- package/dist/lint/post-synth/dwde011.js +67 -0
- package/dist/lint/post-synth/dwds010.js +21 -0
- package/dist/lint/post-synth/index.js +36 -0
- package/dist/lint/post-synth/wasm-helpers.js +234 -0
- package/dist/lint/rules/index.js +9 -0
- package/dist/lint/rules/policy-shape.js +114 -0
- package/dist/lsp/completions.js +52 -0
- package/dist/lsp/hover.js +56 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +51 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/policy-coverage.js +134 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/package-cli.js +18 -0
- package/dist/plugin.js +304 -0
- package/dist/policy-text.js +117 -0
- package/dist/schema-artifact.js +66 -0
- package/dist/serializer.js +203 -0
- package/dist/spec/fetch.js +96 -0
- package/dist/spec/parse.js +164 -0
- package/dist/spec/pin.js +170 -0
- package/dist/spec/wasm.js +194 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate.js +74 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/coverage.ts +2 -2
- package/src/import/generator.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/spec/fetch.ts +2 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed embedding of a cedar or dogwood policy into
|
|
3
|
+
* `AWS::BedrockAgentCore::Policy` (#1660).
|
|
4
|
+
*
|
|
5
|
+
* The aws lexicon keeps the deployment vehicle, exactly as it does for AVP
|
|
6
|
+
* (#1652, `../avp/embed.ts`). What differs is the shape of the vehicle:
|
|
7
|
+
* AgentCore's `Definition` is a two-arm `oneOf`, not the single `Static` arm
|
|
8
|
+
* `AWS::VerifiedPermissions::Policy` has —
|
|
9
|
+
*
|
|
10
|
+
* ```jsonc
|
|
11
|
+
* "PolicyDefinition": {
|
|
12
|
+
* "oneOf": [{ "required": ["Cedar"] }, { "required": ["Policy"] }]
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* — where `Cedar.Statement` is plain Cedar and `Policy.Statement` is the
|
|
17
|
+
* language-agnostic arm. That second arm is what makes AgentCore the
|
|
18
|
+
* deployment target for the dogwood dialect: a `.dw` policy is a Cedar policy
|
|
19
|
+
* with `when temporal { … }` clauses, which the `Cedar` arm has no business
|
|
20
|
+
* accepting, and the `Policy` arm exists precisely so a policy engine can be
|
|
21
|
+
* handed something that is not plain Cedar.
|
|
22
|
+
*
|
|
23
|
+
* A project that has both lexicons installed writes
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* new BedrockAgentCorePolicy({
|
|
27
|
+
* PolicyEngineId: engine.ref(),
|
|
28
|
+
* Name: "writeNeedsApproval",
|
|
29
|
+
* ...agentCoreStagedPolicy("writeNeedsApproval", writeNeedsApproval, "log-only"),
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* and the statement is the same text `chant build` writes to `policies.dw`,
|
|
34
|
+
* rendered by the same renderer, with `@id` intact.
|
|
35
|
+
*
|
|
36
|
+
* ### Why no dependency, and why the example uses plain objects
|
|
37
|
+
*
|
|
38
|
+
* Same rule as `../avp/embed.ts` and for the same reason: a cedar → aws
|
|
39
|
+
* dependency would invert the epic's decision that Cedar is vendor-neutral and
|
|
40
|
+
* make the cedar lexicon unbuildable without the aws one. Nothing here imports
|
|
41
|
+
* `@intentius/chant-lexicon-aws`. The seam is the data shape, which is stable
|
|
42
|
+
* CloudFormation, and the prop names below are the generated class's own
|
|
43
|
+
* (`PolicyEngineId`, `Name`, `Definition`, `EnforcementMode`, `Description`),
|
|
44
|
+
* so substituting the real class is a one-line edit. `examples/agentcore-policy/`
|
|
45
|
+
* demonstrates the pairing with a plain-object stand-in, because the shipped
|
|
46
|
+
* cedar examples build against the cedar serializer alone and an `AWS::*` entity
|
|
47
|
+
* declared in that tree would have no serializer to emit it.
|
|
48
|
+
*
|
|
49
|
+
* ### What this module refuses
|
|
50
|
+
*
|
|
51
|
+
* Templates. `AWS::VerifiedPermissions::Policy` has a `TemplateLinked` arm;
|
|
52
|
+
* AgentCore's `Definition` union does not, so a statement carrying a
|
|
53
|
+
* `?principal` or `?resource` slot has nowhere to go and would be rejected at
|
|
54
|
+
* deploy time with an error about the policy text rather than about the slot.
|
|
55
|
+
* {@link agentCoreStatement} throws instead, at authoring time, naming the
|
|
56
|
+
* declaration.
|
|
57
|
+
*/
|
|
58
|
+
import { isDeclarable } from "@intentius/chant/declarable";
|
|
59
|
+
import { cedarPolicyRecords, getProps, renderPolicyText, resolvePolicyId, CEDAR_POLICY_TYPE, } from "../serializer.js";
|
|
60
|
+
import { dogwoodPolicyRecords, renderTemporalPolicyText } from "../dogwood/serialize.js";
|
|
61
|
+
import { DOGWOOD_POLICY_TYPE } from "../dogwood/policy.js";
|
|
62
|
+
import { enforcementMode } from "./enforcement.js";
|
|
63
|
+
// ── Upstream's own limits ─────────────────────────────────────────
|
|
64
|
+
/** `Statement` minLength, on both arms of the union. */
|
|
65
|
+
export const AGENTCORE_STATEMENT_MIN = 35;
|
|
66
|
+
/** `Statement` maxLength, on both arms of the union. */
|
|
67
|
+
export const AGENTCORE_STATEMENT_MAX = 10000;
|
|
68
|
+
/** `Name` — an identifier, and create-only, so a rename replaces the policy. */
|
|
69
|
+
export const AGENTCORE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
70
|
+
/**
|
|
71
|
+
* `?principal` / `?resource` — Cedar's two template slots.
|
|
72
|
+
*
|
|
73
|
+
* Scanned over the whole statement rather than only the scope positions, which
|
|
74
|
+
* is where a slot can legally appear. Inside a `temporal { … }` clause the `?`
|
|
75
|
+
* sigil means a macro parameter, so in principle a macro parameter named
|
|
76
|
+
* `?principal` would read as a slot here — but macro parameters are legal only
|
|
77
|
+
* inside a macro *definition* body, and this module renders policies, never
|
|
78
|
+
* definitions. There is nowhere in a policy statement for the pattern to mean
|
|
79
|
+
* anything else.
|
|
80
|
+
*/
|
|
81
|
+
const TEMPLATE_SLOT = /\?(?:principal|resource)\b/;
|
|
82
|
+
// ── Rendering ─────────────────────────────────────────────────────
|
|
83
|
+
/** The dialect keys `TemporalPolicyProps` adds to `CedarPolicyProps`. */
|
|
84
|
+
const DOGWOOD_ONLY_PROPS = ["whenTemporal", "unlessTemporal", "whenGuardrails", "unlessGuardrails"];
|
|
85
|
+
function hasTemporalProps(props) {
|
|
86
|
+
return DOGWOOD_ONLY_PROPS.some((key) => {
|
|
87
|
+
const value = props[key];
|
|
88
|
+
return Array.isArray(value) ? value.length > 0 : value !== undefined;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function renderRecord(name, props, options) {
|
|
92
|
+
const temporal = hasTemporalProps(props);
|
|
93
|
+
const id = options.policyId ?? resolvePolicyId(name, props);
|
|
94
|
+
return { text: temporal ? renderTemporalPolicyText(id, props) : renderPolicyText(id, props), temporal };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Every policy in a build, as one statement.
|
|
98
|
+
*
|
|
99
|
+
* Cedar policies first, then temporal ones, both in declaration order, so the
|
|
100
|
+
* text is stable across builds. Any temporal policy in the set puts the whole
|
|
101
|
+
* statement in the `Policy` arm — which is correct rather than a compromise: a
|
|
102
|
+
* `.dw` file holds plain Cedar policies too, and splitting the set across two
|
|
103
|
+
* AgentCore policies to keep one of them in the `Cedar` arm would give the two
|
|
104
|
+
* halves separate `EnforcementMode` dials for no reason.
|
|
105
|
+
*/
|
|
106
|
+
function renderPolicySet(entities) {
|
|
107
|
+
const parts = [];
|
|
108
|
+
for (const { id, props } of cedarPolicyRecords(entities))
|
|
109
|
+
parts.push(renderPolicyText(id, props));
|
|
110
|
+
const temporalRecords = dogwoodPolicyRecords(entities);
|
|
111
|
+
for (const { id, props } of temporalRecords)
|
|
112
|
+
parts.push(renderTemporalPolicyText(id, props));
|
|
113
|
+
return { text: parts.join("\n\n"), temporal: temporalRecords.length > 0 };
|
|
114
|
+
}
|
|
115
|
+
function render(name, source, options) {
|
|
116
|
+
if (source instanceof Map)
|
|
117
|
+
return renderPolicySet(source);
|
|
118
|
+
if (isDeclarable(source)) {
|
|
119
|
+
if (source.entityType !== CEDAR_POLICY_TYPE && source.entityType !== DOGWOOD_POLICY_TYPE) {
|
|
120
|
+
throw new Error(`cedar: "${name}" is a ${source.entityType}, which is not a policy — AWS::BedrockAgentCore::Policy embeds ${CEDAR_POLICY_TYPE} or ${DOGWOOD_POLICY_TYPE}.`);
|
|
121
|
+
}
|
|
122
|
+
// Read straight off the entity: no reference walk, because a single
|
|
123
|
+
// Declarable carries no map of the names its siblings were declared under.
|
|
124
|
+
// A scope that names another declared entity needs the policy-set form,
|
|
125
|
+
// which walks references the way the serializer does.
|
|
126
|
+
const props = getProps(source);
|
|
127
|
+
const temporal = source.entityType === DOGWOOD_POLICY_TYPE || hasTemporalProps(props);
|
|
128
|
+
const id = options.policyId ?? resolvePolicyId(name, props);
|
|
129
|
+
return { text: temporal ? renderTemporalPolicyText(id, props) : renderPolicyText(id, props), temporal };
|
|
130
|
+
}
|
|
131
|
+
return renderRecord(name, source, options);
|
|
132
|
+
}
|
|
133
|
+
function assertEmbeddable(name, statement) {
|
|
134
|
+
if (statement.text.length === 0) {
|
|
135
|
+
throw new Error(`cedar: "${name}" rendered no policy text, so there is nothing for AgentCore to evaluate.`);
|
|
136
|
+
}
|
|
137
|
+
if (TEMPLATE_SLOT.test(statement.text)) {
|
|
138
|
+
throw new Error(`cedar: the statement for "${name}" carries a Cedar template slot — AWS::BedrockAgentCore::Policy takes a static statement, and its Definition union has no template-linked arm. Write the ?principal/?resource slot as a concrete entity, or deploy the template through AWS::VerifiedPermissions::Policy instead.`);
|
|
139
|
+
}
|
|
140
|
+
if (statement.text.length < AGENTCORE_STATEMENT_MIN) {
|
|
141
|
+
throw new Error(`cedar: the statement for "${name}" is ${statement.text.length} characters and AgentCore's minimum is ${AGENTCORE_STATEMENT_MIN}.`);
|
|
142
|
+
}
|
|
143
|
+
if (statement.text.length > AGENTCORE_STATEMENT_MAX) {
|
|
144
|
+
throw new Error(`cedar: the statement for "${name}" is ${statement.text.length} characters and AgentCore's maximum is ${AGENTCORE_STATEMENT_MAX} — split it across policies, each with its own EnforcementMode.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── The seam ──────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* The policy text for one AgentCore policy — the exact string a `Statement`
|
|
150
|
+
* wants, on whichever arm it lands.
|
|
151
|
+
*
|
|
152
|
+
* `name` is the chant entity name; the `@id` annotation the statement carries
|
|
153
|
+
* is derived from it the same way the serializer derives it, so the statement
|
|
154
|
+
* in the policy engine can be matched back to the declaration that produced it.
|
|
155
|
+
*/
|
|
156
|
+
export function agentCoreStatement(name, source, options = {}) {
|
|
157
|
+
const statement = render(name, source, options);
|
|
158
|
+
assertEmbeddable(name, statement);
|
|
159
|
+
return statement.text;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The `Definition` property of `AWS::BedrockAgentCore::Policy`.
|
|
163
|
+
*
|
|
164
|
+
* The arm is chosen by what the policy is: a `Dogwood::TemporalPolicy`, or any
|
|
165
|
+
* props carrying a temporal clause, lands in `Definition.Policy`; plain Cedar
|
|
166
|
+
* lands in `Definition.Cedar`. `options.language` overrides in the one
|
|
167
|
+
* direction that is safe (see {@link AgentCoreEmbedOptions.language}).
|
|
168
|
+
*/
|
|
169
|
+
export function agentCorePolicyDefinition(name, source, options = {}) {
|
|
170
|
+
const statement = render(name, source, options);
|
|
171
|
+
assertEmbeddable(name, statement);
|
|
172
|
+
if (options.language === "cedar" && statement.temporal) {
|
|
173
|
+
throw new Error(`cedar: "${name}" has temporal clauses, so it cannot go in Definition.Cedar — that arm is plain Cedar, and AgentCore's Cedar parser has no \`when temporal\` rule. Drop \`language: "cedar"\` and it lands in Definition.Policy.`);
|
|
174
|
+
}
|
|
175
|
+
const dogwood = options.language === "dogwood" || (options.language === undefined && statement.temporal);
|
|
176
|
+
return dogwood ? { Policy: { Statement: statement.text } } : { Cedar: { Statement: statement.text } };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A policy and its rollout stage, ready to spread into the generated class.
|
|
180
|
+
*
|
|
181
|
+
* The staged-rollout pattern in one call, which is the point (#1660): shipping
|
|
182
|
+
* log-only and promoting to enforcing is a one-token diff, not a hand-edited
|
|
183
|
+
* `EnforcementMode` string somewhere else in the template. See
|
|
184
|
+
* ./enforcement.ts for why observe-before-enforce is the default reading of a
|
|
185
|
+
* temporal policy rather than an optional nicety.
|
|
186
|
+
*/
|
|
187
|
+
export function agentCoreStagedPolicy(name, source, stage, options = {}) {
|
|
188
|
+
return {
|
|
189
|
+
Name: agentCorePolicyName(name),
|
|
190
|
+
Definition: agentCorePolicyDefinition(name, source, options),
|
|
191
|
+
EnforcementMode: enforcementMode(stage),
|
|
192
|
+
...(options.description ? { Description: options.description } : {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Every required prop of `AWS::BedrockAgentCore::Policy`, plus the stage.
|
|
197
|
+
*
|
|
198
|
+
* `PolicyEngineId` is a string here rather than an AttrRef because this module
|
|
199
|
+
* does not know the aws lexicon's reference types. A project passes
|
|
200
|
+
* `engine.ref()` in place of the literal and TypeScript is satisfied by the
|
|
201
|
+
* generated class's own prop type, not by this one.
|
|
202
|
+
*/
|
|
203
|
+
export function agentCorePolicyResource(name, source, policyEngineId, options = {}) {
|
|
204
|
+
const { stage, ...embed } = options;
|
|
205
|
+
return {
|
|
206
|
+
PolicyEngineId: policyEngineId,
|
|
207
|
+
Name: agentCorePolicyName(name),
|
|
208
|
+
Definition: agentCorePolicyDefinition(name, source, embed),
|
|
209
|
+
...(stage ? { EnforcementMode: enforcementMode(stage) } : {}),
|
|
210
|
+
...(embed.description ? { Description: embed.description } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Every policy in a build, as one AgentCore `Definition` each, keyed by chant
|
|
215
|
+
* entity name.
|
|
216
|
+
*
|
|
217
|
+
* The whole-set form, and the one that walks references between declared
|
|
218
|
+
* entities — that is what `cedarPolicyRecords`/`dogwoodPolicyRecords` do. One
|
|
219
|
+
* definition per policy rather than one merged statement, because
|
|
220
|
+
* `EnforcementMode` is a per-policy dial and merging would throw it away; hand
|
|
221
|
+
* the map to {@link agentCorePolicyDefinition} instead when a single AgentCore
|
|
222
|
+
* policy really should carry the whole set.
|
|
223
|
+
*/
|
|
224
|
+
export function agentCorePolicySet(entities, options = {}) {
|
|
225
|
+
const out = {};
|
|
226
|
+
for (const { name, id, props } of cedarPolicyRecords(entities)) {
|
|
227
|
+
out[name] = agentCorePolicyDefinition(name, props, { ...options, policyId: id });
|
|
228
|
+
}
|
|
229
|
+
for (const { name, id, props } of dogwoodPolicyRecords(entities)) {
|
|
230
|
+
out[name] = agentCorePolicyDefinition(name, props, { ...options, policyId: id, language: "dogwood" });
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The chant entity name as an AgentCore `Name`.
|
|
236
|
+
*
|
|
237
|
+
* `Name` is create-only and matches `^[A-Za-z][A-Za-z0-9_]*$` — no hyphens,
|
|
238
|
+
* which is why it is not the kebab-case policy id the `@id` annotation carries.
|
|
239
|
+
* Checked rather than coerced: silently rewriting a name that is create-only
|
|
240
|
+
* would mean a later rename replaces the policy without anyone having asked.
|
|
241
|
+
*/
|
|
242
|
+
export function agentCorePolicyName(name) {
|
|
243
|
+
if (!AGENTCORE_NAME_PATTERN.test(name)) {
|
|
244
|
+
throw new Error(`cedar: "${name}" is not a legal AWS::BedrockAgentCore::Policy Name — it must match ${AGENTCORE_NAME_PATTERN.source} (letters, digits and underscores, starting with a letter).`);
|
|
245
|
+
}
|
|
246
|
+
if (name.length > 48) {
|
|
247
|
+
throw new Error(`cedar: the AgentCore policy Name "${name}" is ${name.length} characters and the maximum is 48.`);
|
|
248
|
+
}
|
|
249
|
+
return name;
|
|
250
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EnforcementMode` — the dial that makes a policy rollout observable before it
|
|
3
|
+
* is binding (#1660).
|
|
4
|
+
*
|
|
5
|
+
* `AWS::BedrockAgentCore::Policy` carries an optional `EnforcementMode` with
|
|
6
|
+
* exactly two values, and the CloudFormation schema says what they mean:
|
|
7
|
+
*
|
|
8
|
+
* > Whether the policy contributes to the enforce decision returned to Gateway.
|
|
9
|
+
* > LOG_ONLY policies are still evaluated but their decisions are observed
|
|
10
|
+
* > only, allowing customers to validate a policy against real traffic before
|
|
11
|
+
* > promoting it.
|
|
12
|
+
*
|
|
13
|
+
* That is observe-before-enforce, in the substrate, per policy — the same shape
|
|
14
|
+
* chant already has at the ops layer, and the reason epic #1646 picked
|
|
15
|
+
* AgentCore as the deployment target for the temporal dialect rather than
|
|
16
|
+
* inventing a staging mechanism. A temporal policy is the case that needs it
|
|
17
|
+
* most: `formerly within 24h …` cannot be reasoned about from the source alone,
|
|
18
|
+
* because whether it fires depends on traffic nobody has replayed yet.
|
|
19
|
+
*
|
|
20
|
+
* ### The two names, and why chant does not reuse AWS's
|
|
21
|
+
*
|
|
22
|
+
* The wire values are `LOG_ONLY` and `ACTIVE`. `ACTIVE` is a poor name for
|
|
23
|
+
* "enforcing" — it reads as "not disabled", and `LOG_ONLY` is active too; it is
|
|
24
|
+
* evaluated on every request. So the authoring vocabulary here is
|
|
25
|
+
* `"log-only"` / `"enforce"`, and {@link enforcementMode} is the one place the
|
|
26
|
+
* translation happens. The wire values are exported as well, for a caller that
|
|
27
|
+
* has one in hand from a live read.
|
|
28
|
+
*
|
|
29
|
+
* ### The showcase seam
|
|
30
|
+
*
|
|
31
|
+
* A staged policy rollout has to be demonstrable end to end: ship the policy
|
|
32
|
+
* log-only, watch what it would have denied, promote it. This module is that
|
|
33
|
+
* seam. The whole rollout is one argument —
|
|
34
|
+
* `agentCoreStagedPolicy(name, policy, stage)` — so the promotion is a
|
|
35
|
+
* one-token diff a reviewer can see, rather than a hand-edited string in a
|
|
36
|
+
* CloudFormation resource. Anything more elaborate (which environments are
|
|
37
|
+
* promoted, who decides) belongs to the caller's own config; this module's job
|
|
38
|
+
* is to make the two states nameable and the transition legible.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The wire values `EnforcementMode` takes, keyed by the stage they implement.
|
|
42
|
+
*
|
|
43
|
+
* A typed constant pair rather than a bare union so a caller reads
|
|
44
|
+
* `AGENTCORE_ENFORCEMENT.logOnly` at the call site and cannot typo the string.
|
|
45
|
+
*/
|
|
46
|
+
export const AGENTCORE_ENFORCEMENT = {
|
|
47
|
+
/** Evaluated on every request; its decision is observed, never returned. */
|
|
48
|
+
logOnly: "LOG_ONLY",
|
|
49
|
+
/** Contributes to the decision Gateway returns. AWS's own default. */
|
|
50
|
+
enforce: "ACTIVE",
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The `EnforcementMode` a stage deploys as.
|
|
54
|
+
*
|
|
55
|
+
* Omitting `EnforcementMode` entirely is legal and means `ACTIVE` — AWS's
|
|
56
|
+
* schema declares that default. This helper never returns `undefined`, so a
|
|
57
|
+
* policy that went through it says which stage it is in on the face of the
|
|
58
|
+
* template, and a diff that promotes one shows the change.
|
|
59
|
+
*/
|
|
60
|
+
export function enforcementMode(stage) {
|
|
61
|
+
return stage === "log-only" ? AGENTCORE_ENFORCEMENT.logOnly : AGENTCORE_ENFORCEMENT.enforce;
|
|
62
|
+
}
|
|
63
|
+
/** The stage a wire value came from — the inverse of {@link enforcementMode}. */
|
|
64
|
+
export function enforcementStage(mode) {
|
|
65
|
+
return mode === AGENTCORE_ENFORCEMENT.logOnly ? "log-only" : "enforce";
|
|
66
|
+
}
|
|
67
|
+
/** True when the policy's decision reaches Gateway rather than only the log. */
|
|
68
|
+
export function isEnforcing(mode) {
|
|
69
|
+
return enforcementStage(mode) === "enforce";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One line of prose for a stage, for a description or a plan summary.
|
|
73
|
+
*
|
|
74
|
+
* Kept here rather than at the call sites so every surface that explains a
|
|
75
|
+
* staged policy explains it the same way.
|
|
76
|
+
*/
|
|
77
|
+
export function describeStage(stage) {
|
|
78
|
+
return stage === "log-only"
|
|
79
|
+
? "Evaluated against real traffic; decisions are logged and do not affect the response."
|
|
80
|
+
: "Enforcing: this policy's decision reaches the gateway.";
|
|
81
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding embedded AgentCore policy statements in a build's emitted output
|
|
3
|
+
* (#1660).
|
|
4
|
+
*
|
|
5
|
+
* The DWD walls read emitted *text* rather than the in-memory model, for the
|
|
6
|
+
* reason `../dogwood/scan.ts` sets out: `chant audit` runs over artifacts chant
|
|
7
|
+
* did not write, and a wall that only fires on chant's own output is not a
|
|
8
|
+
* wall. An embedded statement is the same situation one lexicon further out —
|
|
9
|
+
* the statement lands in whatever the aws lexicon emitted, so this reads the
|
|
10
|
+
* emitted template rather than reaching for a resource class the cedar lexicon
|
|
11
|
+
* deliberately does not import.
|
|
12
|
+
*
|
|
13
|
+
* Structural, not CloudFormation-specific. The thing being looked for is an
|
|
14
|
+
* object with a `Definition.Policy.Statement` string, which is AgentCore's
|
|
15
|
+
* language-agnostic arm wherever it appears — in a CFN template's
|
|
16
|
+
* `Resources.<id>.Properties`, in a plan JSON, in a fixture. The nearest
|
|
17
|
+
* enclosing key is carried along as the name to blame, which for a CFN template
|
|
18
|
+
* is the logical id.
|
|
19
|
+
*/
|
|
20
|
+
import { isRecord } from "../policy-text.js";
|
|
21
|
+
function walk(node, logicalId, found) {
|
|
22
|
+
if (Array.isArray(node)) {
|
|
23
|
+
for (const item of node)
|
|
24
|
+
walk(item, logicalId, found);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!isRecord(node))
|
|
28
|
+
return;
|
|
29
|
+
const definition = node.Definition;
|
|
30
|
+
if (isRecord(definition)) {
|
|
31
|
+
for (const arm of ["Cedar", "Policy"]) {
|
|
32
|
+
const value = definition[arm];
|
|
33
|
+
if (isRecord(value) && typeof value.Statement === "string") {
|
|
34
|
+
found.push({ ...(logicalId ? { logicalId } : {}), arm, statement: value.Statement });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of Object.entries(node)) {
|
|
39
|
+
// `Properties` is CloudFormation's own wrapper, so it is not the name to
|
|
40
|
+
// blame — the logical id one level up is.
|
|
41
|
+
walk(value, key === "Properties" ? logicalId : key, found);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Every emitted text in a build, with the lexicon and filename that carried it. */
|
|
45
|
+
function emittedTexts(ctx) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
48
|
+
if (typeof output === "string") {
|
|
49
|
+
if (output.length > 0)
|
|
50
|
+
out.push({ lexicon, source: lexicon, text: output });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (typeof output.primary === "string" && output.primary.length > 0) {
|
|
54
|
+
out.push({ lexicon, source: lexicon, text: output.primary });
|
|
55
|
+
}
|
|
56
|
+
for (const [filename, content] of Object.entries(output.files ?? {})) {
|
|
57
|
+
if (typeof content === "string")
|
|
58
|
+
out.push({ lexicon, source: filename, text: content });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every AgentCore policy definition embedded in a build's emitted output.
|
|
65
|
+
*
|
|
66
|
+
* Non-JSON output is skipped rather than reported: most emitted files are not
|
|
67
|
+
* JSON, and a parse failure here says nothing about whether a statement is
|
|
68
|
+
* there.
|
|
69
|
+
*/
|
|
70
|
+
export function embeddedAgentCoreDefinitions(ctx) {
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const { lexicon, source, text } of emittedTexts(ctx)) {
|
|
73
|
+
const trimmed = text.trimStart();
|
|
74
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
75
|
+
continue;
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(text);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const found = [];
|
|
84
|
+
walk(parsed, undefined, found);
|
|
85
|
+
for (const hit of found)
|
|
86
|
+
results.push({ lexicon, source, ...hit });
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
/** Only the language-agnostic arm — the one a `.dw` statement travels in. */
|
|
91
|
+
export function embeddedAgentCorePolicyStatements(ctx) {
|
|
92
|
+
return embeddedAgentCoreDefinitions(ctx).filter((d) => d.arm === "Policy");
|
|
93
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policies that are simply *there* (#1652, chant #1278).
|
|
3
|
+
*
|
|
4
|
+
* For most lexicons ambient discovery is housekeeping — an unattached security
|
|
5
|
+
* group, an orphaned volume. For an authorization store it is a security
|
|
6
|
+
* finding. A policy in the store that no chant entity declares is a grant
|
|
7
|
+
* nobody in the source tree can see: it was added in the console, or by a
|
|
8
|
+
* previous tool, or by a teammate's script, and it is being evaluated on every
|
|
9
|
+
* request. `describeResources` structurally cannot report it, because it
|
|
10
|
+
* resolves outward from what was declared and this is precisely what was not.
|
|
11
|
+
*
|
|
12
|
+
* The reader reports the policy and its statement. It does not decide that an
|
|
13
|
+
* ambient `permit` is dangerous — that is a conclusion, and putting conclusions
|
|
14
|
+
* in observations is the mistake chant #1271 undid. What it does carry is the
|
|
15
|
+
* `effect`, because "which ambient policies are permits" is the first question
|
|
16
|
+
* anyone asks and re-parsing the statement to answer it is work every consumer
|
|
17
|
+
* would repeat.
|
|
18
|
+
*/
|
|
19
|
+
import { CEDAR_POLICY_TYPE } from "../serializer.js";
|
|
20
|
+
import { credentialsAvailable } from "./client.js";
|
|
21
|
+
import { ownershipFromDescription } from "./ownership.js";
|
|
22
|
+
import { loadLivePolicies, resolvePolicyStoreId } from "./store.js";
|
|
23
|
+
import { effectFromStatement } from "./statement.js";
|
|
24
|
+
/**
|
|
25
|
+
* The kinds this lexicon can enumerate beyond the declared estate.
|
|
26
|
+
*
|
|
27
|
+
* One, and it is the only kind cedar deploys. Declared separately from the
|
|
28
|
+
* reader so `chant search` can say `--ambient` is relevant to a policy query
|
|
29
|
+
* without paying for a scan to find out.
|
|
30
|
+
*/
|
|
31
|
+
export const AVP_AMBIENT_KINDS = [CEDAR_POLICY_TYPE];
|
|
32
|
+
/**
|
|
33
|
+
* Policies in the store that nothing declares and nothing already observed.
|
|
34
|
+
*
|
|
35
|
+
* Best-effort by contract: ambient discovery is additive, and a failure here
|
|
36
|
+
* must not sink a managed observation that already succeeded, so the whole scan
|
|
37
|
+
* degrades to `{}` rather than throwing. The managed answer is complete without
|
|
38
|
+
* any of this.
|
|
39
|
+
*/
|
|
40
|
+
export async function observeAvpAmbient(options) {
|
|
41
|
+
if (!options.kinds.includes(CEDAR_POLICY_TYPE))
|
|
42
|
+
return {};
|
|
43
|
+
const env = options.env ?? process.env;
|
|
44
|
+
const policyStoreId = resolvePolicyStoreId({
|
|
45
|
+
environment: options.environment,
|
|
46
|
+
...(options.policyStoreId ? { policyStoreId: options.policyStoreId } : {}),
|
|
47
|
+
...(options.entities ? { entities: options.entities } : {}),
|
|
48
|
+
env,
|
|
49
|
+
});
|
|
50
|
+
if (!policyStoreId || !credentialsAvailable(env))
|
|
51
|
+
return {};
|
|
52
|
+
let policies;
|
|
53
|
+
try {
|
|
54
|
+
policies = await loadLivePolicies({
|
|
55
|
+
policyStoreId,
|
|
56
|
+
...(options.client ? { client: options.client } : {}),
|
|
57
|
+
withStatements: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
// Two exclusions, because "managed" has two spellings here: the AVP policy id
|
|
64
|
+
// the observation recorded as a physicalId, and the Cedar id the source
|
|
65
|
+
// declares. A declared policy whose read failed has no physicalId, and
|
|
66
|
+
// calling it ambient would turn a hole into a security finding.
|
|
67
|
+
const observedIds = new Set(Object.values(options.observed)
|
|
68
|
+
.map((meta) => meta.physicalId)
|
|
69
|
+
.filter((id) => typeof id === "string" && id.length > 0));
|
|
70
|
+
const declaredIds = new Set(options.declaredPolicyIds ?? []);
|
|
71
|
+
const ambient = {};
|
|
72
|
+
for (const policy of policies) {
|
|
73
|
+
if (observedIds.has(policy.policyId))
|
|
74
|
+
continue;
|
|
75
|
+
if (policy.cedarPolicyId !== undefined && declaredIds.has(policy.cedarPolicyId))
|
|
76
|
+
continue;
|
|
77
|
+
const effect = policy.statement ? effectFromStatement(policy.statement) : undefined;
|
|
78
|
+
ambient[`policy/${policy.policyId}`] = {
|
|
79
|
+
type: CEDAR_POLICY_TYPE,
|
|
80
|
+
status: policy.policyType || "STATIC",
|
|
81
|
+
physicalId: policy.policyId,
|
|
82
|
+
...(policy.lastUpdatedDate ? { lastUpdated: policy.lastUpdatedDate } : {}),
|
|
83
|
+
ownership: ownershipFromDescription(policy.description),
|
|
84
|
+
ambient: true,
|
|
85
|
+
attributes: {
|
|
86
|
+
policyStoreId: policy.policyStoreId,
|
|
87
|
+
policyId: policy.policyId,
|
|
88
|
+
policyType: policy.policyType,
|
|
89
|
+
...(effect ? { effect } : {}),
|
|
90
|
+
...(policy.cedarPolicyId ? { cedarPolicyId: policy.cedarPolicyId } : {}),
|
|
91
|
+
...(policy.authoredDescription ? { description: policy.authoredDescription } : {}),
|
|
92
|
+
// The statement itself, so the consumer can judge the grant rather than
|
|
93
|
+
// trusting a summary this reader invented.
|
|
94
|
+
...(policy.statement ? { statement: policy.statement } : {}),
|
|
95
|
+
...(policy.createdDate ? { createdDate: policy.createdDate } : {}),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return ambient;
|
|
100
|
+
}
|