@intentius/chant-lexicon-cedar 0.124.30 → 0.124.32

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.
Files changed (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Temporal windows — the one thing dogwood's past-only operators cannot omit.
3
+ *
4
+ * `formerly`, `previous` and `since` all take a mandatory `within <n><unit>`
5
+ * clause; the unit is one of `s`, `m`, `h`, `d` and nothing else. Upstream's
6
+ * `temporal/grammar.pest` spells that out:
7
+ *
8
+ * ```
9
+ * within = { "within" ~ within_payload }
10
+ * within_payload = { param_ref | (integer ~ time_unit) }
11
+ * time_unit = { "s" | "m" | "h" | "d" }
12
+ * ```
13
+ *
14
+ * Typing the window as a value rather than as free text is what makes
15
+ * "a `formerly` without a window" unrepresentable in the builders (#1658's
16
+ * third lint wall) — you cannot call {@link formerly} without one.
17
+ *
18
+ * The `param_ref` arm (`within ?w`) is legal only inside a macro body; it is
19
+ * modelled by {@link MacroWindow} rather than by this type, so an ordinary
20
+ * policy cannot accidentally emit a sigil the expander would reject.
21
+ */
22
+ const WINDOW_TEXT = /^(\d+)([smhd])$/;
23
+ const PARAM_TEXT = /^\?[A-Za-z_][A-Za-z0-9_]*$/;
24
+ /** True when the payload is `?w` rather than `1h`. */
25
+ export function isWindowParam(value) {
26
+ return "param" in value;
27
+ }
28
+ /**
29
+ * `within ?w` inside a macro body.
30
+ *
31
+ * Exported from `./macros.ts` too, which is where the sigil rules are
32
+ * documented and where a reader looking for it will be.
33
+ */
34
+ export function windowParam(param) {
35
+ if (!PARAM_TEXT.test(param)) {
36
+ throw new Error(`dogwood: a window parameter carries the "?" sigil — got "${param}"`);
37
+ }
38
+ return { param };
39
+ }
40
+ /** Seconds in one of each unit — the scale `max_window` comparisons run on. */
41
+ const UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 };
42
+ /** Narrow a {@link WindowLike} to the structured form, validating as it goes. */
43
+ export function window(value) {
44
+ if (typeof value !== "string") {
45
+ if (!Number.isInteger(value.value) || value.value < 0) {
46
+ throw new Error(`dogwood: a temporal window must be a non-negative integer, got ${String(value.value)}`);
47
+ }
48
+ if (!(value.unit in UNIT_SECONDS)) {
49
+ throw new Error(`dogwood: a temporal window unit must be s, m, h or d, got "${String(value.unit)}"`);
50
+ }
51
+ return { value: value.value, unit: value.unit };
52
+ }
53
+ const match = WINDOW_TEXT.exec(value);
54
+ if (!match) {
55
+ throw new Error(`dogwood: "${value}" is not a temporal window — write an integer and one of s, m, h, d (for example "1h")`);
56
+ }
57
+ return { value: Number(match[1]), unit: match[2] };
58
+ }
59
+ /** `1h` — the surface form, in a `within` clause or as a macro-call argument. */
60
+ export function renderWindow(value) {
61
+ const w = window(value);
62
+ return `${w.value}${w.unit}`;
63
+ }
64
+ /** A `within` payload: `1h`, or the `?w` a macro body defers to its call site. */
65
+ export function renderWindowValue(value) {
66
+ return isWindowParam(value) ? value.param : renderWindow(value);
67
+ }
68
+ /** Narrow a window argument, letting a `?w` parameter through unchanged. */
69
+ export function windowValue(value) {
70
+ return typeof value === "object" && "param" in value ? windowParam(value.param) : window(value);
71
+ }
72
+ /** The window in seconds, so `90m` and `1h` are comparable to a `max_window`. */
73
+ export function windowSeconds(value) {
74
+ const w = window(value);
75
+ return w.value * UNIT_SECONDS[w.unit];
76
+ }
77
+ /**
78
+ * Upstream's fallback cap. `ServiceSchema::defaults()` and an event schema
79
+ * with no `max_window` directive both look back at most 24 hours, per the
80
+ * comment atop `event_schema/grammar.pest` ("absent → a 24h default").
81
+ */
82
+ export const DEFAULT_MAX_WINDOW = { value: 24, unit: "h" };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * A starter policy pack for gates (chant#3182): rules a platform team writes
3
+ * once and puts on every gate, the way Sentinel or OPA policies run against a
4
+ * Terraform plan. Each builder returns one cedar `Policy` over the plan
5
+ * summary core puts in `context.plan` (`GatePlanSummary`, declared by
6
+ * `GATE_CEDAR_SCHEMA`), so an Op needs no context code of its own.
7
+ *
8
+ * ```ts
9
+ * import { gatePolicy, protectStateful, capDeletes, permitLowRiskAgents } from "@intentius/chant-lexicon-cedar";
10
+ *
11
+ * export const prodGate = gatePolicy("prod", [
12
+ * protectStateful(),
13
+ * capDeletes({ max: 5 }),
14
+ * permitLowRiskAgents({ maxChanges: 10 }),
15
+ * ]);
16
+ *
17
+ * gate("approve-prod", { plan: plan.out.planDigest, approval: { policy: prodGate, mode: "log-only" } });
18
+ * ```
19
+ *
20
+ * How the rules combine is Cedar's: a `forbid` beats every `permit`, and with
21
+ * no permit the answer is deny. The pack's forbids are the limits and its
22
+ * permits say when an agent may pass a gate on its own. A gate's quorum is
23
+ * unchanged by any of it, since a deny never removes a human's approval
24
+ * (`tallyGateApprovals`), and in `log-only` mode, the default, every decision
25
+ * is recorded and none binds, so a team can watch a rule against real plans
26
+ * before switching the gate to `enforce`.
27
+ *
28
+ * Every forbid fails closed: on a gate whose context has no `plan` (no change
29
+ * set behind the gate's `plan`), it applies. A limit nobody can check is not
30
+ * a reason to let an agent through.
31
+ */
32
+ import { Policy } from "./generated/index.js";
33
+ import { escapeCedarString } from "./policy-text.js";
34
+ import { GATE_AGENT_TYPE, PASS_GATE_ACTION } from "./gate-policy.js";
35
+ /**
36
+ * Resource types whose delete or replacement loses data: databases, buckets,
37
+ * volumes, queues with retention, and keys. Terraform-provider type names
38
+ * and the CloudFormation names the aws lexicon plans with.
39
+ */
40
+ export const STATEFUL_TYPES = [
41
+ // AWS, terraform
42
+ "aws_db_instance", "aws_rds_cluster", "aws_rds_cluster_instance", "aws_rds_global_cluster",
43
+ "aws_dynamodb_table", "aws_dynamodb_global_table", "aws_s3_bucket", "aws_efs_file_system",
44
+ "aws_ebs_volume", "aws_elasticache_cluster", "aws_elasticache_replication_group",
45
+ "aws_docdb_cluster", "aws_neptune_cluster", "aws_redshift_cluster", "aws_opensearch_domain",
46
+ "aws_elasticsearch_domain", "aws_msk_cluster", "aws_kinesis_stream", "aws_sqs_queue",
47
+ "aws_kms_key", "aws_secretsmanager_secret", "aws_backup_vault", "aws_fsx_lustre_file_system",
48
+ // AWS, CloudFormation
49
+ "AWS::RDS::DBInstance", "AWS::RDS::DBCluster", "AWS::RDS::GlobalCluster", "AWS::DynamoDB::Table",
50
+ "AWS::DynamoDB::GlobalTable", "AWS::S3::Bucket", "AWS::EFS::FileSystem", "AWS::EC2::Volume",
51
+ "AWS::ElastiCache::CacheCluster", "AWS::ElastiCache::ReplicationGroup", "AWS::DocDB::DBCluster",
52
+ "AWS::Neptune::DBCluster", "AWS::Redshift::Cluster", "AWS::OpenSearchService::Domain",
53
+ "AWS::MSK::Cluster", "AWS::Kinesis::Stream", "AWS::SQS::Queue", "AWS::KMS::Key",
54
+ "AWS::SecretsManager::Secret", "AWS::Backup::BackupVault",
55
+ // Google Cloud
56
+ "google_sql_database_instance", "google_sql_database", "google_storage_bucket",
57
+ "google_bigquery_dataset", "google_bigquery_table", "google_spanner_instance",
58
+ "google_spanner_database", "google_compute_disk", "google_filestore_instance",
59
+ "google_redis_instance", "google_kms_crypto_key", "google_bigtable_instance",
60
+ // Azure
61
+ "azurerm_mssql_server", "azurerm_mssql_database", "azurerm_postgresql_flexible_server",
62
+ "azurerm_mysql_flexible_server", "azurerm_storage_account", "azurerm_cosmosdb_account",
63
+ "azurerm_managed_disk", "azurerm_redis_cache", "azurerm_key_vault",
64
+ ];
65
+ // The generated scope types are narrowed to the project's own schema; these
66
+ // names are the gate schema's (`GATE_CEDAR_SCHEMA`).
67
+ const AGENT = GATE_AGENT_TYPE;
68
+ const PASS_GATE = PASS_GATE_ACTION;
69
+ const set = (values) => `[${values.map((v) => `"${escapeCedarString(v)}"`).join(", ")}]`;
70
+ function count(name, value, fallback) {
71
+ const n = value ?? fallback;
72
+ if (!Number.isInteger(n) || n < 0)
73
+ throw new Error(`${name} must be a whole number of at least 0, got ${n}`);
74
+ return n;
75
+ }
76
+ function nonEmpty(name, values) {
77
+ if (!values || values.length === 0 || values.some((v) => typeof v !== "string" || v === "")) {
78
+ throw new Error(`${name} needs at least one non-empty name`);
79
+ }
80
+ return values;
81
+ }
82
+ function principal(appliesTo, fallback) {
83
+ return (appliesTo ?? fallback) === "agents" ? { is: AGENT } : {};
84
+ }
85
+ /** A forbid on `PassGate` that applies when there is no plan summary, or when `condition` holds. `condition` reads `context.plan`. */
86
+ function forbid(id, appliesTo, condition, description) {
87
+ return new Policy({
88
+ effect: "forbid",
89
+ principal: principal(appliesTo, "everyone"),
90
+ action: { eq: PASS_GATE },
91
+ // Written as `unless`, so the validator sees `context has plan` guard the
92
+ // read of it (it tracks a `has` through `&&`): the forbid applies unless
93
+ // there is a plan and the condition does not hold.
94
+ unless: [`context has plan && !(${condition})`],
95
+ annotations: { id, description },
96
+ });
97
+ }
98
+ /** Forbid a plan that deletes more than `max` resources. Default id `cap-deletes`. */
99
+ export function capDeletes(options = {}) {
100
+ const max = count("capDeletes: max", options.max, 0);
101
+ return forbid(options.id ?? "cap-deletes", options.appliesTo, `context.plan.deletes > ${max}`, `A plan may delete at most ${max} resources.`);
102
+ }
103
+ /** Forbid a plan that replaces more than `max` resources. Default id `cap-replacements`. */
104
+ export function capReplacements(options = {}) {
105
+ const max = count("capReplacements: max", options.max, 0);
106
+ return forbid(options.id ?? "cap-replacements", options.appliesTo, `context.plan.replaces > ${max}`, `A plan may replace at most ${max} resources.`);
107
+ }
108
+ /**
109
+ * Forbid a plan that deletes or replaces a resource of a stateful type: an
110
+ * RDS instance, a DynamoDB table, a bucket. Default id `protect-stateful`.
111
+ */
112
+ export function protectStateful(options = {}) {
113
+ const types = set(nonEmpty("protectStateful: types", options.types ?? STATEFUL_TYPES));
114
+ const deletes = options.deletes ?? true;
115
+ const replacements = options.replacements ?? true;
116
+ if (!deletes && !replacements)
117
+ throw new Error("protectStateful: deletes and replacements are both off, so the rule forbids nothing");
118
+ const conditions = [
119
+ ...(deletes ? [`context.plan.deletedTypes.containsAny(${types})`] : []),
120
+ ...(replacements ? [`context.plan.replacedTypes.containsAny(${types})`] : []),
121
+ ];
122
+ return forbid(options.id ?? "protect-stateful", options.appliesTo, conditions.length === 1 ? conditions[0] : `(${conditions.join(" || ")})`, `A plan may not ${[deletes ? "delete" : "", replacements ? "replace" : ""].filter(Boolean).join(" or ")} a resource that holds state.`);
123
+ }
124
+ /** Forbid a plan that changes anything outside these regions. Default id `allow-regions`. */
125
+ export function allowRegions(options) {
126
+ const regions = nonEmpty("allowRegions: regions", options.regions);
127
+ return forbid(options.id ?? "allow-regions", options.appliesTo, `!${set(regions)}.containsAll(context.plan.regions)`, `A plan may change resources only in ${regions.join(", ")}.`);
128
+ }
129
+ /**
130
+ * Forbid a plan whose tagged creates do not all carry `keys`. Reads
131
+ * `tags`, `tags_all` and `labels`. Default id `require-create-tags`.
132
+ */
133
+ export function requireCreateTags(options) {
134
+ const keys = nonEmpty("requireCreateTags: keys", options.keys);
135
+ const missing = `(context.plan.taggedCreates > 0 && !context.plan.createTagKeys.containsAll(${set(keys)}))`;
136
+ return forbid(options.id ?? "require-create-tags", options.appliesTo, options.strict ? `(${missing} || context.plan.untaggedCreates > 0)` : missing, `Every created resource carries the tags ${keys.join(", ")}.`);
137
+ }
138
+ /**
139
+ * Permit an agent to pass a gate on its own for a plan that deletes and
140
+ * replaces nothing, changes at most `maxChanges` resources, and planned
141
+ * every member in full (no failed member, no hole). In `enforce` mode that
142
+ * permit passes the gate; any forbid in the same set still wins.
143
+ */
144
+ export function permitLowRiskAgents(options = {}) {
145
+ const max = count("permitLowRiskAgents: maxChanges", options.maxChanges, 10);
146
+ return new Policy({
147
+ effect: "permit",
148
+ principal: { is: AGENT },
149
+ action: { eq: PASS_GATE },
150
+ when: [
151
+ "context has plan && " +
152
+ `context.plan.deletes == 0 && context.plan.replaces == 0 && context.plan.changes <= ${max} && ` +
153
+ "context.plan.failed == 0 && context.plan.holes == 0",
154
+ ],
155
+ annotations: {
156
+ id: options.id ?? "permit-low-risk-agents",
157
+ description: `An agent may pass a plan that deletes and replaces nothing and changes at most ${max} resources.`,
158
+ },
159
+ });
160
+ }
161
+ /** Permit an agent to pass a plan that only changes tags or labels. Default id `permit-tag-only-agents`. */
162
+ export function permitTagOnlyAgents(options = {}) {
163
+ return new Policy({
164
+ effect: "permit",
165
+ principal: { is: AGENT },
166
+ action: { eq: PASS_GATE },
167
+ when: ["context has plan && context.plan.tagOnly && context.plan.failed == 0 && context.plan.holes == 0"],
168
+ annotations: {
169
+ id: options.id ?? "permit-tag-only-agents",
170
+ description: "An agent may pass a plan that only changes tags or labels.",
171
+ },
172
+ });
173
+ }
174
+ /**
175
+ * The pack as one record for `gatePolicy(name, starterGatePolicies(...))`:
176
+ * stateful types protected, deletes capped, regions and tags when given,
177
+ * and the two agent permits.
178
+ */
179
+ export function starterGatePolicies(options = {}) {
180
+ return {
181
+ protectStateful: protectStateful(options.statefulTypes ? { types: options.statefulTypes } : {}),
182
+ capDeletes: capDeletes({ max: options.maxDeletes ?? 0 }),
183
+ ...(options.maxReplacements !== undefined ? { capReplacements: capReplacements({ max: options.maxReplacements }) } : {}),
184
+ ...(options.regions ? { allowRegions: allowRegions({ regions: options.regions }) } : {}),
185
+ ...(options.requiredTags ? { requireCreateTags: requireCreateTags({ keys: options.requiredTags }) } : {}),
186
+ permitLowRiskAgents: permitLowRiskAgents(options.agentMaxChanges !== undefined ? { maxChanges: options.agentMaxChanges } : {}),
187
+ permitTagOnlyAgents: permitTagOnlyAgents(),
188
+ };
189
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Cedar as a gate's approval policy (chant#2508).
3
+ *
4
+ * Two halves, one per side of the build:
5
+ *
6
+ * - {@link gatePolicy} renders a set of cedar `Policy` declarables to Cedar
7
+ * text at authoring time and returns core's `GatePolicyRef`, plain data that
8
+ * lands in the Op's build output. A gate names it as `approval.policy`.
9
+ * - {@link evaluateGatePolicy} is what core's `loadGatePolicyEvaluator("cedar")`
10
+ * imports from `@intentius/chant-lexicon-cedar/gate-policy`. `chant approve`
11
+ * calls it once per approval with a `PassGate` request, through the same
12
+ * `cedar-wasm` the post-synth checks load (`./lint/post-synth/wasm-helpers.ts`).
13
+ *
14
+ * The request uses these entity types, so a policy is written against them:
15
+ *
16
+ * | Position | Entity | Attributes / parents |
17
+ * |-----------|------------------------------------------|----------------------|
18
+ * | principal | `Chant::Human::"<name>"` or `Chant::Agent::"<name>"` | parents: one `Chant::Role::"<role>"` per claimed role |
19
+ * | action | `Chant::Action::"PassGate"` | |
20
+ * | resource | `Chant::Gate::"<op>/<gate>"` | `op`, `gate` |
21
+ * | context | `planDigest` when the gate binds a plan, plus the gate's `approval.context` | |
22
+ *
23
+ * So `principal is Chant::Agent` picks out an agent, `principal in
24
+ * Chant::Role::"maintainer"` a role, and `context.risk == "low"` a declared
25
+ * plan attribute.
26
+ */
27
+ import { gatePolicyVersion, } from "@intentius/chant/op";
28
+ import { getProps, resolvePolicyId } from "./policy-text.js";
29
+ import { CEDAR_POLICY_TYPE, renderPolicyText } from "./serializer.js";
30
+ import { describeError, loadWasm, wasmLoadError } from "./lint/post-synth/wasm-helpers.js";
31
+ export const GATE_HUMAN_TYPE = "Chant::Human";
32
+ export const GATE_AGENT_TYPE = "Chant::Agent";
33
+ export const GATE_ROLE_TYPE = "Chant::Role";
34
+ export const GATE_RESOURCE_TYPE = "Chant::Gate";
35
+ /** `action == Chant::Action::"PassGate"`, ready for a `Policy`'s `action` scope. */
36
+ export const PASS_GATE_ACTION = 'Chant::Action::"PassGate"';
37
+ function entries(name, policies) {
38
+ if (Array.isArray(policies)) {
39
+ return policies.map((p, i) => [`${name}-${i}`, p]);
40
+ }
41
+ if (isDeclarable(policies))
42
+ return [[name, policies]];
43
+ return Object.entries(policies);
44
+ }
45
+ function isDeclarable(value) {
46
+ return typeof value === "object" && value !== null && "entityType" in value;
47
+ }
48
+ /**
49
+ * A gate policy set from cedar `Policy` declarables. `name` is recorded with
50
+ * every decision; ids come from each policy's `annotations.id`, else its
51
+ * record key, else `<name>-<index>`.
52
+ *
53
+ * Throws when a member is not a cedar policy, or when the rendered set does
54
+ * not parse, so a broken policy fails where it is written and not at the
55
+ * first `chant approve`.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * export const shipPolicy = gatePolicy("ship", {
60
+ * agentLowRisk: new Policy({
61
+ * effect: "permit",
62
+ * principal: { is: GATE_AGENT_TYPE },
63
+ * action: { eq: PASS_GATE_ACTION },
64
+ * when: ['context.risk == "low"'],
65
+ * }),
66
+ * });
67
+ * ```
68
+ */
69
+ export function gatePolicy(name, policies) {
70
+ const members = entries(name, policies);
71
+ if (members.length === 0)
72
+ throw new Error(`gatePolicy("${name}"): the policy set is empty`);
73
+ const texts = members.map(([logical, entity]) => {
74
+ if (entity.entityType !== CEDAR_POLICY_TYPE) {
75
+ throw new Error(`gatePolicy("${name}"): "${logical}" is a ${entity.entityType}, not a ${CEDAR_POLICY_TYPE}`);
76
+ }
77
+ const props = getProps(entity);
78
+ return renderPolicyText(resolvePolicyId(logical, props), props);
79
+ });
80
+ const text = texts.join("\n\n") + "\n";
81
+ const wasm = loadWasm();
82
+ if (wasm) {
83
+ const parsed = wasm.checkParsePolicySet({ staticPolicies: text });
84
+ if (parsed.type === "failure") {
85
+ throw new Error(`gatePolicy("${name}"): the policy set does not parse: ${parsed.errors.map(describeError).join("; ")}`);
86
+ }
87
+ }
88
+ return { kind: "gate-policy", lexicon: "cedar", name, version: gatePolicyVersion(text), text };
89
+ }
90
+ /** A context value as Cedar JSON. Cedar has no floats or null, so those become strings or are dropped. */
91
+ function cedarValue(value) {
92
+ if (typeof value === "string" || typeof value === "boolean")
93
+ return value;
94
+ if (typeof value === "number")
95
+ return Number.isInteger(value) ? value : String(value);
96
+ if (Array.isArray(value)) {
97
+ return value.map(cedarValue).filter((v) => v !== undefined);
98
+ }
99
+ if (typeof value === "object" && value !== null) {
100
+ const out = {};
101
+ for (const [k, v] of Object.entries(value)) {
102
+ const converted = cedarValue(v);
103
+ if (converted !== undefined)
104
+ out[k] = converted;
105
+ }
106
+ return out;
107
+ }
108
+ return undefined;
109
+ }
110
+ /**
111
+ * The set as a record keyed by each policy's `@id`. Handed the text whole,
112
+ * cedar-wasm names policies by position (`policy0`), and a recorded decision
113
+ * would then name a rule nobody wrote. A policy with no `@id` keeps its
114
+ * positional name. Exported for `./gate-schema.ts`, so validation names rules the same way.
115
+ */
116
+ export function keyedById(wasm, policy) {
117
+ const parts = wasm.policySetTextToParts(policy.text);
118
+ if (parts.type === "failure") {
119
+ throw new Error(`policy "${policy.name}" does not parse: ${parts.errors.map(describeError).join("; ")}`);
120
+ }
121
+ const keyed = {};
122
+ parts.policies.forEach((text, index) => {
123
+ const id = /@id\(\s*"((?:[^"\\]|\\.)*)"\s*\)/.exec(text)?.[1] ?? `policy${index}`;
124
+ if (id in keyed)
125
+ throw new Error(`policy "${policy.name}" has two policies with id "${id}"`);
126
+ keyed[id] = text;
127
+ });
128
+ return keyed;
129
+ }
130
+ /**
131
+ * Evaluate one `PassGate` request. Called by core through
132
+ * `loadGatePolicyEvaluator("cedar")`; see the module doc for the entity model.
133
+ *
134
+ * Throws when the wasm cannot be loaded or the request cannot be evaluated at
135
+ * all, so `chant approve` refuses rather than recording a decision nobody
136
+ * made. A policy that errors on this request is reported in `errors` and does
137
+ * not apply, which is Cedar's own rule.
138
+ */
139
+ export function evaluateGatePolicy(policy, request) {
140
+ const wasm = loadWasm();
141
+ if (!wasm)
142
+ throw new Error(`@cedar-policy/cedar-wasm could not be loaded: ${wasmLoadError()}`);
143
+ const principalType = request.principal.kind === "agent" ? GATE_AGENT_TYPE : GATE_HUMAN_TYPE;
144
+ const roles = [...new Set(request.principal.roles)];
145
+ const principal = { type: principalType, id: request.principal.name };
146
+ const resource = { type: GATE_RESOURCE_TYPE, id: `${request.resource.op}/${request.resource.gate}` };
147
+ const answer = wasm.isAuthorized({
148
+ principal,
149
+ action: { type: "Chant::Action", id: request.action },
150
+ resource,
151
+ context: (cedarValue(request.context) ?? {}),
152
+ policies: { staticPolicies: keyedById(wasm, policy) },
153
+ entities: [
154
+ { uid: principal, attrs: {}, parents: roles.map((id) => ({ type: GATE_ROLE_TYPE, id })) },
155
+ ...roles.map((id) => ({ uid: { type: GATE_ROLE_TYPE, id }, attrs: {}, parents: [] })),
156
+ { uid: resource, attrs: { op: request.resource.op, gate: request.resource.gate }, parents: [] },
157
+ ],
158
+ });
159
+ if (answer.type === "failure") {
160
+ throw new Error(`policy "${policy.name}" could not be evaluated: ${answer.errors.map(describeError).join("; ")}`);
161
+ }
162
+ const { decision, diagnostics } = answer.response;
163
+ return {
164
+ decision,
165
+ determining: [...diagnostics.reason].sort(),
166
+ errors: diagnostics.errors.map((e) => `${e.policyId}: ${describeError(e.error)}`),
167
+ };
168
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The Cedar schema of a gate's `PassGate` request (chant#3182).
3
+ *
4
+ * {@link GATE_CEDAR_SCHEMA} declares the `Chant` namespace `evaluateGatePolicy`
5
+ * builds requests in (`./gate-policy.ts`), with the plan summary core puts in
6
+ * `context.plan` (`@intentius/chant/op`'s `GatePlanSummary`) as the common type
7
+ * `Chant::PlanSummary`. Two uses:
8
+ *
9
+ * - {@link validateGatePolicy} checks a gate policy set against it, so a rule
10
+ * that reads `context.plan.deleteCount` (a field that does not exist) fails
11
+ * where it is written rather than erroring on every request.
12
+ * - A project that writes gate policies with typed `Policy` scopes puts this
13
+ * text in its own `.cedarschema` (or hands {@link gateCedarSchema}'s output
14
+ * to `new Schema({ text })`), and the generated scope types then name
15
+ * `Chant::Agent` and `Chant::Action::"PassGate"` without a cast.
16
+ *
17
+ * A gate's own `approval.context` keys differ per gate, so the schema declares
18
+ * only `planDigest` and `plan` unless {@link gateCedarSchema} is told the rest.
19
+ * Every context attribute is optional: a gate with no plan, or a plan step
20
+ * that returned no change set, has neither.
21
+ */
22
+ import { loadWasm, validatePolicySet, wasmLoadError } from "./lint/post-synth/wasm-helpers.js";
23
+ import { keyedById } from "./gate-policy.js";
24
+ /**
25
+ * Each `GatePlanSummary` field as a Cedar type. Typed as a record over the
26
+ * summary's keys, so a field added to the summary in core fails to compile
27
+ * here until the schema says what it is.
28
+ */
29
+ export const GATE_PLAN_SUMMARY_CEDAR_TYPES = {
30
+ members: "Set<String>",
31
+ membersChanged: "Set<String>",
32
+ failedMembers: "Set<String>",
33
+ failed: "Long",
34
+ holes: "Long",
35
+ entries: "Long",
36
+ changes: "Long",
37
+ creates: "Long",
38
+ updates: "Long",
39
+ replaces: "Long",
40
+ deletes: "Long",
41
+ reads: "Long",
42
+ noOps: "Long",
43
+ forgets: "Long",
44
+ types: "Set<String>",
45
+ createdTypes: "Set<String>",
46
+ updatedTypes: "Set<String>",
47
+ replacedTypes: "Set<String>",
48
+ deletedTypes: "Set<String>",
49
+ deleted: "Set<String>",
50
+ replaced: "Set<String>",
51
+ regions: "Set<String>",
52
+ scopes: "Set<String>",
53
+ lexicons: "Set<String>",
54
+ taggedCreates: "Long",
55
+ untaggedCreates: "Long",
56
+ createTagKeys: "Set<String>",
57
+ tagOnly: "Bool",
58
+ truncated: "Bool",
59
+ };
60
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
61
+ /** The schema text, with the gate's own context attributes when given. */
62
+ export function gateCedarSchema(options = {}) {
63
+ const extra = Object.entries(options.context ?? {});
64
+ for (const [key, type] of extra) {
65
+ if (!IDENT.test(key))
66
+ throw new Error(`gateCedarSchema: context attribute "${key}" is not a Cedar identifier`);
67
+ if (key === "plan" || key === "planDigest")
68
+ throw new Error(`gateCedarSchema: context attribute "${key}" is chant's own`);
69
+ if (type.trim() === "")
70
+ throw new Error(`gateCedarSchema: context attribute "${key}" has no type`);
71
+ }
72
+ const summary = Object.entries(GATE_PLAN_SUMMARY_CEDAR_TYPES)
73
+ .map(([k, t]) => ` "${k}": ${t},`)
74
+ .join("\n");
75
+ const context = [
76
+ ` "planDigest"?: String,`,
77
+ ` "plan"?: PlanSummary,`,
78
+ ...extra.map(([k, t]) => ` "${k}"?: ${t},`),
79
+ ].join("\n");
80
+ return `// The request chant builds for a gate approval (chant#2508, #3182).
81
+ namespace Chant {
82
+ // context.plan: the plan's change-set summary (GatePlanSummary).
83
+ type PlanSummary = {
84
+ ${summary}
85
+ };
86
+
87
+ // A role a human or agent claimed when approving.
88
+ entity Role;
89
+ entity Human in [Role];
90
+ entity Agent in [Role];
91
+ // "<op>/<gate>".
92
+ entity Gate = {
93
+ "op": String,
94
+ "gate": String,
95
+ };
96
+
97
+ action PassGate appliesTo {
98
+ principal: [Human, Agent],
99
+ resource: [Gate],
100
+ context: {
101
+ ${context}
102
+ }
103
+ };
104
+ }
105
+ `;
106
+ }
107
+ /** The schema with no gate-specific context: `planDigest` and `plan` only. */
108
+ export const GATE_CEDAR_SCHEMA = gateCedarSchema();
109
+ /**
110
+ * Validate a gate policy set against the gate schema. Throws when the wasm
111
+ * cannot be loaded or validation cannot run at all (a schema that does not
112
+ * parse); findings come back sorted.
113
+ */
114
+ export function validateGatePolicy(policy, options = {}) {
115
+ const wasm = loadWasm();
116
+ if (!wasm)
117
+ throw new Error(`@cedar-policy/cedar-wasm could not be loaded: ${wasmLoadError()}`);
118
+ const outcome = validatePolicySet(wasm, { staticPolicies: keyedById(wasm, policy) }, gateCedarSchema(options));
119
+ if (outcome.failure)
120
+ throw new Error(`policy "${policy.name}" could not be validated: ${outcome.failure}`);
121
+ return { errors: outcome.errors, warnings: outcome.warnings };
122
+ }
@@ -0,0 +1,94 @@
1
+ import { createProperty, createResource } from "./runtime.js";
2
+ /**
3
+ * The policy resource. Declare one per policy:
4
+ *
5
+ * ```ts
6
+ * export const allowOwnerRead = new Policy({
7
+ * principal: { is: "App::User" },
8
+ * action: { eq: ReadAction },
9
+ * when: ['resource.owner == principal'],
10
+ * });
11
+ * ```
12
+ */
13
+ export const Policy = createResource("Cedar::Policy", "cedar", {});
14
+ export const Application = createResource("App::Application", "cedar", {});
15
+ export const ApplicationAttributes = createProperty("App::Application.Attributes", "cedar");
16
+ export const Document = createResource("App::Document", "cedar", {});
17
+ export const DocumentAttributes = createProperty("App::Document.Attributes", "cedar");
18
+ export const Folder = createResource("App::Folder", "cedar", {});
19
+ export const FolderAttributes = createProperty("App::Folder.Attributes", "cedar");
20
+ export const Group = createResource("App::Group", "cedar", {});
21
+ export const GroupAttributes = createProperty("App::Group.Attributes", "cedar");
22
+ export const ServiceAccount = createResource("App::ServiceAccount", "cedar", {});
23
+ export const ServiceAccountAttributes = createProperty("App::ServiceAccount.Attributes", "cedar");
24
+ export const Team = createResource("App::Team", "cedar", {});
25
+ export const TeamAttributes = createProperty("App::Team.Attributes", "cedar");
26
+ export const User = createResource("App::User", "cedar", {});
27
+ export const UserAttributes = createProperty("App::User.Attributes", "cedar");
28
+ /**
29
+ * `App::Action::"admin"`.
30
+ * Principals: App::User.
31
+ * Resources: App::Application.
32
+ */
33
+ export const AdminAction = "App::Action::\"admin\"";
34
+ export const AdminContext = createProperty("App::Action::\"admin\".Context", "cedar");
35
+ /**
36
+ * `App::Action::"approve"`.
37
+ * Principals: App::User.
38
+ * Resources: App::Document.
39
+ */
40
+ export const ApproveAction = "App::Action::\"approve\"";
41
+ export const ApproveContext = createProperty("App::Action::\"approve\".Context", "cedar");
42
+ /**
43
+ * `App::Action::"comment"`.
44
+ * Principals: App::User.
45
+ * Resources: App::Document, App::Folder.
46
+ */
47
+ export const CommentAction = "App::Action::\"comment\"";
48
+ export const CommentContext = createProperty("App::Action::\"comment\".Context", "cedar");
49
+ /**
50
+ * `App::Action::"create"`.
51
+ * Principals: App::User.
52
+ * Resources: App::Document, App::Folder.
53
+ */
54
+ export const CreateAction = "App::Action::\"create\"";
55
+ export const CreateContext = createProperty("App::Action::\"create\".Context", "cedar");
56
+ /**
57
+ * `App::Action::"delete"`.
58
+ * Principals: App::User.
59
+ * Resources: App::Document.
60
+ */
61
+ export const DeleteAction = "App::Action::\"delete\"";
62
+ export const DeleteContext = createProperty("App::Action::\"delete\".Context", "cedar");
63
+ /**
64
+ * `App::Action::"list"`.
65
+ * Principals: App::ServiceAccount, App::User.
66
+ * Resources: App::Document, App::Folder.
67
+ */
68
+ export const ListAction = "App::Action::\"list\"";
69
+ export const ListContext = createProperty("App::Action::\"list\".Context", "cedar");
70
+ /**
71
+ * `App::Action::"read"`.
72
+ * Principals: App::ServiceAccount, App::User.
73
+ * Resources: App::Document, App::Folder.
74
+ */
75
+ export const ReadAction = "App::Action::\"read\"";
76
+ export const ReadContext = createProperty("App::Action::\"read\".Context", "cedar");
77
+ /**
78
+ * `App::Action::"share"`.
79
+ * Principals: App::User.
80
+ * Resources: App::Document.
81
+ */
82
+ export const ShareAction = "App::Action::\"share\"";
83
+ export const ShareContext = createProperty("App::Action::\"share\".Context", "cedar");
84
+ /**
85
+ * `App::Action::"write"`.
86
+ * Principals: App::User.
87
+ * Resources: App::Document, App::Folder.
88
+ */
89
+ export const WriteAction = "App::Action::\"write\"";
90
+ export const WriteContext = createProperty("App::Action::\"write\".Context", "cedar");
91
+ /** Every generated action constant, for exhaustive iteration. */
92
+ export const ALL_ACTIONS = [AdminAction, ApproveAction, CommentAction, CreateAction, DeleteAction, ListAction, ReadAction, ShareAction, WriteAction];
93
+ /** Every entity type name the schema declares. */
94
+ export const ALL_ENTITY_TYPES = ["App::Application", "App::Document", "App::Folder", "App::Group", "App::ServiceAccount", "App::Team", "App::User"];
@@ -0,0 +1,2 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ export { createProperty, createResource } from "@intentius/chant/runtime";