Skip to content

policy: cel-go's own policy compiler already implements the composition #548 sketches — variables, nested first-match rules, typed outputs, source-preserving composition, a conformance runner — evaluate it for the five surfaces before writing a module system #1488

Description

@picatz

Observed behavior

All five policy surfaces are lists of boolean CEL rules: TaskPolicyConfig (pkg/flowstate/v1/taskpolicy_config.go:19-36) is allow: []string, deny: []string, rule_cost_limit; netpolicy.Config, the auth assumption and secret policies and the file's own signals: follow the same shape (#548's table). Semantics are deny-first then allow-list, fail closed. #548 finding 2 records that nothing decomposes a policy ("no import, no named rule set, no per-tenant layer"), finding 3 that nothing tests one, and its sketch invents extends: with monotonic narrowing. #326 asks for one policy language and names composability and reuse as the thing to get right. #1468 asks for the test and explain verbs.

cel-go ships a policy compiler (policy/, its own Go module at f8478f1) that already answers most of finding 2: a YAML document with name, imports, and a rule holding ordered variables (lazily evaluated, memoized, referenced as variables.x), a match list evaluated top-down first-match, each entry a condition plus either an output or a nested rule with its own scope; outputs must agree in type across the graph, and if every output is conditional the compiled type is optional(T). The composer folds the document into one CEL AST (cel.bind for variables), preserving source positions into the compiled expression, with aggregate-output semantics (cel-expr/cel-go#1408) and a conformance runner with a YAML test-case format. Its environment is the same env.Config document the profile-as-data issue proposes rendering.

The shape it would give Flowstate, and the invariants it has to pass

A task-shape policy as a cel-policy document produces a decision value rather than a boolean per rule — for instance {"allow": false, "rule": "no-plugin-writes", "reason": "…"} — which is exactly what flow policy explain (#1468) and the audit record need, and which today's allow/deny lists cannot say. Deny-first is one match ordering; "no rule matched → deny" is optional.none() → deny at the boundary. Variables give the named sub-expressions #548 wants shared, without a second language.

What has to be checked rather than assumed:

  • Fail closed. A policy is refused at load unless its composed output type is optional(Decision), and none is deny on every surface (invariant 6).
  • Compile at load, bound composition. cel-policy composes into one AST; the node and nesting bounds the Limits issue proposes are what bound a forty-tenant document, and ExpressionNodeLimit is the right knob.
  • One evaluator. The composed AST is a cel.Ast; it must be planned and evaluated through Evaluator with Limits.programOptions(), not through the policy module's helpers.
  • Layering. cel-policy has no extends:; Policy: one vocabulary, composable modules, a test verb, and enforcement below the layer that can bypass it #548's monotonic narrowing (a layer may deny more and allow less) would sit above it as a composition of documents, which is a smaller thing to design than a module system from nothing.
  • Dependency footprint. policy/ is a separate module with its own go.mod; adopting it adds a dependency that must pass the supply-chain bar. The alternative — borrowing the document shape and semantics and writing the composer over cel.bind in-tree — is a few hundred lines and keeps the tree self-contained.
  • Migration. Today's allow:/deny: lists are a degenerate cel-policy document (each rule a match entry), so a rewrite is mechanical and the R3 edition machinery covers the file surfaces.

Desired outcome

A recorded decision: adopt cel-policy's document shape (with or without its Go module) as the one policy document for the five surfaces, with typed decision outputs and the conformance test format as the *.policy.test.yaml #1468 needs; or refuse it with the invariant it fails named. Either way #548's module-system sketch is re-scoped to what the decision leaves undone.

Acceptance criteria

Constraints and dependencies

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIdentity, authn/authz, tokens, policydesignkind/decisionOwner or design decision required before implementationsecurity

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions