Policy Interface
Design: Policy Interface
Status: Final — Option A selected | Issue #38
| Version | Date | Summary |
|---|---|---|
| 1.1 | 2026-05-14 | Record decision (Option A); add framing section; rename options A/B; remove Option B |
| 1.0 | 2026-04-19 | Initial draft — compared typed accessor (Option A) and opaque marker (Option B) |
Decision: oam.Policy is a typed accessor interface with 18 methods. Reason:
compile-time verification, no type assertions in handler code, and explicit NoopPolicy
behaviour (no limits, no defaults, security-sensitive bools default-deny) are more important
than the flexibility of a marker interface for future policy types.
Scope: The Policy and Enforceable interface definitions in pkg/oam, how
*api.EnvironmentPolicy in crane satisfies Policy after migration, and how handler
code uses the interface. This does not cover TransformContext, handler registration,
or the pipeline execution loop.
Framing
Policy vs ClusterProfile
These are separate concerns with different owners:
- ClusterProfile — describes how the platform implements each trait (which ingress
controller, which certificate issuer). Written once per cluster by a platform operator.
Covered in
design-cluster-profile.md. - Policy — describes enforcement constraints and defaults applied to application components (max replicas, allowed registries, memory limits). Written per environment by a platform or security operator. Covered here.
The two inputs are orthogonal. A cluster profile says “ingress means Gateway API here”; a policy says “no component may request more than 2 replicas in staging”. ClusterProfile values flow into trait rendering; Policy values flow into component configuration enforcement.
Policy is launcher-native from day one
kurel build will accept a --policy flag pointing to a policy document. This means the
oam.Policy interface is a first-class launcher abstraction from Phase 1, not solely a
crane compatibility seam.
When no policy is supplied, launcher passes NoopPolicy — a concrete type that satisfies
oam.Policy with the following semantics:
- No enforced limits — all limit methods return nil or empty string
- No defaults applied — all default methods return nil or empty string
- Security-sensitive features denied by default — all security flag methods return false
This is intentional default-deny behaviour for security flags, not a “permit everything”
stance. Handlers always receive a non-nil Policy value; nil checks in handler code are
not needed or intended.
crane compatibility
crane’s *api.EnvironmentPolicy is the existing concrete policy type. After migration,
crane wires it into launcher by satisfying the oam.Policy interface. The question is how.
The interface must be rich enough to serve both crane’s EnvironmentPolicy and future
launcher-native policy document types that may have different enforcement semantics.
Background
Current state in crane
Component config types (e.g. WebserviceConfig, WorkerConfig) implement this interface.
The transformer calls ApplyPolicy after parsing each component, passing the environment
policy from the request.
Goal after migration
Where crane’s *api.EnvironmentPolicy satisfies Policy — so that migrated handlers
compile without the import path change breaking anything beyond the type signature.
Compatibility scope
The compatibility requirement is behavioral, not zero code change: migrated OAM fixtures must produce identical manifest output. Handler code will be updated as part of the migration (Phase 4 in the roadmap). The question is what shape the interface takes.
Option A — Typed Accessor Interface
Interface definition
Policy exposes typed getter methods corresponding to every piece of data that handlers
currently access via *api.EnvironmentPolicy.
NoopPolicy
How crane satisfies Policy
crane adds accessor methods to *api.EnvironmentPolicy. No adapter or wrapper struct is
needed — the existing type grows a method set:
How handler code changes
The change is mechanical: the parameter type changes from *api.EnvironmentPolicy to
oam.Policy, and field accesses become method calls:
No type assertions. No imports of crane’s api package in handler code.
Interface growth
If EnvironmentPolicy gains a new field (e.g. MaxPodCount), Policy must be explicitly
extended with a new method, and NoopPolicy must implement it. This is an intentional
gate — it ensures new policy fields are consciously exposed to the public interface.
The var _ oam.Policy = (*EnvironmentPolicy)(nil) compile check in crane’s file catches
any omission immediately.
Summary
- 19 methods (as defined above)
- crane gains ~20 accessor methods on
EnvironmentPolicy(pure boilerplate, no logic) - Handler code: method calls, no type assertions, no adapter imports
- Compiler verifies the contract at crane build time
- Interface must grow manually as
EnvironmentPolicygrows
Why Not an Opaque Marker Interface
The rejected alternative defined Policy as a single marker method (oamPolicy()),
with all data access via type assertions in handler code. It was rejected because:
- Handler code requires type assertions (to the adapter type or to local sub-interfaces) — no compiler verification of coverage
NoopPolicyhas no data methods; enforcement is silently skipped by failed assertions rather than by explicit zero-value returns — the “no policy = no constraints” behaviour is implicit, not self-documenting- A nil pointer wrapped in the interface (
(*NoopPolicy)(nil)) silently skips all enforcement without error - The
Policyinterface never grows, so the adapter accumulates data silently asEnvironmentPolicyevolves — no compile-time gate
The explicit interface growth of the typed accessor approach (every new policy field
requires a new method and a NoopPolicy stub) is an intentional gate, not a burden —
it ensures new policy data is consciously exposed to the public API surface.