@vellumai/vellum-gateway 0.3.19 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +70 -0
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/__tests__/feature-flag-defaults-sync.test.ts +41 -0
- package/src/__tests__/feature-flags-route.test.ts +536 -0
- package/src/__tests__/guardian-control-plane-proxy.test.ts +187 -0
- package/src/__tests__/schema.test.ts +5 -0
- package/src/config.ts +82 -2
- package/src/feature-flag-defaults.ts +161 -0
- package/src/feature-flag-registry.json +61 -0
- package/src/feature-flags-auth.test.ts +286 -0
- package/src/http/routes/feature-flags.ts +211 -0
- package/src/http/routes/guardian-control-plane-proxy.ts +111 -0
- package/src/index.ts +165 -0
- package/src/schema.ts +134 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -29,6 +29,76 @@ Internet
|
|
|
29
29
|
+-- /webhooks/* --> BLOCKED (404, never forwarded to runtime)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Assistant Feature Flags API
|
|
33
|
+
|
|
34
|
+
The gateway exposes a REST API for reading and mutating assistant feature flags. Assistant feature flags are assistant-scoped, declaration-driven booleans that can gate any assistant behavior. Skill availability is one consumer, but not a required coupling (see [`assistant/ARCHITECTURE.md`](../assistant/ARCHITECTURE.md) for resolver and skill enforcement details).
|
|
35
|
+
|
|
36
|
+
**Unified registry loader:** The gateway loads the unified feature flag registry from `meta/feature-flags/feature-flag-registry.json` (bundled copy at `gateway/src/feature-flag-registry.json`) via `loadFeatureFlagDefaults()` in `gateway/src/feature-flag-defaults.ts`. Only flags with `scope: "assistant"` are used for the API. The registry is loaded once and cached for the lifetime of the process. Invalid entries are skipped with a warning. The `isFlagDeclared()` helper validates that a flag key exists in the registry before allowing writes.
|
|
37
|
+
|
|
38
|
+
**Endpoints (GET/PATCH contract):**
|
|
39
|
+
|
|
40
|
+
| Method | Path | Description |
|
|
41
|
+
|--------|------|-------------|
|
|
42
|
+
| GET | `/v1/feature-flags` | List all declared assistant feature flags from the defaults registry, merged with persisted values from workspace config. Returns `{ flags: FeatureFlagEntry[] }` where each entry has `key`, `enabled`, `defaultEnabled`, and `description`. |
|
|
43
|
+
| PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must match `feature_flags.<flagId>.enabled` and be declared in the defaults registry. Writes to the `assistantFeatureFlagValues` config section. |
|
|
44
|
+
|
|
45
|
+
**Unified registry:** All declared feature flags and their default values are defined in the unified registry at `meta/feature-flags/feature-flag-registry.json` (bundled copy at `gateway/src/feature-flag-registry.json`). The gateway loads this registry on startup via `gateway/src/feature-flag-defaults.ts`, filtering to `scope: "assistant"` flags. Labels come from the registry. The GET endpoint merges persisted overrides with registry defaults to produce the full flag list. The PATCH endpoint validates that the target flag key exists in the registry before accepting a write. Only declared keys are exposed by this API.
|
|
46
|
+
|
|
47
|
+
**Flag key format:** The canonical key format is `feature_flags.<flagId>.enabled`. Only keys matching this pattern are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the `assistantFeatureFlagValues` config section.
|
|
48
|
+
|
|
49
|
+
**Storage:** Flag overrides are persisted in `~/.vellum/workspace/config.json`. Writes go to the `assistantFeatureFlagValues` section as a `Record<string, boolean>`. The GET endpoint reads from `assistantFeatureFlagValues` and merges with registry defaults. The gateway writes atomically (temp file + rename). The daemon's config watcher hot-reloads changes, so flag mutations take effect on the next session or tool resolution without a restart.
|
|
50
|
+
|
|
51
|
+
**Token separation (authentication boundary):**
|
|
52
|
+
|
|
53
|
+
The assistant feature flags API uses a dedicated token (the **feature-flag token**) stored at `~/.vellum/feature-flag-token`, separate from the **runtime token** (`~/.vellum/http-token`). This separation ensures that clients with feature-flag access cannot access runtime endpoints, and vice versa.
|
|
54
|
+
|
|
55
|
+
| Operation | Accepted tokens |
|
|
56
|
+
|-----------|----------------|
|
|
57
|
+
| `GET /v1/feature-flags` | Runtime bearer token OR feature-flag token |
|
|
58
|
+
| `PATCH /v1/feature-flags/:key` | Feature-flag token ONLY (runtime token is explicitly rejected) |
|
|
59
|
+
|
|
60
|
+
The feature-flag token is auto-generated on first gateway startup if the file does not exist. The gateway watches the token file for changes and hot-reloads without restart.
|
|
61
|
+
|
|
62
|
+
**`assistantFeatureFlagValues` config section:** This is the canonical storage location for assistant feature flag overrides. It is a `Record<string, boolean>` keyed by canonical flag keys (`feature_flags.<id>.enabled`). The gateway's PATCH handler writes exclusively to this section. The daemon's resolver reads it with highest priority, falling back to the legacy `featureFlags` section and then the defaults registry. Undeclared keys are ignored by the resolver.
|
|
63
|
+
|
|
64
|
+
**Key source files:**
|
|
65
|
+
|
|
66
|
+
| File | Purpose |
|
|
67
|
+
|------|---------|
|
|
68
|
+
| `gateway/src/http/routes/feature-flags.ts` | GET and PATCH handlers; config read/write logic; legacy key mapping; key format validation |
|
|
69
|
+
| `gateway/src/feature-flag-defaults.ts` | `loadFeatureFlagDefaults()` — loads the shared defaults registry; `isFlagDeclared()` — validates flag keys |
|
|
70
|
+
| `gateway/src/config.ts` | `readOrGenerateFeatureFlagToken()` — token provisioning; `featureFlagToken` config field |
|
|
71
|
+
| `gateway/src/index.ts` | Route registration, auth enforcement (dual-token for GET, flag-token-only for PATCH), token file watcher |
|
|
72
|
+
| `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
|
|
73
|
+
| `gateway/src/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
|
|
74
|
+
|
|
75
|
+
### Guardian Verification Control-Plane Proxy
|
|
76
|
+
|
|
77
|
+
Guardian verification endpoints are exposed directly by the gateway and forwarded to runtime integration handlers even when the broad runtime proxy is disabled. This keeps assistant skills and user-facing tooling on gateway URLs only.
|
|
78
|
+
|
|
79
|
+
**Forwarded endpoints:**
|
|
80
|
+
|
|
81
|
+
| Method | Path |
|
|
82
|
+
|--------|------|
|
|
83
|
+
| POST | `/v1/integrations/guardian/challenge` |
|
|
84
|
+
| GET | `/v1/integrations/guardian/status` |
|
|
85
|
+
| POST | `/v1/integrations/guardian/outbound/start` |
|
|
86
|
+
| POST | `/v1/integrations/guardian/outbound/resend` |
|
|
87
|
+
| POST | `/v1/integrations/guardian/outbound/cancel` |
|
|
88
|
+
|
|
89
|
+
**Authentication boundary:**
|
|
90
|
+
|
|
91
|
+
- Gateway validates caller bearer auth against the runtime token.
|
|
92
|
+
- Gateway forwards requests to runtime with the runtime bearer token and `X-Gateway-Origin` proof header.
|
|
93
|
+
- Upstream 4xx/5xx responses are passed through, while connection errors return `502` and timeouts return `504`.
|
|
94
|
+
|
|
95
|
+
**Key source files:**
|
|
96
|
+
|
|
97
|
+
| File | Purpose |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| `gateway/src/http/routes/guardian-control-plane-proxy.ts` | Guardian control-plane proxy handlers and upstream forwarding |
|
|
100
|
+
| `gateway/src/index.ts` | Route registration and bearer-auth enforcement for `/v1/integrations/guardian/*` |
|
|
101
|
+
|
|
32
102
|
### Channel Binding Lifecycle (Lane Separation)
|
|
33
103
|
|
|
34
104
|
Each channel (desktop, Telegram, etc.) operates in its own **lane**: conversations created by an external channel are never displayed in the desktop thread list, and desktop conversations are never exposed to external channels. The `channelBinding` metadata on a conversation is used solely for routing inbound/outbound messages within that lane and for filtering sessions during desktop session restoration.
|
package/README.md
CHANGED
|
@@ -216,6 +216,11 @@ The gateway serves as the single public ingress point for all external callbacks
|
|
|
216
216
|
| `/webhooks/twilio/sms` | POST | Twilio SMS webhook — validates X-Twilio-Signature (HMAC-SHA1), normalizes into `GatewayInboundEventV1` with `sourceChannel: "sms"`, deduplicates by `MessageSid`, and forwards to runtime |
|
|
217
217
|
| `/deliver/sms` | POST | Internal endpoint for the assistant runtime to deliver outbound SMS messages via the Twilio Messages API |
|
|
218
218
|
| `/webhooks/oauth/callback` | GET | OAuth2 callback endpoint — receives authorization codes from OAuth providers (Google, Slack, etc.) and forwards them to the assistant runtime |
|
|
219
|
+
| `/v1/integrations/guardian/challenge` | POST | Authenticated control-plane proxy for creating guardian verification challenges |
|
|
220
|
+
| `/v1/integrations/guardian/status` | GET | Authenticated control-plane proxy for guardian binding status |
|
|
221
|
+
| `/v1/integrations/guardian/outbound/start` | POST | Authenticated control-plane proxy for starting outbound guardian verification |
|
|
222
|
+
| `/v1/integrations/guardian/outbound/resend` | POST | Authenticated control-plane proxy for resending outbound guardian verification |
|
|
223
|
+
| `/v1/integrations/guardian/outbound/cancel` | POST | Authenticated control-plane proxy for cancelling outbound guardian verification |
|
|
219
224
|
| `/healthz` | GET | Liveness probe |
|
|
220
225
|
| `/readyz` | GET | Readiness probe |
|
|
221
226
|
| `/schema` | GET | Returns the OpenAPI 3.1 schema for this gateway |
|
|
@@ -283,9 +288,9 @@ Inbound SMS follows the same gateway-only pattern as voice and Telegram:
|
|
|
283
288
|
|
|
284
289
|
When `INGRESS_PUBLIC_BASE_URL` is configured, the gateway prioritizes it as the canonical URL for Twilio signature validation. If the signature only validates against the raw local request URL (fallback), a warning is logged indicating potential drift between the configured ingress URL and the actual webhook registration. The raw URL fallback is preserved for local-dev operability.
|
|
285
290
|
|
|
286
|
-
## Default Mode:
|
|
291
|
+
## Default Mode: Dedicated Routes Only
|
|
287
292
|
|
|
288
|
-
By default the
|
|
293
|
+
By default, the broad runtime proxy is disabled. Dedicated gateway-managed routes (webhooks, delivery endpoints, and explicit control-plane proxies such as `/v1/integrations/guardian/*`) remain available, but arbitrary runtime passthrough routes return `404` unless `GATEWAY_RUNTIME_PROXY_ENABLED=true`.
|
|
289
294
|
|
|
290
295
|
## Runtime Proxy Mode
|
|
291
296
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
describe("feature flag registry availability", () => {
|
|
7
|
+
test("unified registry exists and contains assistant-scope flags", () => {
|
|
8
|
+
const repoRoot = join(process.cwd(), "..");
|
|
9
|
+
const registryPath = join(repoRoot, "meta", "feature-flags", "feature-flag-registry.json");
|
|
10
|
+
|
|
11
|
+
const raw = readFileSync(registryPath, "utf-8");
|
|
12
|
+
const registry = JSON.parse(raw);
|
|
13
|
+
|
|
14
|
+
expect(registry.version).toBe(1);
|
|
15
|
+
expect(Array.isArray(registry.flags)).toBe(true);
|
|
16
|
+
|
|
17
|
+
const assistantFlags = registry.flags.filter((f: { scope: string }) => f.scope === "assistant");
|
|
18
|
+
expect(assistantFlags.length).toBeGreaterThan(0);
|
|
19
|
+
|
|
20
|
+
// Every assistant-scope flag should have required fields
|
|
21
|
+
for (const flag of assistantFlags) {
|
|
22
|
+
expect(typeof flag.id).toBe("string");
|
|
23
|
+
expect(typeof flag.key).toBe("string");
|
|
24
|
+
expect(typeof flag.label).toBe("string");
|
|
25
|
+
expect(typeof flag.description).toBe("string");
|
|
26
|
+
expect(typeof flag.defaultEnabled).toBe("boolean");
|
|
27
|
+
expect(flag.scope).toBe("assistant");
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("bundled gateway/src/feature-flag-registry.json matches canonical meta/ copy", () => {
|
|
32
|
+
const repoRoot = join(process.cwd(), "..");
|
|
33
|
+
const canonicalPath = join(repoRoot, "meta", "feature-flags", "feature-flag-registry.json");
|
|
34
|
+
const bundledPath = join(process.cwd(), "src", "feature-flag-registry.json");
|
|
35
|
+
|
|
36
|
+
const canonical = JSON.parse(readFileSync(canonicalPath, "utf-8"));
|
|
37
|
+
const bundled = JSON.parse(readFileSync(bundledPath, "utf-8"));
|
|
38
|
+
|
|
39
|
+
expect(bundled).toEqual(canonical);
|
|
40
|
+
});
|
|
41
|
+
});
|