@vellumai/vellum-gateway 0.5.10 → 0.5.11

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/AGENTS.md CHANGED
@@ -25,9 +25,9 @@ All assistant API requests from clients, CLI, skills, and user-facing tooling **
25
25
 
26
26
  **Ban on hardcoded runtime hosts/ports:** Do not embed `localhost:7821`, `127.0.0.1:7821`, or runtime-port-derived URLs in docs, skills, or user-facing guidance. Always reference gateway URLs instead. A CI guard test (`gateway-only-guard.test.ts`) enforces this — any new direct runtime URL reference in production code or skills will fail CI.
27
27
 
28
- **SKILL.md retrieval contract:** For config/status retrieval in bundled skills, use `bash` + canonical CLI surfaces. Start with `assistant config get` for generic config keys and secure credential surfaces (`credential_store`, `assistant keys`) for secrets. Do not use direct gateway `curl` for read-only retrieval paths. Do not use keychain lookup commands (`security find-generic-password`, `secret-tool`) in SKILL.md. `host_bash` is not allowed for Vellum CLI retrieval commands unless a documented exception is intentionally allowlisted.
28
+ **SKILL.md retrieval contract:** For config/status retrieval in bundled skills, use `bash` + canonical CLI surfaces. Start with `assistant config get` for generic config keys and secure credential surfaces (`credential_store`, `assistant keys`) for secrets. Do not use direct gateway `curl` for read-only retrieval paths. Do not use credential store lookup commands (`security find-generic-password`, `secret-tool`) in SKILL.md. `host_bash` is not allowed for Vellum CLI retrieval commands unless a documented exception is intentionally allowlisted.
29
29
 
30
- **SKILL.md proxied outbound pattern:** For outbound third-party API calls from skills that require stored credentials, default to `bash` with `network_mode: "proxied"` and `credential_ids` instead of manual token/keychain plumbing. This keeps credentials out of chat and enforces credential policies consistently.
30
+ **SKILL.md proxied outbound pattern:** For outbound third-party API calls from skills that require stored credentials, default to `bash` with `network_mode: "proxied"` and `credential_ids` instead of manual token/credential store plumbing. This keeps credentials out of chat and enforces credential policies consistently.
31
31
 
32
32
  **SKILL.md gateway URL pattern:** For gateway control-plane writes/actions that are not exposed through a CLI read command, use `$INTERNAL_GATEWAY_BASE_URL` (injected by `bash` and `host_bash`). Do not hardcode `localhost`/ports in skill examples, and do not instruct users/agents to manually export the variable from Settings. For public ingress URLs (e.g. OAuth redirect URIs, webhook registration), use `assistant config get ingress.publicBaseUrl` or load the `public-ingress` skill — do not inject public URLs as environment variables.
33
33
 
package/ARCHITECTURE.md CHANGED
@@ -43,11 +43,11 @@ The gateway exposes a REST API for reading and mutating assistant feature flags.
43
43
  | Method | Path | Description |
44
44
  | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
45
  | GET | `/v1/feature-flags` | List all declared assistant feature flags from the defaults registry, merged with persisted values from the feature flag store. Returns `{ flags: FeatureFlagEntry[] }` where each entry has `key`, `enabled`, `defaultEnabled`, and `description`. |
46
- | 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 `~/.vellum/protected/feature-flags.json`. |
46
+ | PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must be a simple kebab-case flag key declared in the defaults registry. Writes to `~/.vellum/protected/feature-flags.json`. |
47
47
 
48
48
  **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.
49
49
 
50
- **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 protected feature flag store (`~/.vellum/protected/feature-flags.json`).
50
+ **Flag key format:** The canonical key format is simple kebab-case (e.g., `browser`, `ces-tools`). Only keys matching this pattern and declared in the registry are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the protected feature flag store (`~/.vellum/protected/feature-flags.json`).
51
51
 
52
52
  **Storage:** Flag overrides are persisted in `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store uses a versioned JSON format (`{ version: 1, values: Record<string, boolean> }`). The GET endpoint reads from the feature flag store and merges with registry defaults. The gateway writes atomically (temp file + rename, 0o600 permissions). The daemon's config watcher monitors the protected directory and hot-reloads changes, so flag mutations take effect on the next session or tool resolution without a restart.
53
53
 
@@ -62,7 +62,7 @@ The assistant feature flags API uses a dedicated feature-flag token stored at `~
62
62
 
63
63
  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.
64
64
 
65
- **Protected feature flag store:** The canonical storage for assistant feature flag overrides is `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store is managed by `gateway/src/feature-flag-store.ts` and uses a versioned JSON format with `Record<string, boolean>` values keyed by canonical flag keys (`feature_flags.<id>.enabled`). The gateway's PATCH handler writes exclusively to this store. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
65
+ **Protected feature flag store:** The canonical storage for assistant feature flag overrides is `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store is managed by `gateway/src/feature-flag-store.ts` and uses a versioned JSON format with `Record<string, boolean>` values keyed by canonical flag keys (simple kebab-case, e.g., `browser`). The gateway's PATCH handler writes exclusively to this store. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
66
66
 
67
67
  **Key source files:**
68
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -25,7 +25,8 @@ mock.module("../logger.js", () => ({
25
25
 
26
26
  import {
27
27
  readCredential,
28
- readTelegramCredentials,
28
+ readServiceCredentials,
29
+ type ServiceCredentialSpec,
29
30
  } from "../credential-reader.js";
30
31
 
31
32
  // ---------------------------------------------------------------------------
@@ -161,51 +162,6 @@ afterEach(() => {
161
162
  }
162
163
  });
163
164
 
164
- // ---------------------------------------------------------------------------
165
- // Tests: encrypted store (existing)
166
- // ---------------------------------------------------------------------------
167
-
168
- describe("readTelegramCredentials", () => {
169
- test("returns null when metadata file does not exist", async () => {
170
- const result = await readTelegramCredentials();
171
- expect(result).toBeNull();
172
- });
173
-
174
- test("returns null when metadata has no Telegram entries", async () => {
175
- writeMetadata([{ service: "github", field: "token" }]);
176
- const result = await readTelegramCredentials();
177
- expect(result).toBeNull();
178
- });
179
-
180
- test("returns null when metadata exists but secrets are missing from encrypted store", async () => {
181
- writeMetadata([
182
- { service: "telegram", field: "bot_token" },
183
- { service: "telegram", field: "webhook_secret" },
184
- ]);
185
-
186
- const result = await readTelegramCredentials();
187
- expect(result).toBeNull();
188
- });
189
-
190
- test("returns credentials from encrypted store", async () => {
191
- writeMetadata([
192
- { service: "telegram", field: "bot_token" },
193
- { service: "telegram", field: "webhook_secret" },
194
- ]);
195
-
196
- writeEncryptedStore({
197
- [credentialKey("telegram", "bot_token")]: "enc-bot-token",
198
- [credentialKey("telegram", "webhook_secret")]: "enc-webhook-secret",
199
- });
200
-
201
- const result = await readTelegramCredentials();
202
- expect(result).toEqual({
203
- botToken: "enc-bot-token",
204
- webhookSecret: "enc-webhook-secret",
205
- });
206
- });
207
- });
208
-
209
165
  // ---------------------------------------------------------------------------
210
166
  // Tests: v2 encrypted store (store.key)
211
167
  // ---------------------------------------------------------------------------
@@ -239,38 +195,129 @@ describe("v2 encrypted store with store.key", () => {
239
195
  const result = await readCredential(credentialKey("test", "key"));
240
196
  expect(result).toBeUndefined();
241
197
  });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Tests: v1 encrypted store backward compatibility
202
+ // ---------------------------------------------------------------------------
203
+
204
+ describe("v1 encrypted store backward compatibility", () => {
205
+ test("v1 store continues to work with entropy-based key derivation", async () => {
206
+ writeEncryptedStore({
207
+ [credentialKey("test", "key")]: "v1-secret-value",
208
+ });
209
+
210
+ const result = await readCredential(credentialKey("test", "key"));
211
+ expect(result).toBe("v1-secret-value");
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Tests: generic readServiceCredentials
217
+ // ---------------------------------------------------------------------------
218
+
219
+ describe("readServiceCredentials", () => {
220
+ const telegramSpec: ServiceCredentialSpec = {
221
+ service: "telegram",
222
+ requiredFields: ["bot_token", "webhook_secret"],
223
+ };
242
224
 
243
- test("returns Telegram credentials from v2 store", async () => {
225
+ test("returns correct Record<string, string> for a valid spec", async () => {
244
226
  writeMetadata([
245
227
  { service: "telegram", field: "bot_token" },
246
228
  { service: "telegram", field: "webhook_secret" },
247
229
  ]);
248
230
 
249
- writeEncryptedStoreV2({
250
- [credentialKey("telegram", "bot_token")]: "v2-bot-token",
251
- [credentialKey("telegram", "webhook_secret")]: "v2-webhook-secret",
231
+ writeEncryptedStore({
232
+ [credentialKey("telegram", "bot_token")]: "my-bot-token",
233
+ [credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
252
234
  });
253
235
 
254
- const result = await readTelegramCredentials();
236
+ const result = await readServiceCredentials(telegramSpec);
255
237
  expect(result).toEqual({
256
- botToken: "v2-bot-token",
257
- webhookSecret: "v2-webhook-secret",
238
+ bot_token: "my-bot-token",
239
+ webhook_secret: "my-webhook-secret",
258
240
  });
259
241
  });
260
- });
261
242
 
262
- // ---------------------------------------------------------------------------
263
- // Tests: v1 encrypted store backward compatibility
264
- // ---------------------------------------------------------------------------
243
+ test("returns null when metadata is missing", async () => {
244
+ // No metadata file written at all
245
+ const result = await readServiceCredentials(telegramSpec);
246
+ expect(result).toBeNull();
247
+ });
248
+
249
+ test("returns null when metadata has no entries for the service", async () => {
250
+ writeMetadata([{ service: "github", field: "token" }]);
251
+
252
+ const result = await readServiceCredentials(telegramSpec);
253
+ expect(result).toBeNull();
254
+ });
255
+
256
+ test("returns null when metadata exists but encrypted values cannot be read", async () => {
257
+ writeMetadata([
258
+ { service: "telegram", field: "bot_token" },
259
+ { service: "telegram", field: "webhook_secret" },
260
+ ]);
261
+ // No encrypted store written — secrets are unreadable
262
+
263
+ const result = await readServiceCredentials(telegramSpec);
264
+ expect(result).toBeNull();
265
+ });
266
+
267
+ test("returns null when only some required fields exist in metadata", async () => {
268
+ writeMetadata([
269
+ { service: "telegram", field: "bot_token" },
270
+ // webhook_secret is missing from metadata
271
+ ]);
265
272
 
266
- describe("v1 encrypted store backward compatibility", () => {
267
- test("v1 store continues to work with entropy-based key derivation", async () => {
268
273
  writeEncryptedStore({
269
- [credentialKey("test", "key")]: "v1-secret-value",
274
+ [credentialKey("telegram", "bot_token")]: "my-bot-token",
275
+ [credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
270
276
  });
271
277
 
272
- const result = await readCredential(credentialKey("test", "key"));
273
- expect(result).toBe("v1-secret-value");
278
+ const result = await readServiceCredentials(telegramSpec);
279
+ expect(result).toBeNull();
280
+ });
281
+
282
+ test("works for a hypothetical new service spec (extensibility)", async () => {
283
+ const customSpec: ServiceCredentialSpec = {
284
+ service: "test_service",
285
+ requiredFields: ["api_key", "secret"],
286
+ };
287
+
288
+ writeMetadata([
289
+ { service: "test_service", field: "api_key" },
290
+ { service: "test_service", field: "secret" },
291
+ ]);
292
+
293
+ writeEncryptedStore({
294
+ [credentialKey("test_service", "api_key")]: "custom-api-key",
295
+ [credentialKey("test_service", "secret")]: "custom-secret",
296
+ });
297
+
298
+ const result = await readServiceCredentials(customSpec);
299
+ expect(result).toEqual({
300
+ api_key: "custom-api-key",
301
+ secret: "custom-secret",
302
+ });
303
+ });
304
+
305
+ test("works with v2 encrypted store", async () => {
306
+ writeMetadata([
307
+ { service: "telegram", field: "bot_token" },
308
+ { service: "telegram", field: "webhook_secret" },
309
+ ]);
310
+
311
+ writeEncryptedStoreV2({
312
+ [credentialKey("telegram", "bot_token")]: "v2-bot-token",
313
+ [credentialKey("telegram", "webhook_secret")]: "v2-webhook-secret",
314
+ });
315
+
316
+ const result = await readServiceCredentials(telegramSpec);
317
+ expect(result).toEqual({
318
+ bot_token: "v2-bot-token",
319
+ webhook_secret: "v2-webhook-secret",
320
+ });
274
321
  });
275
322
  });
276
323
 
@@ -297,7 +344,7 @@ describe("secret leak prevention", () => {
297
344
  expect(serialized).not.toContain(secretValue);
298
345
  });
299
346
 
300
- test("failed encrypted store read does not leak secret values into logs", async () => {
347
+ test("service credential read does not leak secret values into logs", async () => {
301
348
  const secretValue = "super-secret-telegram-token";
302
349
 
303
350
  writeMetadata([
@@ -309,7 +356,10 @@ describe("secret leak prevention", () => {
309
356
  [credentialKey("telegram", "webhook_secret")]: "webhook-secret-value",
310
357
  });
311
358
 
312
- const result = await readTelegramCredentials();
359
+ const result = await readServiceCredentials({
360
+ service: "telegram",
361
+ requiredFields: ["bot_token", "webhook_secret"],
362
+ });
313
363
  expect(result).not.toBeNull();
314
364
 
315
365
  const serialized = allLogStrings();