Skip to main content
← Back to list
01Issue
FeatureClosedSwamp CLI
Assigneesstack72

Relationships

↑ child of #662

#670 serve-auth: sealed CEL grant-condition environment

Opened by stack72 · 6/17/2026

Parent

Sub-issue of #662 (serve authentication & authorization). Layer 1, item 3.

Summary

Add a third CEL surface — the grant-condition environment — for evaluating conditions attached to authorization grants. This environment is permanently sealed: no I/O, no extension registrations, no data/vault/env/file receivers. A condition is a pure function over (resource fields, principal context) that returns a boolean.

This is the foundation the AccessDecisionService (layer 1, item 4) will use to evaluate conditions like `tags.env == "staging"` or `owner.createdBy == principal.sub`.

Context: existing CEL surfaces

swamp has two CEL surfaces today, both in `src/infrastructure/cel/cel_evaluator.ts`:

  1. Internal surface (`CelEvaluator` class) — used in workflow conditions, data queries, definition evaluation. Has `file.`, `data.`, `model.`, `vault.`, `env.*` namespace receivers. Full I/O.
  2. Extension-author surface (`createExtensionCelEnvironment()`) — baseline with mixed-type arithmetic overloads only. Extensions register their own functions. No swamp internals.

The grant-condition environment is a third surface — neither of the above. It must NOT be added to the internal `CelEvaluator` class (which has I/O receivers) or the extension baseline (which is for extension authors). It is its own factory function.

What to build

Factory function

A new function (e.g., `createGrantConditionEnvironment()`) in the cel infrastructure that returns a cel-js `Environment` configured with:

Resource fields (flat, no wrapper object) — matching the `data.query()` predicate style where fields are bare:

  • Workflow resources: `name`, `tags`, `collective`
  • Model resources: `type`, `collective`
  • Data resources: `name`, `ns`, `tags`, `owner`
  • Access resources: `name`

These are the fields a condition can reference when the grant targets that resource kind. Example: a grant on `workflow:@acme/*` with condition `tags.env == "staging"` — the `tags` field comes from the workflow being checked.

Principal context (namespaced) — matching the `run.*` style:

  • `principal.sub` — stable subject identifier
  • `principal.email` — email (may be absent)
  • `principal.groups` — list of local group names
  • `principal.collectives` — list of IdP-asserted collectives

Example: `collective in principal.collectives` or `owner.createdBy == principal.sub`.

Arithmetic baseline — the same mixed-type overloads from `createExtensionCelEnvironment()`. Reuse, don't duplicate.

Nothing else. No `data.`, `file.`, `vault.`, `env.`, `model.*` receivers. No `registerFunction` or `registerType` calls beyond what the factory sets up. This environment is sealed at construction.

Write-time validation

A function that takes a CEL condition string and a resource kind, and validates:

  1. Parses — is it valid CEL syntax?
  2. Type-checks — do the referenced fields exist in the declared environment for that resource kind? A typo like `tag.env` (instead of `tags.env`) or a type mismatch (`tags.env > 5` where `tags.env` is a string) should fail at grant creation time, not silently never-match at evaluation time.
  3. Source length — reject conditions over 1KB (simple sanity cap for phase 1).

This validation will be called by the Grant model's `create` method (from #667, now merged) when a condition is present. Currently the condition field is stored as an unvalidated string — this issue adds the validation.

Evaluation function

A function that takes a compiled/parsed condition, a resource-fields map, and a principal context, and returns `boolean`. This is what the AccessDecisionService (item 4) will call per-rule.

Key invariants

The seal is permanent. No extension registrations, no host functions, no I/O — ever. This is a decided architectural boundary, not a phase-1 simplification. New predicates (CIDR matching, semver comparison) are added to the baseline by the swamp team, never by extensions. Dynamic facts enter as data (principal attributes, resource fields), never as functions.

Conditions are deterministic. Same inputs → same output. No randomness, no time functions, no external state. This is what makes the cost-bounding story in phase 2 possible — if the environment is sealed and deterministic, bounding its cost is straightforward.

Write-time validation catches errors at authoring, not at evaluation. A bad condition must fail when the admin creates the grant, not silently never-match in production.

The environment does NOT use `unlistedVariablesAreDyn: true`. Unlike the extension surface, the grant environment should declare its variables explicitly so that type checking catches references to non-existent fields. This is the key difference from the extension baseline.

Scope

  • New factory function in `src/infrastructure/cel/`
  • Validation function for write-time checking
  • Evaluation function for runtime use
  • Tests covering: valid conditions, type errors caught at validation, field availability per resource kind, principal context access, the seal (no I/O receivers available)
  • Wire the validation into the Grant model's `create` method (update the existing code from #667)

Out of scope

  • Cost-bounding (AST depth, comprehension nesting, cost budgets) — phase 2, only needed for rule packs
  • `matches()` regex complexity validation — phase 2
  • AccessDecisionService — layer 1, item 4 (depends on this)
  • CLI commands — layer 2

References

  • CEL evaluator: `src/infrastructure/cel/cel_evaluator.ts`
  • CEL tests: `src/infrastructure/cel/cel_evaluator_test.ts`
  • Expressions design: `design/expressions.md`
  • Grant model (condition stored as string): `src/domain/models/access/grant_model.ts`
  • Access value objects: `src/domain/access/`
02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED+ 1 MOREASSIGNEDCLASSIFICATION+ 1 MOREPR_MERGED

Closed

6/17/2026, 10:21:36 PM

No activity in this phase yet.

03Sludge Pulse
stack72 assigned stack726/17/2026, 6:44:23 PM
stack72 linked parent of #6626/17/2026, 10:20:31 PM

Sign in to post a ripple.