@bivy/bivy 0.10.1-staging.182 → 0.10.1-staging.183

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.
@@ -13,7 +13,7 @@
13
13
  // `src/credentials/`. Later phases move the implementations under this directory
14
14
  // and wire selection into the live path. See docs/credentials-service-plan.md.
15
15
  // --- The record model + selection (new, pure) ------------------------------
16
- export { DEFAULT_LABEL, normalizeProvider, normalizeLabel, credKey, parseCredKey, agentNativeLabel, defaultSyncFor, resolveCredential, missingPresetLabels, } from "./records.js";
16
+ export { DEFAULT_LABEL, normalizeProvider, normalizeLabel, credKey, parseCredKey, agentNativeLabel, defaultSyncFor, inferReferenceBackend, resolveCredential, missingPresetLabels, } from "./records.js";
17
17
  // --- The v3 document engine: schema, migration, merge (new, pure) -----------
18
18
  export { emptyDocument, recordFromStored, preferIncomingRecord, tombstoneWinsRecord, migrateToV3, mergeDocuments, } from "./document.js";
19
19
  // --- Selection presets (config-as-code) -------------------------------------
@@ -25,4 +25,4 @@ export { createCredentialStore, buildAgentCredentialEnv, apiKeyEnvVar, NodeCrede
25
25
  // --- The daemon credential API (ours — currently named pi-auth.ts) ----------
26
26
  // Phase 5 renames the file to `src/credentials/api.ts`; the facade keeps the
27
27
  // import path stable so call sites don't churn when it moves.
28
- export { listProviders, exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, setProviderApiKey, setProviderCredential, removeProvider, } from "../runtime/pi-auth.js";
28
+ export { listProviders, exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, setProviderApiKey, setProviderCredential, setProviderReference, removeProvider, } from "../runtime/pi-auth.js";
@@ -57,6 +57,20 @@ export function defaultSyncFor(origin) {
57
57
  // logins stay on the node they were made on until explicitly promoted.
58
58
  return origin === "bivy" ? "account" : "node";
59
59
  }
60
+ /**
61
+ * The reference backend a pointer targets, inferred from its scheme:
62
+ * `op://…` → 1Password, `env://NAME` → an environment variable. Anything else
63
+ * (a bare value, a `secret://` local-vault ref, junk) returns undefined — a
64
+ * reference credential must point at an external, per-node-resolvable secret.
65
+ */
66
+ export function inferReferenceBackend(ref) {
67
+ const value = String(ref ?? "").trim();
68
+ if (value.startsWith("op://"))
69
+ return "1password";
70
+ if (value.startsWith("env://"))
71
+ return "env";
72
+ return undefined;
73
+ }
60
74
  function recordsForProvider(records, provider) {
61
75
  const id = normalizeProvider(provider);
62
76
  return records.filter((r) => r.provider === id);
@@ -21,7 +21,7 @@ import path from "node:path";
21
21
  import { randomBytes } from "node:crypto";
22
22
  import { seal, open } from "../e2e.js";
23
23
  import { migrateToV3, mergeDocuments, recordFromStored, emptyDocument, } from "../credentials/document.js";
24
- import { credKey, parseCredKey, DEFAULT_LABEL } from "../credentials/records.js";
24
+ import { credKey, parseCredKey, normalizeLabel, DEFAULT_LABEL } from "../credentials/records.js";
25
25
  const LOCK_STALE_MS = 30_000;
26
26
  const LOCK_RETRY_MS = 25;
27
27
  const LOCK_TIMEOUT_MS = 10_000;
@@ -221,6 +221,45 @@ export class BivyCredentialStore {
221
221
  throw new Error("API key cannot be empty");
222
222
  await this.modify(provider, async () => ({ type: "api_key", key: apiKey }));
223
223
  }
224
+ /**
225
+ * Store a reference credential — a pointer (`op://…` / `env://NAME`) resolved
226
+ * per-node at read time (see the resolver), never the secret itself. The
227
+ * pointer is safe to sync across nodes; the secret stays in the manager. A
228
+ * reference is api-key-shaped, so it cannot model a rotating OAuth token set.
229
+ * Writes at `provider:label` (defaulting to the provider's default slot).
230
+ */
231
+ async setReference(provider, ref, backend, label = DEFAULT_LABEL) {
232
+ const id = providerId(provider);
233
+ if (!id)
234
+ throw new Error("Provider is required");
235
+ const pointer = String(ref ?? "").trim();
236
+ if (!pointer)
237
+ throw new Error("Reference cannot be empty");
238
+ const key = credKey(id, label);
239
+ await this.enqueue(id, async () => {
240
+ await this.acquireLock();
241
+ try {
242
+ const document = this.readDocument();
243
+ const existing = document.credentials[key];
244
+ const record = {
245
+ provider: id,
246
+ label: normalizeLabel(label),
247
+ source: { kind: "reference", ref: pointer, backend },
248
+ // Preserve an existing record's sync/origin; a new reference is a
249
+ // Bivy-first, opt-out-sync credential (only the pointer ever syncs).
250
+ sync: existing?.sync ?? "account",
251
+ origin: existing?.origin ?? "bivy",
252
+ updatedAt: Date.now(),
253
+ };
254
+ document.credentials[key] = record;
255
+ delete document.deletedAt[key];
256
+ this.writeDocument(document);
257
+ }
258
+ finally {
259
+ await this.releaseLock();
260
+ }
261
+ });
262
+ }
224
263
  /**
225
264
  * Every stored credential, keyed by provider id — the cross-node snapshot.
226
265
  * The wire stays provider-keyed (v2 shape), so only `provider:default` records
@@ -10,8 +10,10 @@
10
10
  //
11
11
  // This keeps Bivy's hot credential path decoupled from Pi: Pi is just another
12
12
  // agent that reads the same store.
13
+ import path from "node:path";
13
14
  import { createCredentialVault } from "./credential-store.js";
14
15
  import { refreshModelOAuth } from "./oauth/model-oauth.js";
16
+ import { resolveSecret } from "../secrets.js";
15
17
  import { resolveCredential } from "../credentials/records.js";
16
18
  import { loadPresets, defaultPresetsPath } from "../credentials/presets.js";
17
19
  /** Refresh an OAuth token this many ms before it expires (clock-skew guard). */
@@ -22,10 +24,14 @@ export class NodeCredentialResolver {
22
24
  store;
23
25
  presetsPath;
24
26
  presetsCache;
27
+ appDir;
25
28
  constructor(credsDir) {
26
29
  this.credsDir = credsDir;
27
30
  this.store = createCredentialVault(credsDir);
28
31
  this.presetsPath = defaultPresetsPath(credsDir);
32
+ // The data dir that holds the local secret vault, for resolving `secret://`
33
+ // references (`op://`/`env://` need no dir). credsDir is `<appDir>/credentials`.
34
+ this.appDir = path.dirname(credsDir);
29
35
  }
30
36
  /** The node's selection presets, read once per resolver (see presets.ts). */
31
37
  presets() {
@@ -40,14 +46,22 @@ export class NodeCredentialResolver {
40
46
  // Selection is data-driven: pick the record for this provider per the active
41
47
  // preset (records.ts). With one credential per provider this resolves to that
42
48
  // credential; ambiguity (multiple accounts, no preset) returns nothing rather
43
- // than guessing. Reference records are resolved in a later phase.
49
+ // than guessing.
44
50
  const records = await this.store.listRecords().catch(() => []);
45
51
  const selection = resolveCredential(id, records, this.presets());
46
52
  if (!selection)
47
53
  return undefined;
48
54
  const source = selection.record.source;
49
- if (source.kind !== "stored")
50
- return undefined;
55
+ // A reference credential is a pointer (op:// / env://) resolved per-node at
56
+ // read time via the secret vault — the secret never lived in our store. It is
57
+ // api-key-shaped. A node that can't resolve it (no `op` session, missing env)
58
+ // reports no credential here rather than falling back to another account.
59
+ if (source.kind === "reference") {
60
+ const token = (await resolveSecret(source.ref, this.appDir).catch(() => undefined))?.trim();
61
+ if (!token)
62
+ return undefined;
63
+ return { provider: id, kind: "api_key", token };
64
+ }
51
65
  const cred = source.cred;
52
66
  if (cred.type === "api_key") {
53
67
  const token = typeof cred.key === "string" ? cred.key : "";
@@ -9,6 +9,7 @@
9
9
  // pi-oauth bridge supplies (Bivy has no model catalog of its own).
10
10
  import { createCredentialVault } from "./credential-store.js";
11
11
  import { listPiProviders } from "./pi-oauth.js";
12
+ import { inferReferenceBackend } from "../credentials/records.js";
12
13
  /**
13
14
  * Enumerate model providers with their current auth status. The catalog + status
14
15
  * come from Pi's provider list (joined with our injected store); the stored
@@ -77,6 +78,25 @@ export async function setProviderCredential(credsDir, provider, opts) {
77
78
  return cred;
78
79
  });
79
80
  }
81
+ /**
82
+ * Store a reference credential for a model provider: a pointer (`op://…` /
83
+ * `env://NAME`) that Bivy resolves per-node at read time, so the secret stays in
84
+ * the password manager / environment and never enters the vault. The backend is
85
+ * inferred from the pointer's scheme. This is the recommended way to use a
86
+ * password-manager-held model key — it supersedes wiring `ANTHROPIC_API_KEY:
87
+ * op://…` through `cli.json`, because a reference is labeled, selectable, and
88
+ * visible in the Models UI like any other credential.
89
+ */
90
+ export async function setProviderReference(credsDir, provider, ref) {
91
+ const id = provider.trim().toLowerCase();
92
+ if (!id)
93
+ throw new Error("Provider is required");
94
+ const pointer = String(ref ?? "").trim();
95
+ const backend = inferReferenceBackend(pointer);
96
+ if (!backend)
97
+ throw new Error("Reference must be an op:// or env:// pointer");
98
+ await createCredentialVault(credsDir).setReference(id, pointer, backend);
99
+ }
80
100
  /** Forget a provider's stored credential (API key or OAuth token). */
81
101
  export async function removeProvider(credsDir, provider) {
82
102
  const id = provider.trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.182",
3
+ "version": "0.10.1-staging.183",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",