@mccaskie-labs/workbench-config 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,403 @@
1
+ export declare const MANIFEST_VERSION: "8c09d0a71651";
2
+ export declare const capabilities: {
3
+ readonly ai: {
4
+ readonly $schema: "./schema.json";
5
+ readonly format: 1;
6
+ readonly component: "ai";
7
+ readonly title: "Claude through the Gateway";
8
+ readonly summary: "Text, structured output and streaming from Claude, metered against the project's budget. Apps never hold an Anthropic key.";
9
+ readonly status: "planned";
10
+ readonly surfaces: {
11
+ readonly gateway: readonly ["POST /anthropic/v1/messages", "POST /anthropic/v1/messages/count_tokens"];
12
+ readonly package: readonly ["createAi", "Ai.text", "Ai.structured", "Ai.stream", "Ai.budget", "createFakeAi"];
13
+ };
14
+ readonly config: readonly [{
15
+ readonly name: "WORKBENCH_GATEWAY_KEY";
16
+ readonly required: true;
17
+ readonly secret: true;
18
+ readonly environments: readonly ["staging", "production"];
19
+ readonly description: "Project key with the ai scope, delivered as a Worker secret by gw_key_issue";
20
+ }, {
21
+ readonly name: "WORKBENCH_ENV";
22
+ readonly required: true;
23
+ readonly secret: false;
24
+ readonly environments: readonly ["development", "staging", "production"];
25
+ readonly description: "Drives key checks; never NODE_ENV";
26
+ }];
27
+ readonly allowed: {
28
+ readonly aliases: readonly [{
29
+ readonly alias: "claude-default";
30
+ readonly provider: "anthropic";
31
+ readonly default_model: "claude-sonnet-5-5";
32
+ }, {
33
+ readonly alias: "claude-best";
34
+ readonly provider: "anthropic";
35
+ readonly default_model: "claude-opus-5-5";
36
+ }, {
37
+ readonly alias: "claude-fast";
38
+ readonly provider: "anthropic";
39
+ readonly default_model: "claude-haiku-5-5";
40
+ }];
41
+ readonly explicit_models: readonly ["claude-sonnet-5", "claude-opus-5"];
42
+ readonly server_tools: readonly ["web_search", "web_fetch", "code_execution"];
43
+ readonly required_headers: readonly ["X-Workbench-Job"];
44
+ };
45
+ readonly limits: readonly [{
46
+ readonly id: "ai.retry-attempts";
47
+ readonly value: 3;
48
+ readonly unit: "attempts";
49
+ readonly per: "request";
50
+ readonly enforced_by: "gateway";
51
+ }];
52
+ readonly errors: readonly [{
53
+ readonly code: "model_not_allowed";
54
+ readonly http: 400;
55
+ readonly retryable: false;
56
+ readonly meaning: "Not an alias or an allowlisted model id";
57
+ }, {
58
+ readonly code: "budget_exceeded";
59
+ readonly http: 402;
60
+ readonly retryable: false;
61
+ readonly meaning: "Enforced monthly budget reached";
62
+ }];
63
+ readonly rules: readonly [{
64
+ readonly id: "ai.no-partial-success";
65
+ readonly kind: "must_not";
66
+ readonly text: "Treat a refusal or a max_tokens truncation as a successful result.";
67
+ readonly enforced_by: "types";
68
+ readonly probe: "ai.text.truncation-throws";
69
+ }, {
70
+ readonly id: "ai.store-resolved-model";
71
+ readonly kind: "must";
72
+ readonly text: "Store the resolved model and request id with any output kept.";
73
+ readonly enforced_by: "review";
74
+ readonly probe: "ai.text.claude-default";
75
+ }, {
76
+ readonly id: "ai.aliases-only";
77
+ readonly kind: "must";
78
+ readonly text: "Ask for a Workbench alias (or an allowlisted explicit id), never an arbitrary model id.";
79
+ readonly enforced_by: "gateway";
80
+ readonly probe: "ai.model-not-allowed";
81
+ }];
82
+ readonly examples: readonly [{
83
+ readonly id: "ai.example.text";
84
+ readonly title: "One-shot text";
85
+ readonly lang: "ts";
86
+ readonly code: "const ai = createAi({ key: env.WORKBENCH_GATEWAY_KEY, env: workbenchEnv(env) })\nconst r = await ai.text({ model: 'claude-default', job: 'article.write', prompt })\nawait save({ text: r.text, model: r.model, requestId: r.requestId })";
87
+ readonly probe: "ai.text.claude-default";
88
+ }];
89
+ readonly probes: readonly [{
90
+ readonly id: "ai.text.claude-default";
91
+ readonly half: "runtime";
92
+ readonly expect: "pass";
93
+ readonly smoke: true;
94
+ readonly cost_hint_usd: 0.002;
95
+ }, {
96
+ readonly id: "ai.text.claude-best";
97
+ readonly half: "runtime";
98
+ readonly expect: "pass";
99
+ readonly cost_hint_usd: 0.004;
100
+ }, {
101
+ readonly id: "ai.text.claude-fast";
102
+ readonly half: "runtime";
103
+ readonly expect: "pass";
104
+ readonly cost_hint_usd: 0.001;
105
+ }, {
106
+ readonly id: "ai.stream";
107
+ readonly half: "runtime";
108
+ readonly expect: "pass";
109
+ readonly cost_hint_usd: 0.002;
110
+ }, {
111
+ readonly id: "ai.structured";
112
+ readonly half: "runtime";
113
+ readonly expect: "pass";
114
+ readonly cost_hint_usd: 0.003;
115
+ }, {
116
+ readonly id: "ai.text.truncation-throws";
117
+ readonly half: "runtime";
118
+ readonly expect: "pass";
119
+ readonly cost_hint_usd: 0.001;
120
+ }, {
121
+ readonly id: "ai.model-not-allowed";
122
+ readonly half: "runtime";
123
+ readonly expect: "refused_as_expected";
124
+ readonly refusal: "model_not_allowed";
125
+ readonly cost_hint_usd: 0;
126
+ }, {
127
+ readonly id: "ai.count-tokens";
128
+ readonly half: "runtime";
129
+ readonly expect: "pass";
130
+ readonly smoke: true;
131
+ readonly cost_hint_usd: 0;
132
+ }];
133
+ };
134
+ readonly config: {
135
+ readonly $schema: "./schema.json";
136
+ readonly format: 1;
137
+ readonly component: "config";
138
+ readonly title: "App configuration with workbench-config";
139
+ readonly summary: "Reads WORKBENCH_ENV and the app's own variables and secrets. A missing or invalid variable fails at startup, by name, and no value is ever printed.";
140
+ readonly status: "built";
141
+ readonly surfaces: {
142
+ readonly package: readonly ["workbenchEnv", "defineConfig", "sharedSecret", "workerSecret", "redact", "redactAll", "WorkbenchConfigError", "capabilitySchema", "capabilities", "MANIFEST_VERSION", "compareDevVars"];
143
+ };
144
+ readonly config: readonly [{
145
+ readonly name: "WORKBENCH_ENV";
146
+ readonly required: true;
147
+ readonly secret: false;
148
+ readonly environments: readonly ["development", "staging", "production"];
149
+ readonly description: "development, staging or production. Drives key checks and the email transport; never NODE_ENV (audit F9)";
150
+ }];
151
+ readonly allowed: {
152
+ readonly environments: readonly ["development", "staging", "production"];
153
+ };
154
+ readonly rules: readonly [{
155
+ readonly id: "config.fail-by-name";
156
+ readonly kind: "must";
157
+ readonly text: "Fail at startup when a required variable is missing or invalid, naming every bad variable in one error.";
158
+ readonly enforced_by: "types";
159
+ readonly probe: "config.missing-variable-named";
160
+ }, {
161
+ readonly id: "config.never-print-values";
162
+ readonly kind: "must_not";
163
+ readonly text: "Print, log or include in an error the value of any variable, valid or not.";
164
+ readonly enforced_by: "types";
165
+ readonly probe: "config.value-never-printed";
166
+ }, {
167
+ readonly id: "config.no-node-env";
168
+ readonly kind: "must_not";
169
+ readonly text: "Decide the environment from NODE_ENV or a default. WORKBENCH_ENV is required and has no fallback.";
170
+ readonly enforced_by: "types";
171
+ readonly probe: "config.env-required";
172
+ }, {
173
+ readonly id: "config.dev-vars-example";
174
+ readonly kind: "must";
175
+ readonly text: "Keep .dev.vars.example listing every variable .dev.vars has, with no values; check with npx workbench-config check.";
176
+ readonly enforced_by: "review";
177
+ readonly probe: "config.dev-vars-check";
178
+ }];
179
+ readonly examples: readonly [{
180
+ readonly id: "config.example.define";
181
+ readonly title: "Declare and read the app's variables";
182
+ readonly lang: "ts";
183
+ readonly code: "import { z } from 'zod'\nimport { defineConfig, workbenchEnv } from '@mccaskie-labs/workbench-config'\n\nconst config = defineConfig({\n WORKBENCH_GATEWAY_KEY: z.string().startsWith('wbk_'),\n SITE_NAME: z.string().default('wb-probe'),\n})\n\nexport default {\n async fetch(request: Request, env: Env) {\n const stage = workbenchEnv(env)\n const { WORKBENCH_GATEWAY_KEY, SITE_NAME } = config.parse(env)\n return new Response(`${SITE_NAME} on ${stage}`)\n },\n}";
184
+ readonly probe: "config.missing-variable-named";
185
+ }];
186
+ readonly probes: readonly [{
187
+ readonly id: "config.missing-variable-named";
188
+ readonly half: "runtime";
189
+ readonly expect: "pass";
190
+ readonly smoke: true;
191
+ readonly cost_hint_usd: 0;
192
+ }, {
193
+ readonly id: "config.value-never-printed";
194
+ readonly half: "runtime";
195
+ readonly expect: "pass";
196
+ readonly smoke: true;
197
+ readonly cost_hint_usd: 0;
198
+ }, {
199
+ readonly id: "config.env-required";
200
+ readonly half: "runtime";
201
+ readonly expect: "pass";
202
+ readonly cost_hint_usd: 0;
203
+ }, {
204
+ readonly id: "config.dev-vars-check";
205
+ readonly half: "runtime";
206
+ readonly expect: "pass";
207
+ readonly cost_hint_usd: 0;
208
+ readonly note: "Manual: run npx workbench-config check in the project";
209
+ }];
210
+ };
211
+ readonly gateway: {
212
+ readonly $schema: "./schema.json";
213
+ readonly format: 1;
214
+ readonly component: "gateway";
215
+ readonly title: "Workbench Gateway: keys, headers, budgets and errors";
216
+ readonly summary: "What every Gateway endpoint shares: project keys by environment and scope, the request and response headers, the monthly budget check and the error codes. Apps hold one Workbench key and never a vendor key.";
217
+ readonly status: "planned";
218
+ readonly surfaces: {
219
+ readonly gateway: readonly ["GET /v1/health", "GET /v1/usage", "GET /v1/capabilities"];
220
+ readonly package: readonly ["DEFAULT_GATEWAY_URL"];
221
+ };
222
+ readonly config: readonly [{
223
+ readonly name: "WORKBENCH_GATEWAY_KEY";
224
+ readonly required: true;
225
+ readonly secret: true;
226
+ readonly environments: readonly ["staging", "production"];
227
+ readonly description: "The project's Gateway key for this environment, delivered as a Worker secret by gw_key_issue. Never in code, .dev.vars.example or logs";
228
+ }, {
229
+ readonly name: "WORKBENCH_GATEWAY_URL";
230
+ readonly required: false;
231
+ readonly secret: false;
232
+ readonly default: "https://gateway.mccaskielabs.dev";
233
+ readonly environments: readonly ["development", "staging", "production"];
234
+ readonly description: "Gateway base URL. Leave unset outside tests";
235
+ }];
236
+ readonly allowed: {
237
+ readonly scopes: readonly ["ai", "images", "search", "email"];
238
+ readonly key_environments: readonly [{
239
+ readonly environment: "development";
240
+ readonly prefix: "wbk_dev_";
241
+ }, {
242
+ readonly environment: "staging";
243
+ readonly prefix: "wbk_stg_";
244
+ }, {
245
+ readonly environment: "production";
246
+ readonly prefix: "wbk_prd_";
247
+ }];
248
+ readonly request_headers: readonly [{
249
+ readonly name: "Authorization";
250
+ readonly required: true;
251
+ readonly description: "Bearer <key>. On /anthropic/* the key goes in x-api-key instead, so the official SDK works unchanged";
252
+ }, {
253
+ readonly name: "X-Workbench-Job";
254
+ readonly required: true;
255
+ readonly pattern: "^[a-z0-9._-]{1,64}$";
256
+ readonly description: "What the call is for, e.g. article.write. Recorded with usage";
257
+ }, {
258
+ readonly name: "X-Workbench-Prompt-Version";
259
+ readonly required: false;
260
+ readonly description: "The app's prompt version, recorded with usage (audit F17)";
261
+ }, {
262
+ readonly name: "X-Workbench-Subject";
263
+ readonly required: false;
264
+ readonly pattern: "^[a-z0-9_-]+:[^\\s]{1,128}$";
265
+ readonly description: "type:id of the thing the call is about";
266
+ }, {
267
+ readonly name: "Idempotency-Key";
268
+ readonly required: false;
269
+ readonly description: "Email only: a repeat within 24 hours returns the stored result";
270
+ }];
271
+ readonly response_headers: readonly ["X-Workbench-Request-Id", "Server-Timing"];
272
+ readonly server_timing_metrics: readonly ["gw", "vendor_fb", "vendor", "total"];
273
+ };
274
+ readonly limits: readonly [{
275
+ readonly id: "gateway.key-cache";
276
+ readonly value: 60;
277
+ readonly unit: "seconds";
278
+ readonly per: "isolate";
279
+ readonly enforced_by: "gateway";
280
+ }, {
281
+ readonly id: "gateway.budget-cache";
282
+ readonly value: 60;
283
+ readonly unit: "seconds";
284
+ readonly per: "isolate";
285
+ readonly enforced_by: "gateway";
286
+ }, {
287
+ readonly id: "gateway.job-format";
288
+ readonly value: "[a-z0-9._-]{1,64}";
289
+ readonly unit: "pattern";
290
+ readonly per: "request";
291
+ readonly enforced_by: "gateway";
292
+ readonly refusal: "invalid_request";
293
+ }];
294
+ readonly errors: readonly [{
295
+ readonly code: "unauthorised";
296
+ readonly http: 401;
297
+ readonly retryable: false;
298
+ readonly meaning: "No key, a malformed or unknown key, a revoked key, or a key from the wrong environment";
299
+ }, {
300
+ readonly code: "forbidden_scope";
301
+ readonly http: 403;
302
+ readonly retryable: false;
303
+ readonly meaning: "The key doesn't carry the scope this endpoint needs";
304
+ }, {
305
+ readonly code: "invalid_request";
306
+ readonly http: 400;
307
+ readonly retryable: false;
308
+ readonly meaning: "The body or a required header is missing or malformed";
309
+ }, {
310
+ readonly code: "budget_exceeded";
311
+ readonly http: 402;
312
+ readonly retryable: false;
313
+ readonly meaning: "The environment's enforced monthly budget, or the global ceiling, is reached";
314
+ }, {
315
+ readonly code: "rate_limited";
316
+ readonly http: 429;
317
+ readonly retryable: true;
318
+ readonly meaning: "Per-key rate limit; it stops runaway loops, budgets limit spend";
319
+ }, {
320
+ readonly code: "not_available";
321
+ readonly http: 501;
322
+ readonly retryable: false;
323
+ readonly meaning: "Not built yet, e.g. bulk email";
324
+ }, {
325
+ readonly code: "upstream_error";
326
+ readonly http: 502;
327
+ readonly retryable: true;
328
+ readonly meaning: "The vendor failed after the Gateway's retries";
329
+ }, {
330
+ readonly code: "upstream_timeout";
331
+ readonly http: 504;
332
+ readonly retryable: true;
333
+ readonly meaning: "The vendor didn't answer in time";
334
+ }];
335
+ readonly rules: readonly [{
336
+ readonly id: "gateway.no-vendor-keys";
337
+ readonly kind: "must_not";
338
+ readonly text: "Hold a vendor key (Anthropic, Google, OpenAI, Serper, email) in an app. The Gateway key is the only Workbench credential an app has.";
339
+ readonly enforced_by: "review";
340
+ readonly probe: "gateway.no-vendor-keys.scan";
341
+ }, {
342
+ readonly id: "gateway.key-per-environment";
343
+ readonly kind: "must";
344
+ readonly text: "Use the key issued for the Worker's own environment. A staging key is refused by production and the other way round.";
345
+ readonly enforced_by: "gateway";
346
+ readonly probe: "gateway.wrong-environment";
347
+ }, {
348
+ readonly id: "gateway.request-id-kept";
349
+ readonly kind: "must";
350
+ readonly text: "Keep X-Workbench-Request-Id with any output stored or any error logged, so it can be matched to a usage row.";
351
+ readonly enforced_by: "review";
352
+ readonly probe: "gateway.health";
353
+ }];
354
+ readonly probes: readonly [{
355
+ readonly id: "gateway.health";
356
+ readonly half: "runtime";
357
+ readonly expect: "pass";
358
+ readonly smoke: true;
359
+ readonly cost_hint_usd: 0;
360
+ }, {
361
+ readonly id: "gateway.bad-key";
362
+ readonly half: "runtime";
363
+ readonly expect: "refused_as_expected";
364
+ readonly refusal: "unauthorised";
365
+ readonly smoke: true;
366
+ readonly cost_hint_usd: 0;
367
+ }, {
368
+ readonly id: "gateway.wrong-scope";
369
+ readonly half: "runtime";
370
+ readonly expect: "refused_as_expected";
371
+ readonly refusal: "forbidden_scope";
372
+ readonly cost_hint_usd: 0;
373
+ }, {
374
+ readonly id: "gateway.wrong-environment";
375
+ readonly half: "runtime";
376
+ readonly expect: "refused_as_expected";
377
+ readonly refusal: "unauthorised";
378
+ readonly cost_hint_usd: 0;
379
+ }, {
380
+ readonly id: "gateway.revoked-key";
381
+ readonly half: "runtime";
382
+ readonly expect: "refused_as_expected";
383
+ readonly refusal: "unauthorised";
384
+ readonly cost_hint_usd: 0;
385
+ readonly note: "Stops within 60 seconds of revocation";
386
+ }, {
387
+ readonly id: "gateway.budget-enforced";
388
+ readonly half: "runtime";
389
+ readonly expect: "refused_as_expected";
390
+ readonly refusal: "budget_exceeded";
391
+ readonly cost_hint_usd: 0.01;
392
+ readonly note: "Uses the development key's US$0.01 enforce budget (docs/phase-4-plan.md §3 Q10)";
393
+ }, {
394
+ readonly id: "gateway.no-vendor-keys.scan";
395
+ readonly half: "runtime";
396
+ readonly expect: "pass";
397
+ readonly cost_hint_usd: 0;
398
+ readonly note: "Manual: grep of the repo and cf_worker_secret_list show only WORKBENCH_GATEWAY_* secrets";
399
+ }];
400
+ };
401
+ };
402
+ export type Alias = (typeof capabilities)["ai"]["allowed"]["aliases"][number]["alias"];
403
+ export type GatewayScope = (typeof capabilities)["gateway"]["allowed"]["scopes"][number];