@byok-sdk/keys 0.3.9 → 0.3.10

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/README.md CHANGED
@@ -125,6 +125,18 @@ authority selection, not a mirror, migration shim, cache, or dual-write path.
125
125
  It stores the complete four-ID provider registry as one versioned deterministic
126
126
  snapshot so delete and the one-enabled invariant share one CAS decision.
127
127
 
128
+ Each normalized profile also has an exact credential-free binding. The
129
+ registry exposes `profile_revision` (a strictly advancing decimal derived from
130
+ `updated_at`) and `profile_hash` (SHA-256 over the normalized runtime-relevant,
131
+ non-secret record). `exactProviderProfileBinding()` and
132
+ `assertExactProviderProfileBinding()` are the local authority used by Agent
133
+ admission. Base URLs and credential bytes never enter the wire binding.
134
+
135
+ For `byok-profile` launches, the Pi launcher receives the exact ref,
136
+ revision/hash, model, and required capabilities. A validation-only invocation
137
+ checks the read-only SQLite authority before task claim; the launch invocation
138
+ checks the same fields again before reading the OS credential or spawning Pi.
139
+
128
140
  ## Module inventory
129
141
 
130
142
  Every module under `src/`, one line of responsibility each. The public surface is
@@ -134,7 +146,7 @@ whatever `index.ts` re-exports; nothing here is reachable by deep import.
134
146
  | --- | --- |
135
147
  | `index.ts` | The package barrel — the single public entry point, and the only supported import path |
136
148
  | `errors.ts` | `ByokKeysError` (`code` + message) and `BYOK_KEYS_ERROR_CODES`, the code strings consumers branch on |
137
- | `provider-profile.ts` | zod schema for the model provider profile, including the adapter/auth-mode legality rules |
149
+ | `provider-profile.ts` | zod schema plus exact credential-free revision/hash binding for the model provider profile |
138
150
  | `headers.ts` | `providerHeaders()` and fail-closed `requiredProviderSecret()` |
139
151
  | `url.ts` | `normalizeProviderUrl()` with the HTTPS / loopback / private-network guard |
140
152
  | `http.ts` | Shared transport guards: injectable `fetch`, timeout, bounded JSON, HTTP error classification |
@@ -4,6 +4,7 @@ import { promises, mkdirSync, existsSync, chmodSync } from 'fs';
4
4
  import os from 'os';
5
5
  import path2, { dirname } from 'path';
6
6
  import { z } from 'zod';
7
+ import { createHash } from 'crypto';
7
8
  import { createRequire } from 'module';
8
9
 
9
10
  // src/errors.ts
@@ -71,16 +72,202 @@ function assertSecretNamespace(value) {
71
72
  return normalized;
72
73
  }
73
74
 
75
+ // src/url.ts
76
+ function normalizeProviderUrl(value) {
77
+ let url;
78
+ try {
79
+ url = new URL(value);
80
+ } catch {
81
+ throw new ByokKeysError(
82
+ "PROVIDER_URL_INVALID",
83
+ "Provider base URL must be absolute"
84
+ );
85
+ }
86
+ if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
87
+ throw new ByokKeysError(
88
+ "PROVIDER_URL_INVALID",
89
+ "Provider URL requires HTTPS; HTTP is allowed only for localhost"
90
+ );
91
+ }
92
+ if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
93
+ throw new ByokKeysError(
94
+ "PROVIDER_URL_INVALID",
95
+ "Private-network provider IPs are not allowed"
96
+ );
97
+ }
98
+ return url.toString().replace(/\/$/u, "");
99
+ }
100
+ function isLoopbackHost(hostname) {
101
+ const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
102
+ return value === "localhost" || value === "127.0.0.1" || value === "::1";
103
+ }
104
+ function isPrivateNetworkLiteral(hostname) {
105
+ const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
106
+ if (value.includes(":")) return true;
107
+ const parts = value.split(".").map(Number);
108
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
109
+ return false;
110
+ }
111
+ return parts[0] === 10 || parts[0] === 127 || parts[0] === 169 && parts[1] === 254 || parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31 || parts[0] === 192 && parts[1] === 168;
112
+ }
113
+
114
+ // src/provider-profile.ts
115
+ var PROVIDER_PROFILE_REF_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u;
116
+ var ProviderProfileRefSchema = z.string().min(1).max(64).regex(
117
+ PROVIDER_PROFILE_REF_PATTERN,
118
+ "provider profile refs must be lowercase portable identifiers"
119
+ );
120
+ var MODEL_PROVIDER_KINDS = [
121
+ "openai",
122
+ "deepseek",
123
+ "anthropic",
124
+ "custom"
125
+ ];
126
+ var PROVIDER_MODEL_CAPABILITIES = ["image-input"];
127
+ var ProviderModelCapabilitySchema = z.enum(PROVIDER_MODEL_CAPABILITIES);
128
+ var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
129
+ var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
130
+ function boundedString(field, maximumLength) {
131
+ return z.string().superRefine((value, ctx) => {
132
+ if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
133
+ ctx.addIssue({
134
+ code: "custom",
135
+ message: `Provider ${field} is invalid`
136
+ });
137
+ }
138
+ }).transform((value) => value.trim());
139
+ }
140
+ function isoTimestamp(field) {
141
+ return boundedString(field, 64).superRefine((value, ctx) => {
142
+ if (!Number.isFinite(Date.parse(value))) {
143
+ ctx.addIssue({
144
+ code: "custom",
145
+ message: `Provider ${field} must be an ISO timestamp`
146
+ });
147
+ }
148
+ });
149
+ }
150
+ var providerBaseUrl = z.string().superRefine((value, ctx) => {
151
+ try {
152
+ normalizeProviderUrl(value);
153
+ } catch (error) {
154
+ ctx.addIssue({
155
+ code: "custom",
156
+ message: error instanceof Error ? error.message : "Provider base URL is invalid",
157
+ params: {
158
+ byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
159
+ }
160
+ });
161
+ }
162
+ }).transform((value) => normalizeProviderUrl(value));
163
+ var ModelProviderProfileSchema = z.object({
164
+ adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
165
+ auth_mode: z.enum(PROVIDER_AUTH_MODES),
166
+ base_url: providerBaseUrl,
167
+ capabilities: z.array(ProviderModelCapabilitySchema).max(8),
168
+ created_at: isoTimestamp("created_at"),
169
+ display_name: boundedString("display_name", 100),
170
+ enabled: z.boolean(),
171
+ kind: z.literal("model"),
172
+ model: boundedString("model", 160),
173
+ profile_ref: ProviderProfileRefSchema,
174
+ provider_kind: z.enum(MODEL_PROVIDER_KINDS),
175
+ updated_at: isoTimestamp("updated_at")
176
+ }).superRefine((profile, ctx) => {
177
+ if (new Set(profile.capabilities).size !== profile.capabilities.length) {
178
+ ctx.addIssue({
179
+ code: "custom",
180
+ message: "Provider capabilities cannot repeat",
181
+ path: ["capabilities"]
182
+ });
183
+ }
184
+ if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
185
+ ctx.addIssue({
186
+ code: "custom",
187
+ message: "Anthropic requires x_api_key authentication",
188
+ path: ["auth_mode"]
189
+ });
190
+ }
191
+ if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
192
+ ctx.addIssue({
193
+ code: "custom",
194
+ message: "OpenAI-compatible providers support bearer or no authentication",
195
+ path: ["auth_mode"]
196
+ });
197
+ }
198
+ if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
199
+ ctx.addIssue({
200
+ code: "custom",
201
+ message: "Provider updated_at cannot precede created_at",
202
+ path: ["updated_at"]
203
+ });
204
+ }
205
+ });
206
+ function exactProviderProfileBinding(profileInput, requiredCapabilities = profileInput.capabilities) {
207
+ const profile = parseModelProviderProfile(profileInput);
208
+ const revision = Date.parse(profile.updated_at);
209
+ if (!Number.isSafeInteger(revision) || revision < 0) {
210
+ throw new ByokKeysError(
211
+ "PROVIDER_PROFILE_INVALID",
212
+ "Provider updated_at cannot be represented as a canonical revision"
213
+ );
214
+ }
215
+ const normalizedCapabilities = [...profile.capabilities].sort();
216
+ const canonical = JSON.stringify({
217
+ adapter: profile.adapter,
218
+ auth_mode: profile.auth_mode,
219
+ base_url: profile.base_url,
220
+ capabilities: normalizedCapabilities,
221
+ kind: profile.kind,
222
+ model: profile.model,
223
+ profile_ref: profile.profile_ref,
224
+ provider_kind: profile.provider_kind
225
+ });
226
+ return {
227
+ profileRef: profile.profile_ref,
228
+ profileRevision: String(revision),
229
+ profileHash: `sha256:${createHash("sha256").update(canonical).digest("hex")}`,
230
+ modelId: profile.model,
231
+ requiredCapabilities: [...requiredCapabilities]
232
+ };
233
+ }
234
+ function assertExactProviderProfileBinding(profile, expected) {
235
+ if (new Set(expected.requiredCapabilities).size !== expected.requiredCapabilities.length) {
236
+ throw new Error("provider profile required capabilities must be unique");
237
+ }
238
+ const actual = exactProviderProfileBinding(profile, expected.requiredCapabilities);
239
+ if (actual.profileRef !== expected.profileRef) throw new Error("provider profile ref mismatch");
240
+ if (actual.profileRevision !== expected.profileRevision) throw new Error("provider profile revision mismatch");
241
+ if (actual.profileHash !== expected.profileHash) throw new Error("provider profile hash mismatch");
242
+ if (actual.modelId !== expected.modelId) throw new Error("provider profile model mismatch");
243
+ const supported = new Set(profile.capabilities);
244
+ const unsupported = expected.requiredCapabilities.find((capability) => !supported.has(capability));
245
+ if (unsupported !== void 0) throw new Error(`provider profile does not support required capability ${unsupported}`);
246
+ }
247
+ function parseModelProviderProfile(value) {
248
+ const result = ModelProviderProfileSchema.safeParse(value);
249
+ if (result.success) return result.data;
250
+ const issue = result.error.issues[0];
251
+ const params = issue?.params;
252
+ throw new ByokKeysError(
253
+ params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
254
+ issue?.message ?? "Provider profile is invalid",
255
+ { cause: result.error }
256
+ );
257
+ }
258
+
74
259
  // src/secret-store.ts
75
260
  var DEFAULT_SECRET_SERVICE_PREFIX = "com.byok.keys";
76
- var MODEL_PROVIDER_SECRET_NAMES = {
77
- anthropic: "model-anthropic-api-key",
78
- custom: "model-custom-api-key",
79
- deepseek: "model-deepseek-api-key",
80
- openai: "model-openai-api-key"
81
- };
82
- function modelProviderSecretName(providerId) {
83
- return MODEL_PROVIDER_SECRET_NAMES[providerId];
261
+ function modelProviderSecretName(profileRef) {
262
+ const parsed = ProviderProfileRefSchema.safeParse(profileRef);
263
+ if (!parsed.success) {
264
+ throw new ByokKeysError(
265
+ "PROVIDER_PROFILE_INVALID",
266
+ "Provider profile ref must be a lowercase portable identifier",
267
+ { cause: parsed.error }
268
+ );
269
+ }
270
+ return assertSecretName(`model-${parsed.data}-api-key`);
84
271
  }
85
272
  function decodeStrictBase64Utf8(encoded) {
86
273
  if (encoded.length % 4 !== 0) return void 0;
@@ -266,11 +453,11 @@ function assertKeychainPath(keychainPath) {
266
453
 
267
454
  // src/pi-provider-projection.ts
268
455
  var PI_PROJECTED_KEY_ENV = "PI_PROVIDER_API_KEY";
269
- function piProjectionProviderId(profileProviderId) {
270
- return `byok-sdk-${profileProviderId}`;
456
+ function piProjectionProviderId(profileRef) {
457
+ return `byok-sdk-${profileRef}`;
271
458
  }
272
459
  function buildPiProviderProjection(profile) {
273
- const projectedProviderId = piProjectionProviderId(profile.provider_id);
460
+ const projectedProviderId = piProjectionProviderId(profile.profile_ref);
274
461
  return {
275
462
  providers: {
276
463
  [projectedProviderId]: {
@@ -281,7 +468,11 @@ function buildPiProviderProjection(profile) {
281
468
  models: [
282
469
  {
283
470
  id: profile.model,
284
- name: profile.display_name
471
+ name: profile.display_name,
472
+ input: [
473
+ "text",
474
+ ...profile.capabilities.includes("image-input") ? ["image"] : []
475
+ ]
285
476
  }
286
477
  ]
287
478
  }
@@ -314,139 +505,12 @@ function buildPiProviderArgs(profile, delegatedArgs) {
314
505
  return [
315
506
  ...delegatedArgs,
316
507
  "--provider",
317
- piProjectionProviderId(profile.provider_id),
508
+ piProjectionProviderId(profile.profile_ref),
318
509
  "--model",
319
510
  profile.model
320
511
  ];
321
512
  }
322
513
 
323
- // src/url.ts
324
- function normalizeProviderUrl(value) {
325
- let url;
326
- try {
327
- url = new URL(value);
328
- } catch {
329
- throw new ByokKeysError(
330
- "PROVIDER_URL_INVALID",
331
- "Provider base URL must be absolute"
332
- );
333
- }
334
- if (url.username || url.password || url.hash || url.search || url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
335
- throw new ByokKeysError(
336
- "PROVIDER_URL_INVALID",
337
- "Provider URL requires HTTPS; HTTP is allowed only for localhost"
338
- );
339
- }
340
- if (isPrivateNetworkLiteral(url.hostname) && !isLoopbackHost(url.hostname)) {
341
- throw new ByokKeysError(
342
- "PROVIDER_URL_INVALID",
343
- "Private-network provider IPs are not allowed"
344
- );
345
- }
346
- return url.toString().replace(/\/$/u, "");
347
- }
348
- function isLoopbackHost(hostname) {
349
- const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
350
- return value === "localhost" || value === "127.0.0.1" || value === "::1";
351
- }
352
- function isPrivateNetworkLiteral(hostname) {
353
- const value = hostname.toLowerCase().replace(/^\[|\]$/gu, "");
354
- if (value.includes(":")) return true;
355
- const parts = value.split(".").map(Number);
356
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
357
- return false;
358
- }
359
- return parts[0] === 10 || parts[0] === 127 || parts[0] === 169 && parts[1] === 254 || parts[0] === 172 && (parts[1] ?? 0) >= 16 && (parts[1] ?? 0) <= 31 || parts[0] === 192 && parts[1] === 168;
360
- }
361
-
362
- // src/provider-profile.ts
363
- var MODEL_PROVIDER_IDS = [
364
- "openai",
365
- "deepseek",
366
- "anthropic",
367
- "custom"
368
- ];
369
- var PROVIDER_AUTH_MODES = ["bearer", "x_api_key", "none"];
370
- var MODEL_PROVIDER_ADAPTERS = ["openai_compatible", "anthropic"];
371
- function boundedString(field, maximumLength) {
372
- return z.string().superRefine((value, ctx) => {
373
- if (value.trim().length === 0 || value.length > maximumLength || /[\u0000\r\n]/u.test(value)) {
374
- ctx.addIssue({
375
- code: "custom",
376
- message: `Provider ${field} is invalid`
377
- });
378
- }
379
- }).transform((value) => value.trim());
380
- }
381
- function isoTimestamp(field) {
382
- return boundedString(field, 64).superRefine((value, ctx) => {
383
- if (!Number.isFinite(Date.parse(value))) {
384
- ctx.addIssue({
385
- code: "custom",
386
- message: `Provider ${field} must be an ISO timestamp`
387
- });
388
- }
389
- });
390
- }
391
- var providerBaseUrl = z.string().superRefine((value, ctx) => {
392
- try {
393
- normalizeProviderUrl(value);
394
- } catch (error) {
395
- ctx.addIssue({
396
- code: "custom",
397
- message: error instanceof Error ? error.message : "Provider base URL is invalid",
398
- params: {
399
- byokCode: error instanceof ByokKeysError ? error.code : "PROVIDER_URL_INVALID"
400
- }
401
- });
402
- }
403
- }).transform((value) => normalizeProviderUrl(value));
404
- var ModelProviderProfileSchema = z.object({
405
- adapter: z.enum(MODEL_PROVIDER_ADAPTERS),
406
- auth_mode: z.enum(PROVIDER_AUTH_MODES),
407
- base_url: providerBaseUrl,
408
- created_at: isoTimestamp("created_at"),
409
- display_name: boundedString("display_name", 100),
410
- enabled: z.boolean(),
411
- kind: z.literal("model"),
412
- model: boundedString("model", 160),
413
- provider_id: z.enum(MODEL_PROVIDER_IDS),
414
- updated_at: isoTimestamp("updated_at")
415
- }).superRefine((profile, ctx) => {
416
- if (profile.adapter === "anthropic" && profile.auth_mode !== "x_api_key") {
417
- ctx.addIssue({
418
- code: "custom",
419
- message: "Anthropic requires x_api_key authentication",
420
- path: ["auth_mode"]
421
- });
422
- }
423
- if (profile.adapter === "openai_compatible" && profile.auth_mode === "x_api_key") {
424
- ctx.addIssue({
425
- code: "custom",
426
- message: "OpenAI-compatible providers support bearer or no authentication",
427
- path: ["auth_mode"]
428
- });
429
- }
430
- if (Date.parse(profile.updated_at) < Date.parse(profile.created_at)) {
431
- ctx.addIssue({
432
- code: "custom",
433
- message: "Provider updated_at cannot precede created_at",
434
- path: ["updated_at"]
435
- });
436
- }
437
- });
438
- function parseModelProviderProfile(value) {
439
- const result = ModelProviderProfileSchema.safeParse(value);
440
- if (result.success) return result.data;
441
- const issue = result.error.issues[0];
442
- const params = issue?.params;
443
- throw new ByokKeysError(
444
- params?.byokCode ?? "PROVIDER_PROFILE_INVALID",
445
- issue?.message ?? "Provider profile is invalid",
446
- { cause: result.error }
447
- );
448
- }
449
-
450
514
  // src/pi-provider-launcher-core.ts
451
515
  var PI_CHILD_BASE_ENV_NAMES = [
452
516
  "PATH",
@@ -480,15 +544,17 @@ var PI_CHILD_WINDOWS_ENV_NAMES = [
480
544
  ];
481
545
  function parsePiProviderLauncherOptions(args) {
482
546
  const separator = args.indexOf("--");
483
- if (separator < 0) throw new Error("launcher arguments must end with -- <pi args>");
484
- const ownArgs = args.slice(0, separator);
485
- const piArgs = args.slice(separator + 1);
486
- if (piArgs.length === 0) throw new Error("launcher requires Pi arguments after --");
547
+ const ownArgs = separator < 0 ? args : args.slice(0, separator);
548
+ const piArgs = separator < 0 ? [] : args.slice(separator + 1);
487
549
  const allowedFlags = /* @__PURE__ */ new Set([
488
550
  "--pi-bin",
489
551
  "--profile-db",
490
552
  "--provider",
491
553
  "--model",
554
+ "--profile-revision",
555
+ "--profile-hash",
556
+ "--required-capabilities",
557
+ "--validate-only",
492
558
  "--session-dir",
493
559
  "--secret-service-prefix",
494
560
  "--macos-keychain-path"
@@ -512,9 +578,10 @@ function parsePiProviderLauncherOptions(args) {
512
578
  if (value === void 0) throw new Error(`${flag} requires a value`);
513
579
  return value;
514
580
  };
515
- const rawProviderId = required("--provider");
516
- if (!MODEL_PROVIDER_IDS.includes(rawProviderId)) {
517
- throw new Error(`provider ${rawProviderId} is not configured by @byok-sdk/keys`);
581
+ const rawProfileRef = required("--provider");
582
+ const profileRef = ProviderProfileRefSchema.safeParse(rawProfileRef);
583
+ if (!profileRef.success) {
584
+ throw new Error(`provider profile ref ${rawProfileRef} is not a valid @byok-sdk/keys identifier`);
518
585
  }
519
586
  const modelId = required("--model");
520
587
  if (modelId.length > 160) throw new Error("--model exceeds 160 characters");
@@ -528,11 +595,54 @@ function parsePiProviderLauncherOptions(args) {
528
595
  if (macosKeychainPath !== void 0 && !path2.posix.isAbsolute(macosKeychainPath)) {
529
596
  throw new Error("--macos-keychain-path must be an absolute path");
530
597
  }
598
+ const validateOnly = values.get("--validate-only") === "true";
599
+ if (values.has("--validate-only") && !["true", "false"].includes(values.get("--validate-only"))) {
600
+ throw new Error("--validate-only must be true or false");
601
+ }
602
+ if (!validateOnly && piArgs.length === 0) {
603
+ throw new Error("launcher requires Pi arguments after --");
604
+ }
605
+ const exactValues = [
606
+ values.get("--profile-revision"),
607
+ values.get("--profile-hash"),
608
+ values.get("--required-capabilities")
609
+ ];
610
+ if (exactValues.some((value) => value !== void 0) && exactValues.some((value) => value === void 0)) {
611
+ throw new Error("exact provider binding requires revision, hash, and required capabilities together");
612
+ }
613
+ let expectedBinding;
614
+ if (exactValues[0] !== void 0) {
615
+ if (!/^(?:0|[1-9][0-9]{0,19})$/u.test(exactValues[0])) {
616
+ throw new Error("--profile-revision must be canonical decimal");
617
+ }
618
+ if (!/^sha256:[0-9a-f]{64}$/u.test(exactValues[1])) {
619
+ throw new Error("--profile-hash must be lowercase sha256");
620
+ }
621
+ let capabilities;
622
+ try {
623
+ capabilities = JSON.parse(exactValues[2]);
624
+ } catch {
625
+ throw new Error("--required-capabilities must be a JSON array");
626
+ }
627
+ const parsedCapabilities = ProviderModelCapabilitySchema.array().max(8).safeParse(capabilities);
628
+ if (!parsedCapabilities.success || new Set(parsedCapabilities.data).size !== parsedCapabilities.data.length) {
629
+ throw new Error("--required-capabilities must contain unique supported capabilities");
630
+ }
631
+ expectedBinding = {
632
+ profileRef: profileRef.data,
633
+ profileRevision: exactValues[0],
634
+ profileHash: exactValues[1],
635
+ modelId,
636
+ requiredCapabilities: parsedCapabilities.data
637
+ };
638
+ }
531
639
  return {
532
640
  piBin: required("--pi-bin"),
533
641
  profileDbPath,
534
- providerId: rawProviderId,
642
+ profileRef: profileRef.data,
535
643
  modelId,
644
+ ...expectedBinding === void 0 ? {} : { expectedBinding },
645
+ validateOnly,
536
646
  sessionDir,
537
647
  ...secretServicePrefix ? { secretServicePrefix } : {},
538
648
  ...macosKeychainPath !== void 0 ? { macosKeychainPath } : {},
@@ -548,11 +658,11 @@ async function resolvePiProviderSecret(profile, createStore) {
548
658
  `${secrets.providerLabel} is unavailable`
549
659
  );
550
660
  }
551
- const secret = await secrets.get(modelProviderSecretName(profile.provider_id));
661
+ const secret = await secrets.get(modelProviderSecretName(profile.profile_ref));
552
662
  if (!secret) {
553
663
  throw new ByokKeysError(
554
664
  "PROVIDER_SECRET_MISSING",
555
- `${profile.provider_id} provider requires a secret in ${secrets.providerLabel}`
665
+ `${profile.profile_ref} provider profile requires a secret in ${secrets.providerLabel}`
556
666
  );
557
667
  }
558
668
  return secret;
@@ -596,10 +706,10 @@ async function ensurePiSessionDirectory(sessionDir) {
596
706
  }
597
707
 
598
708
  // src/profile-store.ts
599
- function providerNotConfigured(providerId) {
709
+ function providerNotConfigured(profileRef) {
600
710
  return new ByokKeysError(
601
711
  "PROVIDER_NOT_CONFIGURED",
602
- `${providerId} model provider is not configured`
712
+ `${profileRef} model provider is not configured`
603
713
  );
604
714
  }
605
715
  function closeSqliteDatabaseAfterInitializationFailure(database, initializationError, message, close = (handle) => handle.close()) {
@@ -668,16 +778,18 @@ function secureSqliteFilePermissions(databasePath) {
668
778
  // src/sqlite-profile-store.ts
669
779
  var SCHEMA = `
670
780
  CREATE TABLE IF NOT EXISTS provider_profile (
671
- provider_id TEXT PRIMARY KEY CHECK (provider_id IN ('openai', 'deepseek', 'anthropic', 'custom')),
672
- kind TEXT NOT NULL CHECK (kind = 'model'),
673
- adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
674
- display_name TEXT NOT NULL,
675
- base_url TEXT NOT NULL,
676
- auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
677
- model TEXT NOT NULL,
678
- enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
679
- created_at TEXT NOT NULL,
680
- updated_at TEXT NOT NULL
781
+ profile_ref TEXT PRIMARY KEY,
782
+ provider_kind TEXT NOT NULL CHECK (provider_kind IN ('openai', 'deepseek', 'anthropic', 'custom')),
783
+ kind TEXT NOT NULL CHECK (kind = 'model'),
784
+ adapter TEXT NOT NULL CHECK (adapter IN ('openai_compatible', 'anthropic')),
785
+ display_name TEXT NOT NULL,
786
+ base_url TEXT NOT NULL,
787
+ auth_mode TEXT NOT NULL CHECK (auth_mode IN ('bearer', 'x_api_key', 'none')),
788
+ model TEXT NOT NULL,
789
+ capabilities TEXT NOT NULL,
790
+ enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
791
+ created_at TEXT NOT NULL,
792
+ updated_at TEXT NOT NULL
681
793
  );
682
794
  `;
683
795
  var ENABLED_INDEX = `
@@ -717,12 +829,12 @@ var SqliteProviderProfileStore = class {
717
829
  this.#closed = true;
718
830
  this.#database.close();
719
831
  }
720
- async delete(providerId) {
721
- const result = this.#database.prepare("DELETE FROM provider_profile WHERE provider_id = ?").run(providerId);
832
+ async delete(profileRef) {
833
+ const result = this.#database.prepare("DELETE FROM provider_profile WHERE profile_ref = ?").run(profileRef);
722
834
  return Number(result.changes) === 1;
723
835
  }
724
- async get(providerId) {
725
- const row = this.#database.prepare("SELECT * FROM provider_profile WHERE provider_id = ?").get(providerId);
836
+ async get(profileRef) {
837
+ const row = this.#database.prepare("SELECT * FROM provider_profile WHERE profile_ref = ?").get(profileRef);
726
838
  return row === void 0 ? void 0 : parseRow(row);
727
839
  }
728
840
  async getEnabled() {
@@ -730,11 +842,11 @@ var SqliteProviderProfileStore = class {
730
842
  return row === void 0 ? void 0 : parseRow(row);
731
843
  }
732
844
  async list() {
733
- const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY provider_id ASC").all();
845
+ const rows = this.#database.prepare("SELECT * FROM provider_profile ORDER BY profile_ref ASC").all();
734
846
  return rows.map(parseRow);
735
847
  }
736
848
  async save(profile) {
737
- const existing = await this.get(profile.provider_id);
849
+ const existing = await this.get(profile.profile_ref);
738
850
  const validated = parseModelProviderProfile({
739
851
  ...profile,
740
852
  created_at: existing?.created_at ?? profile.created_at
@@ -742,40 +854,44 @@ var SqliteProviderProfileStore = class {
742
854
  this.#transaction(() => {
743
855
  if (validated.enabled) {
744
856
  this.#database.prepare(
745
- "UPDATE provider_profile SET enabled = 0 WHERE provider_id <> ?"
746
- ).run(validated.provider_id);
857
+ "UPDATE provider_profile SET enabled = 0 WHERE profile_ref <> ?"
858
+ ).run(validated.profile_ref);
747
859
  }
748
860
  this.#database.prepare(
749
861
  `INSERT INTO provider_profile (
750
- provider_id, kind, adapter, display_name, base_url,
751
- auth_mode, model, enabled, created_at, updated_at
752
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
753
- ON CONFLICT(provider_id) DO UPDATE SET
862
+ profile_ref, provider_kind, kind, adapter, display_name, base_url,
863
+ auth_mode, model, capabilities, enabled, created_at, updated_at
864
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
865
+ ON CONFLICT(profile_ref) DO UPDATE SET
866
+ provider_kind = excluded.provider_kind,
754
867
  adapter = excluded.adapter,
755
868
  display_name = excluded.display_name,
756
869
  base_url = excluded.base_url,
757
870
  auth_mode = excluded.auth_mode,
758
871
  model = excluded.model,
872
+ capabilities = excluded.capabilities,
759
873
  enabled = excluded.enabled,
760
874
  updated_at = excluded.updated_at`
761
875
  ).run(
762
- validated.provider_id,
876
+ validated.profile_ref,
877
+ validated.provider_kind,
763
878
  validated.kind,
764
879
  validated.adapter,
765
880
  validated.display_name,
766
881
  validated.base_url,
767
882
  validated.auth_mode,
768
883
  validated.model,
884
+ JSON.stringify(validated.capabilities),
769
885
  validated.enabled ? 1 : 0,
770
886
  validated.created_at,
771
887
  validated.updated_at
772
888
  );
773
889
  });
774
- return await this.get(validated.provider_id);
890
+ return await this.get(validated.profile_ref);
775
891
  }
776
- async setEnabled(providerId) {
777
- const existing = await this.get(providerId);
778
- if (existing === void 0) throw providerNotConfigured(providerId);
892
+ async setEnabled(profileRef) {
893
+ const existing = await this.get(profileRef);
894
+ if (existing === void 0) throw providerNotConfigured(profileRef);
779
895
  return this.save({ ...existing, enabled: true });
780
896
  }
781
897
  /** `BEGIN IMMEDIATE` / `COMMIT` / `ROLLBACK`, per `providers.ts:1252-1263`. */
@@ -791,8 +907,19 @@ var SqliteProviderProfileStore = class {
791
907
  }
792
908
  };
793
909
  function parseRow(row) {
910
+ let capabilities;
911
+ try {
912
+ capabilities = JSON.parse(row.capabilities);
913
+ } catch (cause) {
914
+ throw new ByokKeysError(
915
+ "PROVIDER_PROFILE_INVALID",
916
+ "Provider profile capabilities column is not valid JSON",
917
+ { cause }
918
+ );
919
+ }
794
920
  return parseModelProviderProfile({
795
921
  ...row,
922
+ capabilities,
796
923
  enabled: row.enabled === 1
797
924
  });
798
925
  }
@@ -1100,15 +1227,19 @@ async function run(options) {
1100
1227
  });
1101
1228
  let projectionDir;
1102
1229
  try {
1103
- const profile = await profiles.get(options.providerId);
1230
+ const profile = await profiles.get(options.profileRef);
1104
1231
  if (profile === void 0) {
1105
- throw new Error(`provider ${options.providerId} is not configured`);
1232
+ throw new Error(`provider profile ${options.profileRef} is not configured`);
1106
1233
  }
1107
1234
  if (profile.model !== options.modelId) {
1108
1235
  throw new Error(
1109
1236
  `selected model ${options.modelId} does not match configured provider model ${profile.model}`
1110
1237
  );
1111
1238
  }
1239
+ if (options.expectedBinding !== void 0) {
1240
+ assertExactProviderProfileBinding(profile, options.expectedBinding);
1241
+ }
1242
+ if (options.validateOnly) return 0;
1112
1243
  const secret = await resolvePiProviderSecret(
1113
1244
  profile,
1114
1245
  () => createSecretStore(