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 21 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 a
downstream runtime’s EnvironmentPolicy type satisfies Policy, 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
downstream 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.
Downstream compatibility
A downstream runtime’s EnvironmentPolicy is an existing concrete policy type. Such a
runtime wires it into launcher by satisfying the oam.Policy interface. The question is how.
The interface must be rich enough to serve both a downstream EnvironmentPolicy and future
launcher-native policy document types that may have different enforcement semantics.
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.
Where a downstream *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.
// launcher/pkg/oam/policy.go// Policy provides environment-level constraints and defaults for OAM handlers.// Handlers call its methods to apply limits and defaults; they must not type-assert.typePolicyinterface {
// Enforced limits — nil / empty string means no limitMaxReplicas() *int32MaxCPU() stringMaxMemory() stringMaxStorageSize() stringAllowedRegistries() []string// Defaults — nil / empty string means "no default; leave OAM value as-is"DefaultReplicas() *int32DefaultCPURequest() stringDefaultMemoryRequest() stringDefaultCPULimit() stringDefaultMemoryLimit() stringDefaultStorageSize() stringDefaultScalerMinReplicas() *int32DefaultScalerMaxReplicas() *int32// Security flags — false is the zero value (default-deny)AllowHostNetwork() boolAllowPrivileged() boolAllowHostPID() boolAllowHostIPC() boolAllowHostPathVolumes() bool// Capability constraints — nil means unconstrainedAllowedCapabilities() []stringForbiddenCapabilities() []stringRequiredCapabilities() []string}
// Enforceable is implemented by component configs that accept policy enforcement.typeEnforceableinterface {
ApplyPolicy(policyPolicy) error}
No type assertions. No imports of the downstream 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 the downstream file
catches any omission immediately.
Summary
19 methods (as defined above)
the downstream type 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 the downstream build time
Interface must grow manually as EnvironmentPolicy grows
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
NoopPolicy has 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 Policy interface never grows, so the adapter accumulates data silently as EnvironmentPolicy evolves — 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.