Skip to content

fix(gateway): inject manifest defaults for unset variables - #59

Merged
olamide226 merged 7 commits into
mainfrom
fix/gateway-manifest-defaults
Oct 1, 2026
Merged

olamide226 merged 7 commits into
mainfrom
fix/gateway-manifest-defaults

Conversation

@olamide226

@olamide226 olamide226 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

The gateway now injects a manifest variable's default when 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.yaml lets a variable declare default:, and every description says it applies when the variable is unset:

  • the schema: "Default value if the environment variable is not set"
  • 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: Validate checks required, then continues past an optional variable that is absent. Nothing ever reads VarDecl.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 logged public_vars=2 instead of 3, the variable was missing from the payload, and rep.get("FEATURE_FLAGS") returned undefined.

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:

  • it adds the default to the declared tier, under the key Tier.Prefix() + name;
  • an empty-string default is still injected;
  • an environment value always wins, even an empty one.

Validation. manifest.Validate now also checks every optional default against its tier, type and pattern, through the same VarDecl.Check used 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.New

  • It applies defaults after Validate. Validate judges only what the environment set, so a defaulted deprecated variable doesn't warn, and a required variable is never defaulted.
  • It applies them before guardrails, so a PUBLIC default gets scanned.

Reload and poller. Both read through readVars, which also applies defaults. Without it:

  • the poller (server.go:352 on main) would see a change on every tick;
  • Reload (server.go:398 on main) 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 adds defaulted_vars, and the per-tier counts include defaults.

Refactor. Tier.Prefix() is now the single source of the REP_<TIER>_ spelling.

Hygiene commit. A separate chore(gateway) commit clears formatting and lint findings that were already on main: gofmt -l listed 3 files, and golangci-lint flagged an unchecked Close in envfile.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. With bump-minor-pre-major it 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.mdx reserves patch for "clarifications, typo fixes, non-normative additions". Minor covers "new optional features, backwards-compatible extensions".

No wire impact.

  • The payload's _meta.version and data-rep-version carry the gateway build version (cmd/rep-gateway/main.go:32, through server.New).
  • Nothing compares a manifest's version: field; manifest.go only parses it.
  • So no SDK or manifest compatibility changes.

What changed in the RFC

  • The header shows Version: 0.2.0 and Updated: 2026-10-01.
  • There is a new Appendix C: Revision History. The RFC had none.

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

  • Gateway --help: --manifest says "(validates variables, injects defaults)"
  • gateway/README.md:
    • adds the missing --manifest row
    • adds a manifest step to the architecture diagram
    • says "targets REP-RFC-0001 v0.2.0"
    • new Upgrading → To 0.1.8 section (the canonical upgrade note)
  • RFC spec/REP-RFC-0001.md: §4.2 step 5, §6.3 Defaults, §11.3, the version header, and Appendix C
  • Spec pages on the docs site:
    • spec/rfc-0001.mdx: version 0.2.0, plus a link to Appendix C
    • spec/index.mdx: versions, with Conformance sharing the RFC version
    • spec/conformance.mdx: the optional-features list
  • Docs landing page (index.mdx): its spec table had a stale 0.1.0 version column. I dropped the column and linked the spec overview.
  • Manifest guide (guides/manifest.mdx):
    • new Defaults section
    • field table
    • an upgrade caution that links to the README note
  • reference/manifest-schema.mdx: the default row and the YAML comment
  • concepts/how-it-works.mdx: startup sequence, step 5
  • concepts/hot-reload.mdx: removing a defaulted variable sends update with the default, not delete
  • reference/gateway-flags.mdx: the --manifest row
  • reference/gateway-endpoints.mdx: /rep/health counts include defaults
  • concepts/security-model.mdx: a rep.manifest.default_applied row in the monitoring table
  • agents.mdx: the manifest section, with a link to Defaults
  • Manifest JSON schema default description: updated in all three tracked copies (schema/, cli/schema/, docs/public/schema/), kept byte-identical
  • CLI docs (cli/README.md, reference/cli.mdx): rep validate checks structure only, not each default against its type
  • examples/.rep.yaml: a comment on the injected empty default
  • spec/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:
    • unset gets its default
    • an empty default is injected
    • an env value wins
    • a value set in another tier wins
    • required is never defaulted
    • no default adds nothing
    • sensitive and server defaults land in their own tiers
  • manifest.TestValidateDefaults:
    • a bad type or pattern is refused, even while overridden
    • a default with no valid tier is refused
    • a required variable's default is not checked
  • server.TestNew_ManifestDefaults (end to end):
    • the payload, plus public_vars and defaulted_vars
    • an env value wins, even an empty one
    • required and unset still errors
    • an invalid default is refused
  • server.TestReload_KeepsManifestDefaults: the default survives a reload, and the poller sees no false change.

Before the fix:

startup log public_vars = 2, want 3
payload public["FEATURE_FLAGS"] = "" (present=false), want ""

After:

  • go vet is clean.
  • go test -race -count=1 ./... passes in all 9 packages.
  • golangci-lint reports 0 issues, and gofmt is clean.
  • pnpm -r build and pnpm -r test pass in the whole workspace.
  • pnpm docs:build succeeds.
  • The real binary shows public_vars=3 defaulted_vars=1 with "FEATURE_FLAGS":"". An invalid default stops it with manifest validation failed: - default: variable "STATUS_URL" must be a valid URL.

Downstream. StageFlow uses FEATURE_FLAGS: {type: csv, default: ""} under strict_guardrails: true, and its Dockerfile copies rep/gateway:latest. It picks this up once a release is cut, and can then drop its || "".

Follow-ups (not in this PR)

  • Generate cli/schema and docs/public/schema from schema/ instead of hand-syncing them.
  • Have rep validate check defaults against their types.
  • Have rep typegen return string for required or defaulted variables.
  • Have Reload re-run validation.
  • The gateway still doesn't validate type: json or a variable's declared tier.

Type

  • fix (patch)

Scope

  • gateway
  • spec
  • docs
  • cli (docs and schema description only)

Checklist

  • Conventional title
  • Tests
  • All tests pass
  • No manual version bumps

`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.
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Gateway now injects manifest defaults for unset optional variables.

The documentation-link issue is non-blocking; it does not make the PR unsafe to merge.

Summary

The PR adds validated manifest defaults to gateway variables at startup and reload and updates the documentation. A new link on the agents page downloads Markdown instead of opening the Defaults guide.

Reviews (3) · Last reviewed commit: "docs: one home for upgrade notes, fewer ..."

Comment thread gateway/internal/config/defaults.go
Comment thread gateway/internal/config/defaults.go Outdated
Comment thread gateway/internal/server/env_test.go
Comment thread gateway/internal/server/defaults_test.go
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.
@olamide226
olamide226 merged commit 66b25ff into main Oct 1, 2026
10 checks passed
@olamide226
olamide226 deleted the fix/gateway-manifest-defaults branch October 1, 2026 12:38
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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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.

View artifacts

T-Rex Ran code and verified through T-Rex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant