Skip to content

Commit a5f562d

Browse files
committed
designs/4294: add value injection proposal
Signed-off-by: Roger Peppe <rogpeppe@gmail.com> Change-Id: Ibbf7690473e9c20949dbe8cadc723a8c164e5ac2 Reviewed-on: https://cue.gerrithub.io/c/cue-lang/proposal/+/1232931 Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com> TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
1 parent 64f9226 commit a5f562d

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

designs/4294-value-injection.md

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Proposal: CUE Value Injection
2+
3+
Roger Peppe
4+
5+
Date: 2026-03-05
6+
7+
Discussion at https://github.com/cue-lang/cue/discussions/4294.
8+
9+
## Abstract
10+
11+
We propose an injection mechanism that allows Go code to supply CUE
12+
values to specific locations in CUE source, identified by `@inject`
13+
attributes. This provides a controlled, explicit way for host programs
14+
to parameterize CUE configurations with values that cannot be expressed
15+
in CUE alone, such as user-provided functions and validators.
16+
17+
## Background
18+
19+
Go programs that use CUE often need to supply values that originate
20+
outside the CUE configuration. Today this is done primarily through
21+
`Value.FillPath`, which works well for simple cases but has
22+
limitations. The calling code must know the exact path where a value
23+
should be placed, which creates tight coupling between Go code and CUE
24+
structure. When a CUE package wants to declare that it expects an
25+
externally-provided value, there is no standard way to express this
26+
intent.
27+
28+
The companion proposal for [user-provided functions and
29+
validators](./4293-user-functions-and-validators.md) makes injection
30+
particularly important. Functions and validators cannot be expressed in
31+
CUE syntax -- they must come from Go. A CUE package that uses a
32+
custom function needs a way to declare the dependency and have it
33+
fulfilled by the host program.
34+
35+
CUE already has the `@extern` attribute mechanism for integrating with
36+
external code. The injection mechanism builds on this existing
37+
infrastructure, using `@extern(inject)` to opt in at the file level
38+
and `@inject` attributes to mark individual fields.
39+
40+
## Proposal
41+
42+
### The Injector type
43+
44+
We add a new type to the `cue/cuecontext` package:
45+
46+
```go
47+
type Injector struct { ... }
48+
49+
func NewInjector() *Injector
50+
```
51+
52+
An `Injector` maintains a registry of named CUE values and an
53+
authorization function that controls which injections are permitted.
54+
55+
### Registering values
56+
57+
```go
58+
func (j *Injector) Register(name string, v cue.Value)
59+
```
60+
61+
`Register` associates a CUE value with a name. If a value is already
62+
registered under that name, the new value is unified with the existing
63+
one. This allows multiple registrations to accumulate constraints
64+
incrementally:
65+
66+
```go
67+
j := cuecontext.NewInjector()
68+
j.Register("example.com/config", ctx.CompileString(`{port: int}`))
69+
j.Register("example.com/config", ctx.CompileString(`{port: 8080}`))
70+
// The registered value is now {port: 8080}
71+
```
72+
73+
### Authorization
74+
75+
Injection is a potentially security-sensitive operation: it allows Go
76+
code to unify values into CUE configurations. The `Injector` requires
77+
explicit authorization before any injection can take effect.
78+
79+
```go
80+
func (j *Injector) Allow(f func(inst *build.Instance, name string) error)
81+
func (j *Injector) AllowAll()
82+
```
83+
84+
`Allow` sets a function that is called for each injection site. It
85+
receives the build instance (identifying the CUE package) and the
86+
injection name. Returning a non-nil error prevents the injection.
87+
`AllowAll` is a convenience that permits all injections.
88+
89+
If no authorization function is set, all injections fail with an
90+
error. This fail-closed default ensures that injections cannot happen
91+
accidentally.
92+
93+
### Connecting to a Context
94+
95+
```go
96+
func Inject(j *Injector) Option
97+
```
98+
99+
`Inject` returns a `cuecontext.Option` that registers the injector
100+
with a CUE context. The injector is activated as an interpreter for
101+
`@extern(inject)` attributes.
102+
103+
```go
104+
j := cuecontext.NewInjector()
105+
j.AllowAll()
106+
ctx := cuecontext.New(cuecontext.Inject(j))
107+
```
108+
109+
### CUE-side syntax
110+
111+
On the CUE side, a file opts into injection with a file-level
112+
`@extern(inject)` attribute. Individual fields are marked with
113+
`@inject(name="...")`:
114+
115+
```cue
116+
@extern(inject)
117+
118+
package myapp
119+
120+
// validate is a user-provided validator, injected from Go.
121+
validate: _ @inject(name="example.com/myapp/validate")
122+
```
123+
124+
When the injector has a value registered for the given name, that value
125+
is unified with the field's CUE-side value. When no value is registered,
126+
the field retains its original value; a default value could be provided
127+
but that's unlikely to be very useful unless there's another
128+
suitable function to use.
129+
130+
Without the file-level `@extern(inject)` attribute, `@inject`
131+
attributes are silently ignored. This ensures that injection is an
132+
explicit opt-in at the file level.
133+
134+
### Combining with user-provided functions
135+
136+
The primary motivating use case is injecting functions and validators
137+
from the companion [user-provided functions
138+
proposal](./4293-user-functions-and-validators.md):
139+
140+
```go
141+
j := cuecontext.NewInjector()
142+
j.AllowAll()
143+
ctx := cuecontext.New(cuecontext.Inject(j))
144+
145+
j.Register("example.com/myapp/validate",
146+
cue.ValidatorFunc(func(s string) error {
147+
if !isValidHostname(s) {
148+
return fmt.Errorf("invalid hostname: %q", s)
149+
}
150+
return nil
151+
}))
152+
```
153+
154+
```cue
155+
@extern(inject)
156+
157+
package myapp
158+
159+
#validHost: _ @inject(name="example.com/myapp/validate")
160+
161+
servers: [...{
162+
host: #validHost & string
163+
}]
164+
```
165+
166+
This pattern cleanly separates concerns: the CUE code declares that it
167+
expects an external validator and specifies where it should be applied,
168+
while the Go code provides the implementation.
169+
170+
## Rationale
171+
172+
### Why not just use FillPath?
173+
174+
`FillPath` requires the Go code to know the exact CUE path where a
175+
value should be placed. This works for simple top-level values but
176+
becomes fragile as CUE structure evolves. The injection mechanism
177+
inverts the dependency: the CUE code declares what it needs, and the Go
178+
code provides it by name.
179+
180+
More importantly, `FillPath` operates on already-compiled CUE values.
181+
Injection happens during compilation, which means injected values
182+
participate fully in CUE's evaluation from the start. This is
183+
particularly important for functions, which need to be available when
184+
expressions referencing them are first evaluated.
185+
186+
### Why require @extern(inject)?
187+
188+
The file-level opt-in serves two purposes. First, it makes injection
189+
visible at a glance: a reader can see immediately that a file depends
190+
on external values. Second, it integrates with CUE's existing
191+
`@extern` infrastructure, which is designed precisely for this kind of
192+
external integration.
193+
194+
### Why an authorization function?
195+
196+
Injection allows Go code to unify values into CUE configurations.
197+
In environments where CUE configurations come from untrusted sources
198+
(for example, user-submitted schemas), unrestricted injection could be
199+
a vector for unexpected behavior. The authorization function gives the
200+
host program fine-grained control over what can be injected and where.
201+
202+
The fail-closed default (requiring explicit `Allow` or `AllowAll`)
203+
ensures that a forgotten authorization step results in clear errors
204+
rather than silent security holes.
205+
206+
### Why use names rather than paths?
207+
208+
Names decouple the injection site from its position in the CUE value
209+
tree. A name like `"example.com/myapp/validate"` is stable even if the
210+
CUE code is refactored. It also allows the same value to be injected
211+
at multiple sites by using the same name.
212+
213+
### What about Go code in CUE modules?
214+
215+
This proposal is just a starting point. It allows some custom-written
216+
Go code to inject values (and functions in particular) into a CUE evaluation
217+
but it remains agnostic as to how that actually happens, and it
218+
does not propose a mechanism for allowing implementation code
219+
to be co-located with a CUE package that specifies an `@inject` tag.
220+
That will be the subject of a future proposal.
221+
222+
## Compatibility
223+
224+
This proposal adds new public API to the `cue/cuecontext` package. It
225+
does not change existing CUE evaluation semantics. The `@inject`
226+
attribute is new; existing code that does not use `@extern(inject)` is
227+
entirely unaffected.
228+
229+
The `@extern(inject)` attribute uses the existing `@extern`
230+
infrastructure, which is designed to be extensible with new interpreter
231+
kinds.
232+
233+
## Implementation
234+
235+
The implementation adds:
236+
237+
- `cue/cuecontext/inject.go`: The `Injector` type and its methods,
238+
plus the `injectInterpreter` and `injectCompiler` types that
239+
integrate with `runtime.Interpreter`.
240+
- The injector compiles `@inject` attributes by looking up the
241+
registered value and returning it as an `adt.Expr`, or returning
242+
`&adt.Top{}` when no value is registered.
243+
244+
A proof-of-concept implementation exists.
245+
246+
## Open issues
247+
248+
- **Name structure.** The current design uses opaque strings as
249+
injection names. It may be worth imposing structure on these names,
250+
for example requiring them to follow a URL-like or module-path-like
251+
convention. This would make authorization policies easier to express
252+
(allowing all injections from a particular domain) and reduce the
253+
risk of name collisions.
254+
255+
- **Versioning.** If injection names follow a structured convention,
256+
it may be useful to include version information, allowing a CUE
257+
package to declare that it requires a particular version of an
258+
injected interface.
259+
260+
- **Discoverability.** There is currently no way for a Go program to
261+
discover what injections a CUE package expects without inspecting
262+
the source. A future mechanism for declaring injection requirements
263+
in module metadata could address this.

0 commit comments

Comments
 (0)