You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
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.
If adopted: one surface (task shape, the smallest) migrates first, flow policy test runs its conformance format, and flow policy explain reports the matched match entry with source position.
Observed behavior
All five policy surfaces are lists of boolean CEL rules:
TaskPolicyConfig(pkg/flowstate/v1/taskpolicy_config.go:19-36) isallow: []string,deny: []string,rule_cost_limit;netpolicy.Config, the auth assumption and secret policies and the file's ownsignals: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 inventsextends: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 atf8478f1) that already answers most of finding 2: a YAML document withname,imports, and aruleholding orderedvariables(lazily evaluated, memoized, referenced asvariables.x), amatchlist evaluated top-down first-match, each entry aconditionplus either anoutputor a nestedrulewith its own scope; outputs must agree in type across the graph, and if every output is conditional the compiled type isoptional(T). The composer folds the document into one CEL AST (cel.bindfor 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 sameenv.Configdocument 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 whatflow policy explain(#1468) and the audit record need, and which today's allow/deny lists cannot say. Deny-first is onematchordering; "no rule matched → deny" isoptional.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:
optional(Decision), andnoneis deny on every surface (invariant 6).Limitsissue proposes are what bound a forty-tenant document, andExpressionNodeLimitis the right knob.cel.Ast; it must be planned and evaluated throughEvaluatorwithLimits.programOptions(), not through thepolicymodule's helpers.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.policy/is a separate module with its owngo.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 overcel.bindin-tree — is a few hundred lines and keeps the tree self-contained.allow:/deny:lists are a degenerate cel-policy document (each rule amatchentry), 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
docs/compares today's allow/deny shape, the cel-policy shape, and Policy: one vocabulary, composable modules, a test verb, and enforcement below the layer that can bypass it #548'sextends:sketch against invariants 2, 5 and 6 with one worked policy (the forty-tenant egress file Policy: one vocabulary, composable modules, a test verb, and enforcement below the layer that can bypass it #548 describes) in each form.flow policy testruns its conformance format, andflow policy explainreports the matchedmatchentry with source position.flow policy testandflow policy explainover the five activations that already exist #1468 are updated with the reason so the question is not reopened by the next reader of the cel-go tree.Constraints and dependencies
Limitsissue come first; a composed policy needs one base environment and one set of bounds.signals:policy (Design: one policy language for signal authorization and beyond. CEL coherence instead of bespoke YAML keys like distinct_from_starter #326) is a file surface with edition rules; the operator files are not. The decision may differ between them and should say so.