fix(gateway): inject manifest defaults for unset variables - #59
Conversation
`gofmt -l` listed three files (trailing blank lines, comment alignment) and `golangci-lint run` reported one errcheck: the unchecked deferred `f.Close()` in envfile.go. Both predate this branch. Fixed here so `make fmt` is a no-op and `make lint` reports 0 issues. No behaviour change.
A `.rep.yaml` variable can declare `default:`, and the schema, the
manifest guide and the schema reference all say it applies when the
variable is unset. The gateway never injected it. `Manifest.Validate`
checked required and type, and for an optional, absent variable it
`continue`d with nothing else to do
(gateway/internal/manifest/manifest.go:150-156 before this change).
Nothing downstream read `VarDecl.Default`, so the variable was missing
from the payload and `rep.get()` returned undefined. A manifest with
`FEATURE_FLAGS: {type: csv, default: ""}` and that variable unset
logged `public_vars=2`, not 3.
`config.ClassifiedVars.ApplyDefaults` now fills in each optional
variable that declares a default and is set in no tier. The value goes
into the declared tier under its `REP_<TIER>_` key and goes through the
same type and pattern check as a set value (`VarDecl.Check`, split out
of `Validate`). `server.New` calls it after validation, so a required
variable's default never hides that it is missing, and before the
guardrails, so a PUBLIC default is scanned. An empty-string default is
still injected. An environment value, even an empty one, always wins.
The poller and `Reload` read through a shared `readVars` helper that
also applies defaults. Otherwise the poller would compare a defaulted
snapshot against a raw one and report a change on every tick, and a
reload would drop the default and broadcast a delete.
Each default is logged as `rep.manifest.default_applied` (name and
tier, not the value). The startup summary counts defaults in the
per-tier totals and adds `defaulted_vars`.
The `REP_<TIER>_` prefix now has one source, `Tier.Prefix()`, which
classification and defaults both use.
A default that fails its declared type (for example `type: url,
default: ""`) is now a startup error. Before, it was silently never
used.
Spec: §4.2 step 5 now includes applying defaults, and the new §6.3
states the rules. The docs site's manifest guide, schema reference and
startup sequence say the same.
|
Review follow-up. ApplyDefaults only checked a default it was about to inject, so an invalid default was accepted at startup while the environment overrode it. It surfaced later: remove the override, and the next reload failed while the gateway kept serving the old value. Every optional default is now checked at startup, whether or not it is in use. Defaults are now added only after all of them pass. Before, a valid default could be appended to the receiver before a later invalid one returned an error. Every example manifest in the repo still loads and passes.
This PR adds a normative rule to the RFC (§4.2 step 5, §6.3: the gateway MUST inject manifest defaults). Under the published versioning policy (docs/src/content/docs/spec/index.mdx), patch is reserved for clarifications and non-normative additions, and minor covers backwards-compatible extensions. So this is a minor bump, 0.1.0 to 0.2.0, with `Updated:` set to the date of the change. The RFC had no revision history. Appendix C now records each version, and the docs site's RFC page links to it rather than duplicating it. The spec index lists the core protocol at 0.2.0 and makes clear that the security model and conformance documents are still 0.1.0. The protocol version is documentation only. The payload's `_meta.version` and `data-rep-version` carry the gateway build version, and nothing compares a manifest's `version:` field. So existing manifests and SDKs are unaffected.
Audited every place that describes manifests, startup or logs, so each one states the new behaviour: - gateway `--help`: `--manifest` says it validates and injects defaults. - gateway README: adds the missing `--manifest` row, a manifest step in the architecture diagram, "targets REP-RFC-0001 v0.2.0", and an "Upgrading / To 0.1.8" section (defaults now injected; a default that fails its own type is now a startup error; `defaulted_vars` and `rep.manifest.default_applied`). - Manifest guide: an "Upgrading to gateway 0.1.8" caution. - Gateway flags, health endpoint (counts include defaults), hot reload (a removed variable with a default updates rather than deletes), agents playbook, and the security-model monitoring table. - Manifest JSON schema `default` description, in all three tracked copies (schema/, cli/schema/, docs/public/schema/), kept identical. - CLI docs: `rep validate` checks structure, not default-vs-type. - RFC §11.3 and the conformance page, and the conformance version in the spec index, now 0.2.0 like the RFC it is extracted from. - examples/.rep.yaml: a comment on the injected empty default. SECURITY-MODEL.md is left unchanged. It is versioned separately and still accurate.
…ults Simplify follow-up. Whether a default fits its declared tier, type and pattern depends only on the manifest, so `manifest.Validate` now checks it. Validate runs once at startup and already sees every declaration. `ApplyDefaults` becomes plain injection that cannot fail, which removes the add-after-validation pass, both of its error paths in `server.New` and `readVars`, and the re-check on every hot-reload poll. Behaviour is unchanged. An invalid default, even one the environment overrides, still stops startup, and the error is now reported with the other manifest validation errors. The validation cases move to `manifest.TestValidateDefaults`.
Simplify follow-up on the docs audit: - Upgrade notes have a single home in the gateway README's "Upgrading" section. The manifest guide's caution keeps one sentence and links there. - The landing page's spec table had a stale 0.1.0 copy of every version. It drops the column and links to the spec overview, which is the only docs page listing versions. Conformance is described as sharing the RFC's version. - The RFC's Appendix C intro no longer restates the versioning policy. - Shorter wording for the schema `default` description (all three copies, kept identical), the CLI validate caveat, and the gateway-flags and security-model table cells. The agents playbook now links to Defaults instead of restating the rule. - The README's quoted startup error matches the real one.
| allowed_origins: ["https://app.example.com"] | ||
| ``` | ||
|
|
||
| An optional variable that declares `default:` is [injected by the gateway](/guides/manifest.md#defaults) when it is unset, so `rep.get('FEATURE_FLAGS')` above returns `""` rather than `undefined`. |
There was a problem hiding this comment.
Defaults Link Downloads Markdown
The new “injected by the gateway” link points to /guides/manifest.md#defaults. Clicking it downloads manifest.md and leaves readers on the agents page instead of opening the Defaults section. This non-blocking navigation issue prevents readers from reaching the linked guidance.
| An optional variable that declares `default:` is [injected by the gateway](/guides/manifest.md#defaults) when it is unset, so `rep.get('FEATURE_FLAGS')` above returns `""` rather than `undefined`. | |
| An optional variable that declares `default:` is [injected by the gateway](/guides/manifest/#defaults) when it is unset, so `rep.get('FEATURE_FLAGS')` above returns `""` rather than `undefined`. |
Artifacts
Authored Chromium navigation script
- The uploaded source served each Astro build and exercised the rendered pages in Chromium, making the navigation test reproducible.
Prior-build browser execution log
- The command, working directory, output, and exit code show that the link was absent at the prior SHA while the intended destination returned 200 with a Defaults anchor.
Current-build browser execution log
- The command, working directory, output, and exit code show that clicking the rendered link downloaded `manifest.md` and left the browser on the agents page.
▶ Prior-build Defaults destination in Chromium
- Chromium opened the prior build’s rendered Defaults destination directly because the link did not yet exist, showing the working baseline.
Prior-build Defaults destination poster
- A frame from the prior-build recording shows the rendered guide at the working destination.
▶ Current-build link click in Chromium
- Chromium clicked the new rendered link and remained on the agents page while the Markdown file downloaded, confirming the broken navigation.
Current-build agents page poster
- A frame after the link click shows the browser still on the agents page rather than at Defaults.
What
The gateway now injects a manifest variable's
defaultwhen the variable is unset. It lands in the declared tier, with the same type and pattern checks and the same guardrail scan as a set value. REP-RFC-0001 goes to 0.2.0 with a revision history, and every user-facing doc surface now describes the behaviour.Why
.rep.yamllets a variable declaredefault:, and every description says it applies when the variable is unset:guides/manifest.mdx: "Default value if not provided"reference/manifest-schema.mdx: "Default value when variable is absent"The gateway never did this, so this is a bug fix.
Root cause (lines on
main)gateway/internal/manifest/manifest.go:147-156:Validatechecksrequired, thencontinues past an optional variable that is absent. Nothing ever readsVarDecl.Default.gateway/internal/server/server.go:61-89: startup reads, validates and runs guardrails. There is no step that fills anything in.Symptom: with
FEATURE_FLAGS: {type: csv, default: ""}unset, startup loggedpublic_vars=2instead of 3, the variable was missing from the payload, andrep.get("FEATURE_FLAGS")returnedundefined.How
Injection.
config.ClassifiedVars.ApplyDefaults(m)(gateway/internal/config/defaults.go) is plain injection and cannot fail. For each declared variable that is optional, has a default, and is set in no tier:Tier.Prefix() + name;Validation.
manifest.Validatenow also checks every optional default against its tier, type and pattern, through the sameVarDecl.Checkused for set values. It checks a default even while the environment overrides it. Otherwise a broken default would only surface when a later reload dropped the override.Order in
server.NewValidate. Validate judges only what the environment set, so a defaulted deprecated variable doesn't warn, and a required variable is never defaulted.Reload and poller. Both read through
readVars, which also applies defaults. Without it:server.go:352onmain) would see a change on every tick;Reload(server.go:398onmain) would drop the default and broadcast a delete.Logging. Each default logs
rep.manifest.default_applied(name and tier, never the value). The startup summary addsdefaulted_vars, and the per-tier counts include defaults.Refactor.
Tier.Prefix()is now the single source of theREP_<TIER>_spelling.Hygiene commit. A separate
chore(gateway)commit clears formatting and lint findings that were already onmain:gofmt -llisted 3 files, andgolangci-lintflagged an uncheckedCloseinenvfile.go.Upgrade impact
A default that fails its own type is now a startup error where it used to be ignored. For example,
type: url, default: ""now fails. The canonical note is in the gateway README, under Upgrading → To 0.1.8.All five example manifests under
examples/pass.I did not add a
BREAKING CHANGE:footer. Withbump-minor-pre-majorit would make release-please cut gateway 0.2.0 instead of 0.1.8. If you'd rather mark it that way, say so and I'll add it.RFC version: 0.1.0 → 0.2.0
Why minor. The published versioning policy in
docs/.../spec/index.mdxreserves patch for "clarifications, typo fixes, non-normative additions". Minor covers "new optional features, backwards-compatible extensions".No wire impact.
_meta.versionanddata-rep-versioncarry the gateway build version (cmd/rep-gateway/main.go:32, throughserver.New).version:field;manifest.goonly parses it.What changed in the RFC
Version: 0.2.0andUpdated: 2026-10-01.Split with #60. This PR does the bump and adds the 0.2.0 history entry. #60 is rebased onto this branch and adds its §3.3 line under the same 0.2.0 entry, so both ship in gateway 0.1.8 and nothing conflicts.
User-facing docs
--help:--manifestsays "(validates variables, injects defaults)"gateway/README.md:--manifestrowspec/REP-RFC-0001.md: §4.2 step 5, §6.3 Defaults, §11.3, the version header, and Appendix Cspec/rfc-0001.mdx: version 0.2.0, plus a link to Appendix Cspec/index.mdx: versions, with Conformance sharing the RFC versionspec/conformance.mdx: the optional-features listindex.mdx): its spec table had a stale 0.1.0 version column. I dropped the column and linked the spec overview.guides/manifest.mdx):reference/manifest-schema.mdx: thedefaultrow and the YAML commentconcepts/how-it-works.mdx: startup sequence, step 5concepts/hot-reload.mdx: removing a defaulted variable sendsupdatewith the default, notdeletereference/gateway-flags.mdx: the--manifestrowreference/gateway-endpoints.mdx:/rep/healthcounts include defaultsconcepts/security-model.mdx: arep.manifest.default_appliedrow in the monitoring tableagents.mdx: the manifest section, with a link to Defaultsdefaultdescription: updated in all three tracked copies (schema/,cli/schema/,docs/public/schema/), kept byte-identicalcli/README.md,reference/cli.mdx):rep validatechecks structure only, not each default against its typeexamples/.rep.yaml: a comment on the injected empty defaultspec/SECURITY-MODEL.md: left unchanged on purpose. It is versioned separately, at 0.1.0, and stays accurate.npm side effect. This PR touches
cli/(the README and the schema copy). When squash-merged, release-please will treat the CLI as changed, and because the npm packages are linked, it will propose an npm patch release whose only change is docs and a schema description. If you'd rather avoid that, drop those two files and I'll move them to a later release.Tests
All table-driven, stdlib only, with
t.Setenv.config.TestApplyDefaults:manifest.TestValidateDefaults:server.TestNew_ManifestDefaults(end to end):public_varsanddefaulted_varsserver.TestReload_KeepsManifestDefaults: the default survives a reload, and the poller sees no false change.Before the fix:
After:
go vetis clean.go test -race -count=1 ./...passes in all 9 packages.golangci-lintreports 0 issues, andgofmtis clean.pnpm -r buildandpnpm -r testpass in the whole workspace.pnpm docs:buildsucceeds.public_vars=3 defaulted_vars=1with"FEATURE_FLAGS":"". An invalid default stops it withmanifest validation failed: - default: variable "STATUS_URL" must be a valid URL.Downstream. StageFlow uses
FEATURE_FLAGS: {type: csv, default: ""}understrict_guardrails: true, and its Dockerfile copiesrep/gateway:latest. It picks this up once a release is cut, and can then drop its|| "".Follow-ups (not in this PR)
cli/schemaanddocs/public/schemafromschema/instead of hand-syncing them.rep validatecheck defaults against their types.rep typegenreturnstringfor required or defaulted variables.Reloadre-run validation.type: jsonor a variable's declared tier.Type
fix(patch)Scope
gatewayspecdocscli(docs and schema description only)Checklist