Documentation
¶
Overview ¶
Package agentenv parses the optional .agents/env.yaml (or .env.json) manifest and produces a Resolver that agent-bundle loaders can use to interpolate ${env:VAR} references in AGENTS.md, skill files, and mcp.json values.
The mechanism has two halves:
A machine-readable manifest declaring which env vars the bundle expects — required vs optional, defaults, sensitive markers, and free-text descriptions. The daemon validates required vars at boot; missing required vars are fail-loud errors, not silent empty-string interpolation.
${env:VAR} interpolation applied at instruction-file load time. Syntax matches what pkg/mcp/config.go already accepts in mcp.json header values, so operators only learn one substitution convention across the whole .agents/ bundle.
Bundles without a manifest keep working unchanged — no manifest, no interpolation, no validation. Zero regression path for existing deployments.
Index ¶
Constants ¶
const ManifestFileJSON = "env.json"
ManifestFileJSON is the JSON variant of the manifest. Both files are probed at load time — YAML first, then JSON. Declaring both is a configuration error (see LoadManifest).
const ManifestFileYAML = "env.yaml"
ManifestFileYAML is the manifest filename with YAML syntax. YAML is the preferred format because multi-line descriptions read cleanly; JSON is supported below for parity with config.json / mcp.json.
const SchemaVersion = 1
SchemaVersion is the current major version of the env-manifest file format. Bump on breaking changes; older versions get rejected at load time with an explicit upgrade-path error.
Variables ¶
This section is empty.
Functions ¶
func FindReferences ¶
FindReferences returns the unique set of NAME values referenced in s via the ${env:NAME} syntax. Order is deterministic (sorted) so callers can produce stable log lines and diff output.
Used by the Resolver during construction to (a) compute the set of names referenced anywhere in the bundle for the "undeclared reference" drift warning, and (b) enable "unreferenced declaration" detection on the manifest side.
func InterpolateEnv ¶
InterpolateEnv is the legacy free-function used by pkg/mcp. It looks up each ${env:NAME} directly via os.Getenv with no manifest awareness — no required-var checks, no sensitive tracking, no drift warnings. Kept exported so pkg/mcp callers didn't churn when interpolation moved into this package; new call sites should go through a Resolver instead (see NewResolver).
Types ¶
type Entry ¶
type Entry struct {
Name string `json:"name" yaml:"name"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
Default string `json:"default,omitempty" yaml:"default,omitempty"`
Sensitive bool `json:"sensitive,omitempty" yaml:"sensitive,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
UsedBy []string `json:"used_by,omitempty" yaml:"used_by,omitempty"`
}
Entry declares one env variable the bundle expects.
A required entry with no env-var set at daemon boot is a fail-loud error (surfaced via Resolver.Errors). An optional entry falls back to Default when unset; missing default → empty string.
UsedBy is optional documentation — a comma-separated list of files (or free-text hints) that reference this var, letting recipe authors grep for coupling. Nothing in the loader validates the entries.
Sensitive marks values that must not appear in verbose logs, the eventlog, or diagnostic surfaces like /stats. Set true for tokens, passwords, API keys.
type Manifest ¶
type Manifest struct {
Version int `json:"version" yaml:"version"`
Env []Entry `json:"env" yaml:"env"`
}
Manifest is the on-disk schema for .agents/env.yaml (or .env.json).
Version follows the convention set by pkg/config (SchemaVersion=1) and pkg/mcp (Servers.Version) — top-level version bump on breaking change, unknown fields ignored for forward-compat.
func LoadManifest ¶
LoadManifest probes agentsDir for env.yaml then env.json and returns whichever it finds. Both present → error (ambiguous; operator should pick one). Neither present → nil manifest, nil error (caller treats as "no interpolation configured"; existing bundles keep working).
agentsDir may be "" — in that case returns (nil, nil) too, matching how config.LoadOrDefault handles the "no .agents/ discovered" case.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver interpolates ${env:NAME} in strings using an env-var lookup, with awareness of which names are declared in the manifest.
Construction is a two-phase process:
NewResolver(manifest, lookup) — records the manifest, resolves each declared name against lookup, records errors for missing required vars. This is when required-var validation fires.
Interpolation call sites (pkg/instruction, pkg/skills, pkg/mcp) use Interpolate(s) as they load bundle files. Interpolate records every unique NAME it sees to enable the "undeclared reference" drift warning at ReportDrift time.
A nil Resolver is safe — Interpolate is a no-op, Errors returns nil, IsSensitive returns false. Loaders should tolerate the nil case (bundle without a manifest) rather than requiring a stub.
func NewResolver ¶
NewResolver builds a Resolver from a parsed manifest and an env-var lookup function (usually os.LookupEnv). Passing nil manifest returns nil — matches the "no manifest, no interpolation" backwards-compat path expected by pkg/config.LoadOrDefault callers.
lookup must return (value, true) if the var is set (even to empty string) and ("", false) if unset. os.LookupEnv has this shape directly; tests can pass a map-backed closure.
func (*Resolver) Errors ¶
Errors returns fatal validation problems from Resolver construction (currently: missing required env vars). Empty slice → boot may proceed; non-empty → boot should log each and exit.
func (*Resolver) Interpolate ¶
Interpolate substitutes ${env:NAME} in s using the resolved manifest values. Undeclared NAMEs fall back to the ambient os.Getenv path so a bundle can still reference standard system env vars (HOME, PATH, etc.) without declaring them — those show up as "undeclared reference" warnings via ReportDrift but don't break interpolation.
Every unique NAME seen is recorded so ReportDrift can compute the undeclared-reference set.
func (*Resolver) InterpolateFunc ¶
InterpolateFunc returns a bare closure suitable for passing to loaders that don't want to import agentenv directly (pkg/instruction, pkg/skills). Nil-safe: nil Resolver returns nil, which loaders interpret as "no interpolation."
func (*Resolver) IsSensitive ¶
IsSensitive reports whether the named var is marked sensitive in the manifest. Used by log-sanitization paths that already redact certain values (mcp.json headers, /stats surfaces) to also redact env-var values marked in the manifest. Nil-safe: nil Resolver → false.
func (*Resolver) ReportDrift ¶
ReportDrift returns non-fatal warnings the daemon should log at boot:
- "undeclared reference: BAR referenced in bundle but not in manifest" — the recipe author probably meant to add it.
- "unreferenced declaration: FOO declared in manifest but not referenced anywhere" — leftover from a refactor.
Both are advisory (per the #322 issue: warn, not error). The daemon keeps running; the recipe author sees the warnings and cleans up on their next iteration.
Callers must invoke ReportDrift AFTER all bundle files have flowed through Interpolate at least once; earlier invocation reports every declaration as unreferenced.
func (*Resolver) SensitiveValues ¶
SensitiveValues returns the set of resolved values that should be redacted in logs, sorted for stable output. Empty when no sensitive entries are declared or nothing has been resolved yet.
Callers that need to grep-and-redact a downstream string (like a full log line) can walk this list; callers that already know which VAR they're logging should prefer IsSensitive.