Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ npx @rep-protocol/cli [command]

Validate a `.rep.yaml` manifest file against the JSON schema.

Structural check only: the gateway, not `rep validate`, checks each [default](https://rep-protocol.dev/guides/manifest/#defaults) against its `type` and `pattern`.

```bash
rep validate --manifest .rep.yaml
```
Expand Down
2 changes: 1 addition & 1 deletion cli/schema/rep-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"default": {
"type": ["string", "number", "boolean"],
"description": "Default value if the environment variable is not set. Only valid for non-required variables. Non-string values are coerced to strings."
"description": "Injected by the gateway when the variable is unset in every tier; ignored if required. Must satisfy type and pattern, or the gateway refuses to start. Non-string values are coerced to strings."
},
"description": {
"type": "string",
Expand Down
2 changes: 1 addition & 1 deletion docs/public/schema/rep-manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
},
"default": {
"type": ["string", "number", "boolean"],
"description": "Default value if the environment variable is not set. Only valid for non-required variables. Non-string values are coerced to strings."
"description": "Injected by the gateway when the variable is unset in every tier; ignored if required. Must satisfy type and pattern, or the gateway refuses to start. Non-string values are coerced to strings."
},
"description": {
"type": "string",
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ settings:
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


```bash
rep validate # fail fast on a malformed or incomplete manifest
rep typegen -o src/rep.d.ts # typed get() / getSecure() overloads
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/concepts/hot-reload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ id: 1708267831000
| `rep:config:update` | A variable's value changed or a new variable was added |
| `rep:config:delete` | A variable was removed |

Manifest [defaults](/guides/manifest/#defaults) are re-applied on every reload. Removing a variable that declares a default sends `rep:config:update` with the default value, not `rep:config:delete`.

## Change detection modes

The gateway supports three modes for detecting environment variable changes:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/concepts/how-it-works.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The gateway performs these steps in order at process start:
2. Read all `REP_*` environment variables
3. Classify each into PUBLIC, SENSITIVE, or SERVER tier
4. Validate name uniqueness after prefix stripping
5. Load and validate `.rep.yaml` manifest if `--manifest` is specified
5. Load and validate `.rep.yaml` manifest if `--manifest` is specified, then inject the `default` of any optional variable left unset
6. Run secret detection guardrails on PUBLIC variables
7. Exit with error if `--strict` and guardrails triggered
8. Generate ephemeral master key, derive AES-256 key via HKDF-SHA256
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/concepts/security-model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ The `<script id="__rep__" type="application/json">` tag is inert data — it doe
| Log Event | Level | Alert On |
|---|---|---|
| `rep.guardrail.warning` | WARN | Any occurrence in production |
| `rep.manifest.default_applied` | INFO | None |
| `rep.session_key.issued` | INFO | Rate exceeding baseline |
| `rep.session_key.rejected` | WARN | Any occurrence |
| `rep.session_key.rate_limited` | WARN | Sustained bursts |
Expand Down
28 changes: 27 additions & 1 deletion docs/src/content/docs/guides/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ settings:
| `tier` | `public` / `sensitive` / `server` | Yes | Security classification |
| `type` | see below | Yes | Value type constraint |
| `required` | boolean | No | Whether the variable must be present at startup |
| `default` | string | No | Default value if not provided |
| `default` | string | No | Value the gateway injects when the variable is unset. See [Defaults](#defaults) |
| `description` | string | No | Human-readable description |
| `example` | string | No | Example value |
| `pattern` | string | No | Regex pattern the value must match |
Expand All @@ -95,6 +95,32 @@ settings:
| `json` | Must be valid JSON |
| `enum` | Must match one of the `values` array entries |

## Defaults

When a variable declares `default`, isn't `required`, and isn't set in any tier, the gateway injects the default as though it had been set. It goes into the declared `tier`, so a `public` default is in the payload and `rep.get()` returns it:

```yaml
FEATURE_FLAGS:
tier: public
type: csv
default: ""
```

With `REP_PUBLIC_FEATURE_FLAGS` unset, `rep.get('FEATURE_FLAGS')` returns `""`, not `undefined`.

- An empty string is a default: `default: ""` injects `""`.
- A value in the environment always wins, even an empty one.
- A default is validated against `type` and `pattern` like a set value, even while the environment overrides it. A default that doesn't match is a manifest error and the gateway refuses to start.
- A `public` default is scanned by the [guardrails](/concepts/variable-classification/#automatic-secret-detection-guardrails).
- `required: true` wins. A required variable that is unset fails startup even if it declares a default.
- Hot reload re-applies defaults. Removing a variable from the environment reverts it to its default.

<Aside type="caution" title="Upgrading to gateway 0.1.8">
Gateways before 0.1.8 ignored defaults. After upgrading, an invalid default (for example `type: url` with `default: ""`) stops startup. See the [gateway upgrade notes](https://github.com/ruachtech/rep/blob/main/gateway/README.md#upgrading).
</Aside>

Each injected default is logged as `rep.manifest.default_applied` with its name and tier (never its value). The startup summary counts defaults in `public_vars`, `sensitive_vars` and `server_vars` and totals them in `defaulted_vars`.

## Settings

| Setting | Type | Default | Description |
Expand Down
12 changes: 7 additions & 5 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,12 @@ REP_SERVER_DB_PASSWORD=never-reaches-browser

## Specification Status

| Document | Status | Version |
|---|---|---|
| [REP-RFC-0001](/spec/rfc-0001/) | Active | 0.1.0 |
| [Security Model](/spec/security-model/) | Active | 0.1.0 |
| [Conformance](/spec/conformance/) | Active | 0.1.0 |
| Document | Status |
|---|---|
| [REP-RFC-0001](/spec/rfc-0001/) | Active |
| [Security Model](/spec/security-model/) | Active |
| [Conformance](/spec/conformance/) | Active |

Current versions and the versioning policy are on the [specification overview](/spec/).

Specification documents are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). Code is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ npx @rep-protocol/cli [command]

Validate a `.rep.yaml` manifest against the JSON schema.

Structural check only: the gateway, not `rep validate`, checks each [default](/guides/manifest/#defaults) against its `type` and `pattern`.

```bash
rep validate [--manifest <path>]
```
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/reference/gateway-endpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Returns gateway health status including variable counts and guardrail status.
}
```

Variable counts include manifest [defaults](/guides/manifest/#defaults) the gateway injected for unset optional variables.

**Use cases:**
- Kubernetes liveness/readiness probes
- Load balancer health checks
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/gateway-flags.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ rep-gateway [flags]
| `--upstream` | `localhost:80` | Upstream server address (proxy mode only) |
| `--port` | `8080` | Listen port |
| `--static-dir` | `/usr/share/nginx/html` | Static file directory (embedded mode only) |
| `--manifest` | — | Path to `.rep.yaml` manifest file |
| `--manifest` | — | Path to `.rep.yaml` manifest (validates variables, injects [defaults](/guides/manifest/#defaults)) |
| `--env-file` | — | Path to `.env` file (env vars take precedence) |
| `--strict` | `false` | Exit on guardrail warnings |
| `--hot-reload` | `false` | Enable hot reload SSE endpoint |
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/manifest-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ variables:
tier: public # Required: public, sensitive, or server
type: url # Required: string, url, number, boolean, csv, json, enum
required: true # Optional (default: false)
default: "" # Optional — default value if not provided
default: "" # Optional — injected when unset (ignored if required)
description: "..." # Optional — human-readable description
example: "..." # Optional — example value
pattern: "^..." # Optional — regex pattern the value must match
Expand All @@ -39,7 +39,7 @@ variables:
| `tier` | `public` \| `sensitive` \| `server` | Yes | Security classification tier |
| `type` | see types table | Yes | Value type constraint |
| `required` | `boolean` | No | Must be present at startup (default: `false`) |
| `default` | `string` | No | Default value when variable is absent |
| `default` | `string` | No | Injected into the declared tier when the variable is unset and not `required`. See [Defaults](/guides/manifest/#defaults) |
| `description` | `string` | No | Human-readable purpose |
| `example` | `string` | No | Example value for documentation |
| `pattern` | `string` | No | Regex the value must match |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/spec/conformance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ A conformant SDK implementation MUST:
Conformant implementations MAY implement:

1. Hot reload via Server-Sent Events
2. Manifest file validation at startup
2. Manifest file validation at startup, including injection of declared defaults (§6.3)
3. TypeScript type generation from manifest
4. Framework-specific adapters (React hooks, Vue composables, Svelte stores)
5. Codemod tooling for migration
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/spec/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ REP is defined by three specification documents. They are the authoritative refe

| Document | Status | Version | Description |
|---|---|---|---|
| [REP-RFC-0001](/spec/rfc-0001/) | Active | 0.1.0 | Core protocol specification — variable classification, gateway architecture, SDK API, wire format, deployment patterns |
| [REP-RFC-0001](/spec/rfc-0001/) | Active | 0.2.0 | Core protocol specification — variable classification, gateway architecture, SDK API, wire format, deployment patterns |
| [Security Model](/spec/security-model/) | Active | 0.1.0 | Threat model, 7 threat analyses, hardening recommendations, known limitations |
| [Conformance](/spec/conformance/) | Active | 0.1.0 | Requirements for conformant gateway and SDK implementations |
| [Conformance](/spec/conformance/) | Active | 0.2.0 | Requirements for conformant gateway and SDK implementations |

## Versioning policy

Expand All @@ -21,7 +21,7 @@ The specification uses semantic versioning:
- **Minor** (0.x.0): New optional features, backwards-compatible extensions
- **Major** (x.0.0): Breaking changes to the wire format, API surface, or security model

The current version (0.1.0) indicates the specification is active and subject to refinement based on implementation experience. Breaking changes are possible before 1.0.
Conformance is extracted from REP-RFC-0001 §11 and shares its version. What changed in each RFC version is in its [revision history](https://github.com/ruachtech/rep/blob/main/spec/REP-RFC-0001.md#appendix-c-revision-history). Pre-1.0 versions indicate the specification is active and subject to refinement based on implementation experience. Breaking changes are possible before 1.0.

## License

Expand Down
9 changes: 7 additions & 2 deletions docs/src/content/docs/spec/rfc-0001.mdx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
---
title: REP-RFC-0001 — Core Protocol Specification
description: The core protocol specification for the Runtime Environment Protocol v0.1.0. Variable classification, gateway behaviour, SDK API, wire format, encryption, and deployment patterns.
description: The core protocol specification for the Runtime Environment Protocol v0.2.0. Variable classification, gateway behaviour, SDK API, wire format, encryption, and deployment patterns.
---

import { Aside, LinkCard } from '@astrojs/starlight/components';

```
Title: Runtime Environment Protocol (REP)
Version: 0.1.0
Version: 0.2.0
Status: Active
Authors: Olamide Adebayo (Ruach Tech)
Created: 2026-02-18
Updated: 2026-10-01
License: CC BY 4.0
```

Expand Down Expand Up @@ -38,6 +39,10 @@ The full specification covers 14 sections. Key topics are documented separately
<LinkCard title="Migration" href="/guides/migration/overview/" description="§10 — Incremental adoption, codemod tool" />
<LinkCard title="Conformance" href="/spec/conformance/" description="§11 — Gateway and SDK conformance requirements" />

## Revision history

See [Appendix C: Revision History](https://github.com/ruachtech/rep/blob/main/spec/REP-RFC-0001.md#appendix-c-revision-history).

## Design requirements

| ID | Requirement |
Expand Down
2 changes: 1 addition & 1 deletion examples/.rep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ variables:
tier: public
type: csv
required: false
default: ""
default: "" # injected by the gateway when unset, so rep.get() returns "" not undefined
description: "Comma-separated list of enabled feature flags"
example: "dark-mode,new-checkout,ai-assist"

Expand Down
12 changes: 11 additions & 1 deletion gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ All configuration is via CLI flags or `REP_GATEWAY_*` environment variables. Fla
| `--upstream` | `REP_GATEWAY_UPSTREAM` | `localhost:80` | Upstream address (proxy mode) |
| `--port` | `REP_GATEWAY_PORT` | `8080` | Listen port |
| `--static-dir` | `REP_GATEWAY_STATIC_DIR` | `/usr/share/nginx/html` | Static files dir (embedded mode) |
| `--manifest` | `REP_GATEWAY_MANIFEST` | (none) | `.rep.yaml` to validate against; also injects declared defaults for unset optional variables |
| `--strict` | `REP_GATEWAY_STRICT` | `false` | Fail on guardrail warnings |
| `--hot-reload` | `REP_GATEWAY_HOT_RELOAD` | `false` | Enable SSE hot reload |
| `--hot-reload-mode` | `REP_GATEWAY_HOT_RELOAD_MODE` | `signal` | `file_watch`, `signal`, or `poll` |
Expand Down Expand Up @@ -149,6 +150,7 @@ All configuration is via CLI flags or `REP_GATEWAY_*` environment variables. Fla
│ │ │ │
│ │ 1. Reads REP_* env vars at boot │ │
│ │ 2. Classifies: PUBLIC / SENSITIVE / SERVER │ │
│ │ + with --manifest: validate, inject defaults│ │
│ │ 3. Runs guardrails on PUBLIC values │ │
│ │ 4. Generates AES-256 key + HMAC secret │ │
│ │ 5. Encrypts SENSITIVE vars │ │
Expand Down Expand Up @@ -197,7 +199,7 @@ gateway/

## Specification Compliance

This implementation targets **REP-RFC-0001 v0.1.0**. See the [conformance checklist](../spec/REP-RFC-0001.md#11-conformance) for full details.
This implementation targets **REP-RFC-0001 v0.2.0**. See the [conformance checklist](../spec/REP-RFC-0001.md#11-conformance) for full details.

| Requirement | Status |
|---|---|
Expand All @@ -214,6 +216,14 @@ This implementation targets **REP-RFC-0001 v0.1.0**. See the [conformance checkl
| Hot reload (optional) | ✅ |
| Health check endpoint | ✅ |

## Upgrading

### To 0.1.8

- **Manifest defaults are now injected.** An optional variable that declares `default:` and is unset in every tier is now served with that value, in its declared tier. An app-side fallback such as `rep.get('X') || ''` can go.
- **A default that fails its own type is now a startup error.** Every optional default is checked against its `type` and `pattern` at startup, even while the environment overrides it. For example, `type: url` with `default: ""` used to be ignored and now stops the gateway with a manifest validation error such as `default: variable "X" must be a valid URL`. Fix the default, or remove it.
- The startup summary gains `defaulted_vars`, and each default is logged as `rep.manifest.default_applied` (name and tier, never the value). `public_vars`, `sensitive_vars`, `server_vars` and `/rep/health` counts include defaults.

## License

Apache 2.0 — see [LICENSE](../LICENSE).
57 changes: 33 additions & 24 deletions gateway/internal/config/classify.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func (t Tier) String() string {
}
}

// tiers lists every Tier, in classification order.
var tiers = []Tier{TierPublic, TierSensitive, TierServer}

// Prefix returns the environment variable prefix for the tier, e.g.
// "REP_PUBLIC_".
func (t Tier) Prefix() string {
return "REP_" + strings.ToUpper(t.String()) + "_"
}

// Variable represents a classified environment variable.
type Variable struct {
// Name is the variable name with the REP_<TIER>_ prefix stripped.
Expand Down Expand Up @@ -136,21 +145,8 @@ func ReadAndClassify(envFile string) (*ClassifiedVars, error) {
continue
}

var v Variable
v.OriginalKey = key
v.Value = value

switch {
case strings.HasPrefix(key, "REP_PUBLIC_"):
v.Name = strings.TrimPrefix(key, "REP_PUBLIC_")
v.Tier = TierPublic
case strings.HasPrefix(key, "REP_SENSITIVE_"):
v.Name = strings.TrimPrefix(key, "REP_SENSITIVE_")
v.Tier = TierSensitive
case strings.HasPrefix(key, "REP_SERVER_"):
v.Name = strings.TrimPrefix(key, "REP_SERVER_")
v.Tier = TierServer
default:
v, ok := classify(key, value)
if !ok {
continue
}

Expand All @@ -162,17 +158,30 @@ func ReadAndClassify(envFile string) (*ClassifiedVars, error) {
)
}
seen[v.Name] = v.OriginalKey
vars.add(v)
}

return vars, nil
}

// Classify into tier bucket.
switch v.Tier {
case TierPublic:
vars.Public = append(vars.Public, v)
case TierSensitive:
vars.Sensitive = append(vars.Sensitive, v)
case TierServer:
vars.Server = append(vars.Server, v)
// classify returns the Variable for key when it carries a tier prefix.
func classify(key, value string) (Variable, bool) {
for _, t := range tiers {
if name, ok := strings.CutPrefix(key, t.Prefix()); ok {
return Variable{Name: name, Value: value, Tier: t, OriginalKey: key}, true
}
}
return Variable{}, false
}

return vars, nil
// add appends v to the bucket for its tier.
func (cv *ClassifiedVars) add(v Variable) {
switch v.Tier {
case TierPublic:
cv.Public = append(cv.Public, v)
case TierSensitive:
cv.Sensitive = append(cv.Sensitive, v)
case TierServer:
cv.Server = append(cv.Server, v)
}
}
2 changes: 1 addition & 1 deletion gateway/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func Parse(args []string, version string) (*Config, error) {
fs.StringVar(&cfg.Upstream, "upstream", envOrDefault("REP_GATEWAY_UPSTREAM", "localhost:80"), "Upstream server address (proxy mode)")
fs.IntVar(&cfg.Port, "port", envOrDefaultInt("REP_GATEWAY_PORT", 8080), "Listen port")
fs.StringVar(&cfg.StaticDir, "static-dir", envOrDefault("REP_GATEWAY_STATIC_DIR", "/usr/share/nginx/html"), "Static file directory (embedded mode)")
fs.StringVar(&cfg.ManifestPath, "manifest", envOrDefault("REP_GATEWAY_MANIFEST", manifestPath), "Path to .rep.yaml manifest")
fs.StringVar(&cfg.ManifestPath, "manifest", envOrDefault("REP_GATEWAY_MANIFEST", manifestPath), "Path to .rep.yaml manifest (validates variables, injects defaults)")
fs.BoolVar(&cfg.Strict, "strict", envOrDefaultBool("REP_GATEWAY_STRICT", defaultStrict), "Exit on guardrail warnings")
fs.BoolVar(&cfg.HotReload, "hot-reload", envOrDefaultBool("REP_GATEWAY_HOT_RELOAD", defaultHotReload), "Enable hot reload SSE endpoint")
fs.StringVar(&cfg.HotReloadMode, "hot-reload-mode", envOrDefault("REP_GATEWAY_HOT_RELOAD_MODE", defaultHotReloadMode), `Hot reload mode: "file_watch", "signal", or "poll"`)
Expand Down
Loading
Loading