Skip to content

fix(gateway): scan csv variables element by element in guardrails - #60

Merged
olamide226 merged 4 commits into
mainfrom
fix/gateway-guardrails-csv-elements
Oct 1, 2026
Merged

olamide226 merged 4 commits into
mainfrom
fix/gateway-guardrails-csv-elements

Conversation

@olamide226

@olamide226 olamide226 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #59. Merge #59 first. CI only runs on PRs into main, so this PR targets main and is rebased on top of #59's branch. Until #59 merges, this diff includes #59's commits; review from fix(gateway): scan csv variables element by element onward. Once #59 is squash-merged, I'll rebase this onto main so only its own changes remain. The two also merge cleanly as they are.

What

For a variable the manifest declares type: csv, the guardrails now check each element (split on commas and trimmed) instead of the joined string. The three checks (known format, entropy, length) all still run on every element. Every other type is scanned whole, as before.

This PR also adds a feature-flags guide and documents the new behaviour on every user-facing surface.

Why

Root cause (lines on main)

  • gateway/internal/guardrails/guardrails.go:105: any PUBLIC value over 64 characters, with no spaces and no http prefix, gets length_anomaly.
  • guardrails.go:64-67: Scan only ever sees v.Value and cannot know its type.
  • server.go:84-89: under --strict (or strict_guardrails: true), any warning stops startup.

A list of short kebab-case flags matches the length rule once it passes 64 characters, so adding one flag became a failed rollout. Reproduced on v0.1.7: a 67-character list was refused, and the same list with ", " separators started.

How

  • Plumbing: guardrails.Scan(vars, m, logger) receives the loaded manifest (nil if there isn't one).
  • csv variables: scanValue runs once per trimmed element.
  • Not weaker:
    • A long opaque element is still flagged, and the warning names its position: csv element 2: …, with the log attribute csv_element=2.
    • A known key format anywhere in the list is now caught. Before, only the start of the joined string was checked.
  • No backstop on purpose: there is no extra whole-value length check for csv. This is per the owner's decision.

RFC

§3.3 states the per-element rule as a MUST. Its line sits under the same 0.2.0 entry in Appendix C that #59 creates.

Feature-flags docs

There was no guide before. FEATURE_FLAGS appeared only as example values, in one worked Kubernetes example (examples/nextjs-csr-embedded), and in one-line snippets.

The new docs/src/content/docs/guides/feature-flags.mdx sits in the sidebar under Guides, next to Manifest File. It covers:

  • one FEATURE_FLAGS: {tier: public, type: csv, default: ""} variable, with an optional kebab-case pattern (tested against the real VarDecl.Check);
  • an SDK read where a flag is off unless named, relying on the default from fix(gateway): inject manifest defaults for unset variables #59;
  • flags are public and are not access control;
  • the per-element guardrail;
  • flipping a flag by restarting, or by hot reload with --env-file, and never by rebuilding;
  • what it isn't: no per-user targeting, rollouts or experiments.

The example is a generic version of StageFlow's landing-page flag, with no hostnames.

User-facing docs

  • RFC §3.3, plus its line in the Appendix C 0.2.0 entry
  • gateway/README.md: a csv bullet under Upgrading → To 0.1.8
  • concepts/variable-classification.mdx:
    • the per-element rule
    • csv element N / csv_element
    • the plugin difference, linked
  • guides/manifest.mdx: the csv row links to the guide
  • New guides/feature-flags.mdx, added to sidebar.mjs (so it also appears in llms.txt and gets a .md mirror)
  • agents.mdx: source list links to the guide
  • Root README.md: the SDK example links to the guide
  • Dev-plugin limitation: documented, not fixed.
    • What: the Vite and Next plugins have no manifest, so they scan whole values (plugins/*/src/guardrails.ts:84), and strict throws (vite/src/index.ts:45-46, next/src/index.ts:77-78).
    • Why not fixed: a fix needs a new plugin option or a YAML parser in two packages.
    • Where it's documented: in full once, under guides/feature-flags.mdx#dev-plugins. One-clause links to it from:
      • reference/plugins/vite.mdx
      • reference/plugins/next.mdx
      • guides/development.mdx
      • plugins/vite/README.md
      • plugins/next/README.md
  • Gateway --help: --strict ("Exit on guardrail warnings") is still accurate, so no change.

npm side effect. This PR touches plugins/*/README.md, so release-please will include the npm packages in a docs-only patch release, as with #59's cli/ changes.

Tests

  • guardrails.TestScan_CSVJudgedByElement (9 cases):
    • a long csv of short tokens passes
    • spaces around elements are trimmed
    • an empty csv passes
    • one opaque 80-character element is flagged, with its position
    • a known format past the first element is flagged
    • a high-entropy element is flagged
    • the same list typed string is flagged
    • with no manifest, the list is flagged
    • a long opaque non-csv value is flagged
  • server.TestNew_StrictGuardrailsJudgeCSVByElement, under Strict: true:
    • the 67-character csv starts
    • a csv holding one opaque token is refused
    • the same list typed string is refused

Before:

New() error = guardrail scan found 1 warning(s) and --strict is enabled; refusing to start, want the gateway to start

After:

  • go vet, race tests, golangci-lint (0 issues) and gofmt are clean.

  • pnpm -r build and pnpm -r test pass.

  • pnpm docs:build succeeds: 48 pages, and the #dev-plugins anchor resolves from all four linking pages.

  • Real binary under strict:

    Binary FEATURE_FLAGS value Result
    Unfixed 67-character list refused
    Fixed 67-character list starts
    Fixed list holding an 80-character token refused, with csv_element=2

Type

  • fix (patch)

Scope

  • gateway
  • spec
  • docs
  • plugins (README only)

Checklist

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

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Gateway guardrails now scan CSV variables element by element.

The outstanding strict-reload guardrail bypass must be fixed before merging. The new guide issues are non-blocking.

Summary

The feature-flags guide describes file edits taking effect without the reload trigger the gateway requires, and its .rep.yaml example fails validation because it lacks a version. The previously reported strict-reload guardrail bypass and stale SDK flag checks also remain outstanding.

Reviews (2) · Last reviewed commit: "docs: state the dev-plugin csv caveat on..."

Comment thread gateway/internal/guardrails/guardrails.go
Comment thread gateway/internal/server/env_test.go
@olamide226
olamide226 force-pushed the fix/gateway-guardrails-csv-elements branch from e2b689e to fa8663b Compare October 1, 2026 12:29
The length heuristic flags any PUBLIC value over 64 characters that has
no spaces and no `http` prefix (gateway/internal/guardrails/guardrails.go:105
before this change). A `csv` value of short kebab-case feature flags
fits that description once the list passes 64 characters. Under
`strict_guardrails: true` (`--strict`) the gateway then refuses to
start (gateway/internal/server/server.go:84-89), so adding one flag to
a values file became a failed rollout. Reproduced with a 67-character
comma list on v0.1.7. The same list with ", " separators started,
because it then contained spaces.

`Scan` scanned `v.Value` whole and had no way to learn the declared
type. It now takes the loaded manifest (nil when there is none). A
variable declared `type: csv` is split on commas, each element is
trimmed, and every heuristic (known format, entropy, length) runs on
each element. The check is not weakened: an 80-character opaque token
inside a list is still flagged, and the warning and log name its
position (`csv element 2: ...`, `csv_element=2`). A known-format token
past the first element is now caught; before, only a prefix of the
joined string was checked. Every other type, and any variable with no
manifest entry, is scanned whole as before.

Spec §3.3 and the guardrails section of the docs describe the
per-element rule.
The repo used `FEATURE_FLAGS` as an example throughout but had no page
on using it. The new `guides/feature-flags` page, under Guides next to
the manifest guide, covers:

- one `FEATURE_FLAGS: {tier: public, type: csv, default: ""}` variable
  rather than one variable per flag, with an optional pattern;
- reading it with the SDK, where a flag is off unless named;
- that flags are public and are not access control;
- the per-element guardrail, and the dev-plugin caveat;
- flipping a flag by changing the environment and restarting, or with
  hot reload when the value comes from `--env-file`;
- what it isn't: no per-user targeting, rollouts or experiments.

The agents playbook and the README's SDK example link to it.
- gateway README "Upgrading / To 0.1.8": csv values are scanned element
  by element, warnings name the element (`csv_element`), and a known
  key format is now caught anywhere in the list.
- Variable classification: the `csv_element` log attribute, and that
  the dev plugins scan whole values.
- Vite and Next.js plugin READMEs and reference pages, and the local
  development guide: the plugins don't read the manifest, so with
  `strict` a long flag list can throw in development even though the
  gateway accepts it. Documented, not fixed: a fix needs a new plugin
  option or a YAML parser in two packages.
- REP-RFC-0001 revision history: the §3.3 rule joins the 0.2.0 entry.
Simplify follow-up:

- The caveat that the dev plugins scan csv values whole was copied in
  eight places, and the copies had started to disagree. It now lives
  once, under "Dev plugins" in the feature-flags guide. The other pages
  and the two plugin READMEs keep a single clause that links there.
- The feature-flags guide drops sentences that repeated themselves.
  Instead of restating them, it links to Defaults, the guardrails
  section, Hot Reload, the Kubernetes recipe and the existing Next.js
  worked example.
if err != nil {
return nil, err
}
vars.ApplyDefaults(s.cfg.Manifest)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Reload bypasses strict guardrails

If a benign environment override masks a secret-shaped PUBLIC manifest default at startup, removing the override makes reload apply that default without scanning it or enforcing --strict. The gateway then publishes the value in HTML and an SSE update instead of rejecting it.

How this was verified: A strict gateway published a synthetic secret after reload, while startup with that default refused to start.

Artifacts

Go source for the focused reload and HTTP probe

  • The authored test exercises startup, Reload, injected HTML, and SSE using a synthetic token; it is the executed source.

Go test overlay for the untracked probe

  • The overlay supplies the authored test to the server package without editing tracked source files.

HTTP responses before removing the benign override

  • The captured test run shows `200 OK` HTML and SSE responses while the override remains and the synthetic token is not published.

HTTP responses after removing the override and reloading

  • The captured test run shows `200 OK` HTML and SSE responses containing the synthetic token after Reload, while a fresh strict startup rejects it.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +32 to +36
const enabled = new Set(
rep.get('FEATURE_FLAGS', '').split(',').map((f) => f.trim()).filter(Boolean),
);

export const isOn = (flag: string) => enabled.has(flag);

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 Flag checks stay stale

The guide constructs enabled only once. After a hot-reload update, the SDK has the new flags, but the example’s isOn still checks the original Set. Readers following the example can make stale flag decisions until the page reloads.

Artifacts

Chromium verification source

  • The authored script renders the verbatim guide snippet with the real SDK and sends an SSE flag update, establishing the executed source.

Chromium check output

  • The command output records its working directory, exit code, SDK values, and flag results before and after the SSE update, confirming the stale result.

▶ Guide example before hot reload

  • Chromium renders the guide snippet with only `landing-page` enabled, showing `new-dashboard` off before the update.

Guide example before hot reload poster

  • The captured browser frame shows the initial SDK value and the guide's matching off result.

▶ Guide example after SDK hot reload

  • Chromium renders the same snippet after an SSE update, showing that the SDK includes `new-dashboard` while `isOn` still reports off.

Guide example after hot reload poster

  • The captured browser frame shows the updated SDK value beside the stale guide result.

View artifacts

T-Rex Ran code and verified through T-Rex

@olamide226
olamide226 force-pushed the fix/gateway-guardrails-csv-elements branch from fa8663b to e455a77 Compare October 1, 2026 12:39
You flip a flag by changing the environment, not the image:

- **Restart:** set `REP_PUBLIC_FEATURE_FLAGS` and restart the container or roll the deployment.
- **Hot reload:** with `--hot-reload` and `--env-file`, the gateway picks up edits to that file without a restart; it does not see changes to its own process environment. Pages using `rep.onChange('FEATURE_FLAGS', …)` or a framework adapter update in place. See [Hot Reload](/concepts/hot-reload/), the [Kubernetes recipe](/deployment/kubernetes/), and the end-to-end [Next.js example](/examples/nextjs-csr-embedded/).

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 File edits need a trigger

The guide says --hot-reload with --env-file picks up file edits, but the default signal mode does not watch the file. Readers following these instructions will keep serving stale flags until they send SIGHUP. Explain that trigger or show a file-detection mode with its required options.

Artifacts

Authored live gateway reproduction script

  • The script builds and launches the gateway, edits the env file, requests the injected payload, and sends a control SIGHUP; it contains the exact test commands.

Live gateway run and observed output

  • Running the authored script exited successfully and recorded the launch arguments, unchanged value after the edit, changed value after SIGHUP, and gateway logs; file editing alone did not reload the flag.

Gateway response before the env-file edit

  • An HTTP request to the running gateway returned the initial injected flag value `landing-page`; this establishes the comparison baseline.

Gateway response after the env-file edit without SIGHUP

  • An HTTP request three seconds after editing the file still returned `landing-page`; the documented invocation did not pick up the edit.

Gateway response after sending SIGHUP

  • An HTTP request after SIGHUP returned `landing-page,dark-mode`; the edited file was readable when reload was triggered.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +12 to +14
```yaml
# .rep.yaml
variables:

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 Complete the manifest example

The YAML labeled .rep.yaml omits the required top-level version. Copying it as a manifest makes rep validate fail before readers can use the feature flags. Add a version or clearly label the YAML as a fragment of an existing manifest.

Artifacts

Feature-flags manifest validation script

  • The authored script extracts the guide’s YAML and invokes the CLI before and after adding only the required version, so both runs can be reproduced.

Copied manifest fails validation

  • The captured CLI run exited 1 and reported the missing required `version` property, confirming the copied example fails.

Manifest with version passes validation

  • The captured CLI run exited 0 after adding only `version: "0.1.0"`, isolating the omission.

View artifacts

T-Rex Ran code and verified through T-Rex

@olamide226
olamide226 merged commit b4c5750 into main Oct 1, 2026
10 checks passed
@olamide226
olamide226 deleted the fix/gateway-guardrails-csv-elements branch October 1, 2026 12:51
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