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

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.
@@ -16,8 +16,8 @@
16
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
- // --- Selection presets (config-as-code) -------------------------------------
20
- export { parsePresets, loadPresets, defaultPresetsPath, PRESETS_FILENAME } from "./presets.js";
19
+ // --- Selection presets + ingest policy (config-as-code) ---------------------
20
+ export { parsePresets, loadPresets, defaultPresetsPath, PRESETS_FILENAME, parseIngestPolicy, loadIngestPolicy, } from "./presets.js";
21
21
  // --- The vault (source of truth) — re-exported from its current home --------
22
22
  export { createCredentialVault, migrateVaultDir, preferIncomingCredential, tombstoneWins, BivyCredentialStore, } from "../runtime/credential-store.js";
23
23
  // --- The node resolver + agent env representation ---------------------------
@@ -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, setProviderReference, removeProvider, } from "../runtime/pi-auth.js";
28
+ export { listProviders, exportProviderAuth, exportProviderAuthTombstones, importProviderAuth, setProviderApiKey, setProviderCredential, setProviderReference, removeProvider, listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, } from "../runtime/pi-auth.js";
@@ -73,3 +73,23 @@ export function loadPresets(filePath) {
73
73
  return {};
74
74
  }
75
75
  }
76
+ /** Read `ingest.policy` from a parsed config; anything unrecognized → `merge`. */
77
+ export function parseIngestPolicy(raw) {
78
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
79
+ const ingest = raw.ingest;
80
+ if (ingest && typeof ingest === "object" && !Array.isArray(ingest)) {
81
+ if (ingest.policy === "separate")
82
+ return "separate";
83
+ }
84
+ }
85
+ return "merge";
86
+ }
87
+ /** Load the ingest policy from the config file; missing/malformed → `merge`. */
88
+ export function loadIngestPolicy(filePath) {
89
+ try {
90
+ return parseIngestPolicy(JSON.parse(fs.readFileSync(filePath, "utf8")));
91
+ }
92
+ catch {
93
+ return "merge";
94
+ }
95
+ }
@@ -13,6 +13,8 @@
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
15
  import { createCredentialVault } from "./credential-store.js";
16
+ import { agentNativeLabel } from "../credentials/records.js";
17
+ import { loadIngestPolicy, defaultPresetsPath } from "../credentials/presets.js";
16
18
  import { resolveCodexHome } from "./codex-auth.js";
17
19
  import { grokAuthEntryKey, resolveGrokHome, GROK_OIDC_ISSUER } from "./grok-auth.js";
18
20
  import { claudeCredentialFiles } from "./anthropic-preflight.js";
@@ -30,6 +32,32 @@ function jwtExpiryMs(token) {
30
32
  return undefined;
31
33
  }
32
34
  }
35
+ /**
36
+ * Fold one mapped agent-native login into the vault per the node's ingest policy
37
+ * (`credentials.config.json` → `ingest.policy`; default `merge`):
38
+ * - `merge`: merge into the provider's `default` slot (historical behavior) — a
39
+ * native login updates the provider's synced Bivy credential, rotation-safe.
40
+ * - `separate`: land as a distinct, node-local credential under a reserved
41
+ * agent-derived label (with the account id when the login carries one), so it
42
+ * never overwrites a Bivy key and is selectable via a preset.
43
+ */
44
+ async function foldIngested(credsDir, agent, mapped) {
45
+ const store = createCredentialVault(credsDir);
46
+ const policy = loadIngestPolicy(defaultPresetsPath(credsDir));
47
+ if (policy !== "separate") {
48
+ return store.importAll({ [mapped.providerId]: mapped.credential });
49
+ }
50
+ const rawAccount = mapped.credential.accountId;
51
+ const accountId = typeof rawAccount === "string" ? rawAccount : undefined;
52
+ const record = {
53
+ provider: mapped.providerId,
54
+ label: agentNativeLabel(agent, accountId),
55
+ source: { kind: "stored", cred: mapped.credential },
56
+ origin: "agent-native",
57
+ sync: "node",
58
+ };
59
+ return store.importRecords([record]);
60
+ }
33
61
  /** Map Codex's `~/.codex/auth.json` into a Bivy credential (provider id → credential). */
34
62
  export function codexAuthToCredential(raw) {
35
63
  if (!raw || typeof raw !== "object")
@@ -64,7 +92,7 @@ export async function ingestCodexCredentials(credsDir) {
64
92
  const mapped = codexAuthToCredential(raw);
65
93
  if (!mapped)
66
94
  return 0;
67
- return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
95
+ return foldIngested(credsDir, "codex", mapped);
68
96
  }
69
97
  /** Map the Claude CLI's `.credentials.json` (`claudeAiOauth`) into a Bivy credential. */
70
98
  export function claudeAuthToCredential(raw) {
@@ -91,7 +119,7 @@ export async function ingestClaudeCredentials(credsDir) {
91
119
  }
92
120
  const mapped = claudeAuthToCredential(raw);
93
121
  if (mapped)
94
- return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
122
+ return foldIngested(credsDir, "claude", mapped);
95
123
  }
96
124
  return 0;
97
125
  }
@@ -139,7 +167,7 @@ export async function ingestGrokCredentials(credsDir) {
139
167
  const mapped = grokAuthToCredential(raw);
140
168
  if (!mapped)
141
169
  return 0;
142
- return createCredentialVault(credsDir).importAll({ [mapped.providerId]: mapped.credential });
170
+ return foldIngested(credsDir, "grok", mapped);
143
171
  }
144
172
  /**
145
173
  * Ingest an agent's native credential store into Bivy's vault. Called when a
@@ -299,6 +299,84 @@ export class BivyCredentialStore {
299
299
  async listRecords() {
300
300
  return Object.values(this.readDocument().credentials);
301
301
  }
302
+ /** Read one credential record by its `provider:label` identity. */
303
+ async readRecord(provider, label = DEFAULT_LABEL) {
304
+ const id = providerId(provider);
305
+ if (!id)
306
+ return undefined;
307
+ return this.readDocument().credentials[credKey(id, label)];
308
+ }
309
+ /**
310
+ * Authoritatively upsert a record at its `provider:label` — the record-addressed
311
+ * write behind the multi-credential API (add/label a specific account). Stamps
312
+ * the store-owned `updatedAt` and clears any tombstone for that slot. Use
313
+ * `importRecords()` (merge) for ingest/sync, where a fresher local login must win.
314
+ */
315
+ async putRecord(record) {
316
+ const id = providerId(record.provider);
317
+ if (!id)
318
+ throw new Error("Provider is required");
319
+ const label = normalizeLabel(record.label);
320
+ const key = credKey(id, label);
321
+ await this.enqueue(id, async () => {
322
+ await this.acquireLock();
323
+ try {
324
+ const document = this.readDocument();
325
+ document.credentials[key] = { ...record, provider: id, label, updatedAt: Date.now() };
326
+ delete document.deletedAt[key];
327
+ this.writeDocument(document);
328
+ }
329
+ finally {
330
+ await this.releaseLock();
331
+ }
332
+ });
333
+ }
334
+ /** Forget one record by `provider:label`, leaving a tombstone for convergence. */
335
+ async deleteRecord(provider, label = DEFAULT_LABEL) {
336
+ const id = providerId(provider);
337
+ if (!id)
338
+ return;
339
+ const key = credKey(id, label);
340
+ await this.enqueue(id, async () => {
341
+ await this.acquireLock();
342
+ try {
343
+ const document = this.readDocument();
344
+ const had = key in document.credentials;
345
+ delete document.credentials[key];
346
+ const deletedAt = Date.now();
347
+ if (!had && (document.deletedAt[key] ?? 0) >= deletedAt)
348
+ return;
349
+ document.deletedAt[key] = deletedAt;
350
+ this.writeDocument(document);
351
+ }
352
+ finally {
353
+ await this.releaseLock();
354
+ }
355
+ });
356
+ }
357
+ /**
358
+ * Merge record snapshots into the vault, record-addressed and non-destructive
359
+ * (freshest-OAuth-wins, rotation-safe, tombstone-newer-wins — see document.ts).
360
+ * This is the record-level counterpart to `importAll`, for agent-native ingest
361
+ * under reserved labels and (later) record-shaped cross-node sync. Returns the
362
+ * number of newly-added records.
363
+ */
364
+ async importRecords(records, deletedAt = {}) {
365
+ await this.acquireLock();
366
+ try {
367
+ const document = this.readDocument();
368
+ const incoming = {};
369
+ for (const record of records)
370
+ incoming[credKey(record.provider, record.label)] = record;
371
+ const result = mergeDocuments(document, incoming, deletedAt);
372
+ if (result.changed)
373
+ this.writeDocument(result.document);
374
+ return result.imported;
375
+ }
376
+ finally {
377
+ await this.releaseLock();
378
+ }
379
+ }
302
380
  /** The plaintext `auth.json` path an agent's own CLI/TUI reads (`<plaintextDir>/auth.json`). */
303
381
  get legacyAuthPath() {
304
382
  return this.legacyFile;
@@ -9,7 +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
+ import { inferReferenceBackend, normalizeLabel, defaultSyncFor, DEFAULT_LABEL, } from "../credentials/records.js";
13
13
  /**
14
14
  * Enumerate model providers with their current auth status. The catalog + status
15
15
  * come from Pi's provider list (joined with our injected store); the stored
@@ -104,3 +104,67 @@ export async function removeProvider(credsDir, provider) {
104
104
  throw new Error("Provider is required");
105
105
  await createCredentialVault(credsDir).delete(id);
106
106
  }
107
+ /** Every stored credential as a non-secret summary (never exposes key material). */
108
+ export async function listCredentialRecords(credsDir) {
109
+ const records = await createCredentialVault(credsDir).listRecords();
110
+ return records.map((record) => {
111
+ const source = record.source;
112
+ if (source.kind === "reference") {
113
+ return { provider: record.provider, label: record.label, kind: "reference", sync: record.sync, origin: record.origin, ref: source.ref };
114
+ }
115
+ const cred = source.cred;
116
+ const summary = { provider: record.provider, label: record.label, kind: cred.type, sync: record.sync, origin: record.origin };
117
+ if (cred.type === "oauth")
118
+ summary.expiresAt = cred.expires;
119
+ return summary;
120
+ });
121
+ }
122
+ /**
123
+ * Preserve an existing record's sync/origin when re-setting it (editing the key of
124
+ * a credential the user opted node-local must not silently re-enable sync), else
125
+ * default to a Bivy-first, opt-out-sync credential.
126
+ */
127
+ async function labeledMeta(credsDir, provider, label) {
128
+ const existing = await createCredentialVault(credsDir).readRecord(provider, label);
129
+ return { sync: existing?.sync ?? defaultSyncFor("bivy"), origin: existing?.origin ?? "bivy" };
130
+ }
131
+ /** Store an API key under a specific label (multi-account). */
132
+ export async function setProviderApiKeyLabeled(credsDir, provider, label, key) {
133
+ const id = provider.trim().toLowerCase();
134
+ if (!id)
135
+ throw new Error("Provider is required");
136
+ const apiKey = String(key ?? "").trim();
137
+ if (!apiKey)
138
+ throw new Error("API key cannot be empty");
139
+ const meta = await labeledMeta(credsDir, id, label);
140
+ await createCredentialVault(credsDir).putRecord({
141
+ provider: id,
142
+ label: normalizeLabel(label),
143
+ source: { kind: "stored", cred: { type: "api_key", key: apiKey } },
144
+ ...meta,
145
+ });
146
+ }
147
+ /** Store a reference (op:// / env://) under a specific label (multi-account). */
148
+ export async function setProviderReferenceLabeled(credsDir, provider, label, ref) {
149
+ const id = provider.trim().toLowerCase();
150
+ if (!id)
151
+ throw new Error("Provider is required");
152
+ const pointer = String(ref ?? "").trim();
153
+ const backend = inferReferenceBackend(pointer);
154
+ if (!backend)
155
+ throw new Error("Reference must be an op:// or env:// pointer");
156
+ const meta = await labeledMeta(credsDir, id, label);
157
+ await createCredentialVault(credsDir).putRecord({
158
+ provider: id,
159
+ label: normalizeLabel(label),
160
+ source: { kind: "reference", ref: pointer, backend },
161
+ ...meta,
162
+ });
163
+ }
164
+ /** Forget a single labeled credential (`provider:label`). */
165
+ export async function removeProviderCredential(credsDir, provider, label = DEFAULT_LABEL) {
166
+ const id = provider.trim().toLowerCase();
167
+ if (!id)
168
+ throw new Error("Provider is required");
169
+ await createCredentialVault(credsDir).deleteRecord(id, label);
170
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.10.1-staging.183",
3
+ "version": "0.10.1-staging.185",
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.",