@bivy/bivy 0.16.21-staging.2 → 0.16.21-staging.4

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/bin/bivy.mjs CHANGED
@@ -5054,6 +5054,7 @@ ${c.bold("bivy")} — Bivy node CLI
5054
5054
  ${c.cyan("bivy login")} Sign this machine into a Bivy account (GitHub or email)
5055
5055
  ${c.cyan("bivy logout")} Sign this machine out (alias: signout)
5056
5056
  ${c.cyan("bivy provider login")} Sign into a model provider (alias: model login)
5057
+ ${c.cyan("bivy auth import")} Import local Claude, Codex, or Grok logins into the vault
5057
5058
  ${c.cyan("bivy update")} Update Bivy + install deps + restart service (waits for active sessions to finish a turn; --force to skip)
5058
5059
  ${c.cyan("bivy update:log")} Show output of the last (or in-progress) update
5059
5060
  ${c.cyan("bivy agent add")} Connect an existing user-owned agent
@@ -5200,6 +5201,7 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5200
5201
  process.exit(await run(nodeBin, [...nodeScriptArgs(configEntry), ...args], { cwd: process.cwd(), env: process.env }));
5201
5202
  break;
5202
5203
  }
5204
+ case "auth":
5203
5205
  case "credentials":
5204
5206
  case "creds": {
5205
5207
  if (!(await ensureDeps())) process.exit(1);
@@ -9,10 +9,41 @@
9
9
  // the handful of daemon bits (credsDir, event emit, and the post-auth refresh /
10
10
  // provider-list helpers) as composition deps.
11
11
  import { testProviderCredential } from "../runtime/credentials.js";
12
+ import { NativeAuthPreviewService } from "../runtime/native-auth-preview.js";
12
13
  import { exportAccountApiKeys, exportAccountOAuthCredentials, importAccountOAuthCredentials, exportRecordTombstones, listCredentialRecords, removeProviderCredential, setProviderApiKeyLabeled, setProviderReferenceLabeled, setCredentialSync, setCredentialUnattended, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, } from "../credentials/api.js";
13
14
  export function createCredentialCommands(deps) {
14
15
  const { credsDir } = deps;
16
+ const nativeAuth = new NativeAuthPreviewService(credsDir);
15
17
  return {
18
+ async "credentials.native.preview"(msg, ctx) {
19
+ try {
20
+ ctx.reply({ type: "credentials.native.preview.ok", requestId: msg.requestId, ...await nativeAuth.preview(msg.label) });
21
+ }
22
+ catch {
23
+ ctx.reply({ type: "credentials.native.preview.error", requestId: msg.requestId, httpStatus: 400, error: "Could not scan native logins. Check the label and machine permissions, then retry." });
24
+ }
25
+ },
26
+ async "credentials.native.import"(msg, ctx) {
27
+ try {
28
+ const result = await nativeAuth.import(msg.previewId, msg.agents, msg.sync);
29
+ if (result.items.some((item) => item.status === "imported")) {
30
+ try {
31
+ await deps.pushModelAuthToControlPlane();
32
+ await deps.refreshSessionAfterAuth();
33
+ deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
34
+ deps.broadcast({ type: "providers.list", providers: await deps.listProvidersUnified() });
35
+ }
36
+ catch {
37
+ result.warning = "Saved on this machine, but sync or runtime refresh could not be confirmed. Check machine connectivity.";
38
+ }
39
+ }
40
+ ctx.reply({ type: "credentials.native.import.ok", requestId: msg.requestId, ...result });
41
+ }
42
+ catch {
43
+ // Filesystem/parse errors may include secrets; never return raw errors.
44
+ ctx.reply({ type: "credentials.native.import.error", requestId: msg.requestId, httpStatus: 400, error: "Import could not complete. Scan again and check the results before retrying; existing credentials will not be replaced." });
45
+ }
46
+ },
16
47
  async "credentials.list"() {
17
48
  deps.sendEvent({ type: "credentials.records", records: await listCredentialRecords(credsDir) });
18
49
  },
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { createInterface } from "node:readline/promises";
4
+ import { stdin, stdout } from "node:process";
5
+ import { createCredentialVault } from "./runtime/credential-store.js";
6
+ import { discoverNativeAuth, nativeAuthSources } from "./runtime/native-auth-import.js";
7
+ import { normalizeLabel } from "./credentials/records.js";
8
+ export async function importNativeAuthCommand(credsDir, args) {
9
+ let sync;
10
+ let yes = false;
11
+ let dryRun = false;
12
+ let label = "default";
13
+ const agents = [];
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+ if (arg === "--yes")
17
+ yes = true;
18
+ else if (arg === "--dry-run")
19
+ dryRun = true;
20
+ else if (arg === "--sync") {
21
+ const value = args[++i];
22
+ if (value !== "node" && value !== "account")
23
+ throw new Error("--sync requires node or account");
24
+ sync = value;
25
+ }
26
+ else if (arg === "--label") {
27
+ const value = args[++i];
28
+ if (!value?.trim() || value.startsWith("--"))
29
+ throw new Error("--label requires a name");
30
+ label = normalizeLabel(value);
31
+ }
32
+ else if (Object.hasOwn(nativeAuthSources, arg))
33
+ agents.push(arg);
34
+ else
35
+ throw new Error("Usage: bivy auth import [claude codex grok] [--sync node|account] [--label name] [--yes] [--dry-run]");
36
+ }
37
+ const selected = [...new Set(agents.length ? agents : Object.keys(nativeAuthSources))];
38
+ const found = selected.map(discoverNativeAuth);
39
+ const vault = createCredentialVault(credsDir);
40
+ const candidates = [];
41
+ for (const item of found) {
42
+ if (item.status !== "found") {
43
+ console.log(`${item.agent}: ${item.status} credential file (keychain-only logins are not supported).`);
44
+ continue;
45
+ }
46
+ const existing = await vault.readRecord(item.provider, label);
47
+ console.log(`${item.agent}: ${item.provider}:${label} (${item.credential.type === "oauth" ? "OAuth" : "API key"})${existing ? " — skipped: slot already exists; use --label to keep both" : " — ready to import"}`);
48
+ if (!existing)
49
+ candidates.push(item);
50
+ }
51
+ console.log("Subscription logins work only with compatible providers/runtimes; access has not been verified.");
52
+ console.log("Native logins are left unchanged. Concurrent OAuth refresh by native CLIs or other machines may require re-login.");
53
+ if (dryRun || !candidates.length)
54
+ return;
55
+ if (!stdin.isTTY && (!yes || !sync))
56
+ throw new Error("Non-interactive import requires --yes and --sync node|account. Use --dry-run to preview.");
57
+ if (yes && !sync)
58
+ throw new Error("--yes requires an explicit --sync node|account.");
59
+ if (!yes) {
60
+ const rl = createInterface({ input: stdin, output: stdout });
61
+ try {
62
+ if (!sync) {
63
+ const answer = (await rl.question("Store on this node, or E2E sync across your account nodes? [node/account] (node): ")).trim().toLowerCase();
64
+ if (answer && answer !== "node" && answer !== "account")
65
+ throw new Error("Expected node or account; nothing imported.");
66
+ sync = answer === "account" ? "account" : "node";
67
+ }
68
+ const answer = await rl.question(`Import ${candidates.length} credential(s), scope ${sync}? [y/N] `);
69
+ if (!/^y(es)?$/i.test(answer.trim())) {
70
+ console.log("Cancelled; nothing imported.");
71
+ return;
72
+ }
73
+ }
74
+ finally {
75
+ rl.close();
76
+ }
77
+ }
78
+ for (const item of candidates) {
79
+ const inserted = await vault.putRecordIfAbsent({
80
+ provider: item.provider, label, origin: "agent-native", sync: sync,
81
+ source: { kind: "stored", cred: item.credential },
82
+ });
83
+ console.log(`${item.provider}:${label}: ${inserted ? `imported (${sync})` : "skipped: slot changed during import"}.`);
84
+ }
85
+ console.log(sync === "account"
86
+ ? "Eligible for E2E account sync when the enrolled Bivy daemon is running; delivery is not yet confirmed."
87
+ : "Stored locally. Use 'bivy credentials sync <provider> <label> account' to enable account sync later.");
88
+ if (label !== "default")
89
+ console.log("Select this label with 'bivy credentials preset set <preset> <provider> <label>' and 'bivy credentials preset use <preset>'.");
90
+ console.log("Agent-managed auth is unchanged; compatible runtimes must use Bivy-managed auth to consume the vault.");
91
+ }
@@ -351,18 +351,28 @@ export class BivyCredentialStore {
351
351
  * `importRecords()` (merge) for ingest/sync, where a fresher local login must win.
352
352
  */
353
353
  async putRecord(record) {
354
+ await this.writeRecord(record, false);
355
+ }
356
+ /** Import only into an empty slot; the existence check shares the vault lock. */
357
+ async putRecordIfAbsent(record) {
358
+ return this.writeRecord(record, true);
359
+ }
360
+ async writeRecord(record, ifAbsent) {
354
361
  const id = providerId(record.provider);
355
362
  if (!id)
356
363
  throw new Error("Provider is required");
357
364
  const label = normalizeLabel(record.label);
358
365
  const key = credKey(id, label);
359
- await this.enqueue(id, async () => {
366
+ return this.enqueue(id, async () => {
360
367
  await this.acquireLock();
361
368
  try {
362
369
  const document = this.readDocument();
370
+ if (ifAbsent && document.credentials[key])
371
+ return false;
363
372
  document.credentials[key] = { ...record, provider: id, label, updatedAt: Date.now() };
364
373
  delete document.deletedAt[key];
365
374
  this.writeDocument(document);
375
+ return true;
366
376
  }
367
377
  finally {
368
378
  await this.releaseLock();
@@ -13,6 +13,7 @@ import { stdin as input, stdout as output } from "node:process";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { listCredentialRecords, setProviderApiKeyLabeled, setProviderReferenceLabeled, removeProviderCredential, setCredentialSync, getCredentialPresets, setActiveCredentialPreset, setCredentialPresetMapping, getCredentialIngestPolicy, setCredentialIngestPolicy, } from "./credentials/api.js";
15
15
  import { defaultPresetsPath, inferReferenceBackend } from "./credentials/index.js";
16
+ import { importNativeAuthCommand } from "./credential-import-command.js";
16
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
18
  const repoRoot = path.resolve(__dirname, "..");
18
19
  const appDir = process.env.BIVY_DATA_DIR ?? path.join(repoRoot, ".bivy");
@@ -35,6 +36,9 @@ Presets (which labeled key a project uses):
35
36
  bivy credentials preset clear <name> <provider> Remove a provider's mapping from a preset
36
37
 
37
38
  Agent-native logins:
39
+ bivy auth import [claude codex grok] [--sync node|account] [--label name] [--yes] [--dry-run]
40
+ Import local credential files (alias: credentials import).
41
+ Preview first; existing slots are never replaced.
38
42
  bivy credentials ingest [merge|separate] Show or set the ingest policy
39
43
 
40
44
  Config file (${path.relative(process.cwd(), configPath) || configPath}):
@@ -199,6 +203,7 @@ async function main() {
199
203
  return;
200
204
  }
201
205
  switch (command) {
206
+ case "import": return importNativeAuthCommand(credsDir, args);
202
207
  case "list": return cmdList();
203
208
  case "add": return cmdAdd(args[0], args[1], args[2]);
204
209
  case "remove":
@@ -3,6 +3,8 @@
3
3
  /** Direct-mode adapters for operations whose canonical implementation is the
4
4
  * client command registry. Relay and HTTP now differ only in framing. */
5
5
  export const CLIENT_COMMAND_ROUTES = [
6
+ { method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
7
+ { method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
6
8
  { method: "post", path: "/api/session/pause", kind: "session.pause" },
7
9
  { method: "post", path: "/api/session/resume", kind: "session.resume" },
8
10
  { method: "post", path: "/api/session/question/answer", kind: "session.question.answer" },
@@ -8,6 +8,13 @@ const optionalSession = { sessionId: Type.Optional(Type.String()) };
8
8
  * this table remains migration-compatible; adding validation is one data row. */
9
9
  export const CLIENT_COMMAND_SCHEMAS = {
10
10
  ping: Type.Object(request),
11
+ "credentials.native.preview": Type.Object({ ...request, label: Type.Optional(Type.String({ maxLength: 100 })) }),
12
+ "credentials.native.import": Type.Object({
13
+ ...request,
14
+ previewId: Type.String({ minLength: 1, maxLength: 100 }),
15
+ agents: Type.Array(Type.Union([Type.Literal("claude"), Type.Literal("codex"), Type.Literal("grok")]), { minItems: 1, maxItems: 3, uniqueItems: true }),
16
+ sync: Type.Union([Type.Literal("node"), Type.Literal("account")]),
17
+ }),
11
18
  "attachment.fetch": Type.Object({ ...request, hash: Type.String() }),
12
19
  "session.pause": Type.Object(session),
13
20
  "session.resume": Type.Object(session),
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ // Explicit native-login discovery. Reading never changes the source login.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { claudeCredentialFiles } from "./anthropic-preflight.js";
7
+ import { resolveCodexHome } from "./codex-auth.js";
8
+ import { resolveGrokHome } from "./grok-auth.js";
9
+ import { claudeAuthToCredential, codexAuthToCredential, grokAuthToCredential } from "./credential-ingest.js";
10
+ export const nativeAuthSources = {
11
+ claude: { files: claudeCredentialFiles, map: claudeAuthToCredential },
12
+ codex: { files: () => [path.join(resolveCodexHome(), "auth.json")], map: codexAuthToCredential },
13
+ grok: { files: () => [path.join(resolveGrokHome(), "auth.json")], map: grokAuthToCredential },
14
+ };
15
+ export function discoverNativeAuth(agent) {
16
+ const source = nativeAuthSources[agent];
17
+ let status = "missing";
18
+ for (const file of source.files()) {
19
+ try {
20
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
21
+ const mapped = source.map(raw);
22
+ if (mapped)
23
+ return { agent, status: "found", provider: mapped.providerId, credential: mapped.credential };
24
+ status = "unsupported";
25
+ }
26
+ catch (error) {
27
+ // Never report raw parse errors: they can contain credential material.
28
+ if (error.code !== "ENOENT")
29
+ status = "unreadable";
30
+ }
31
+ }
32
+ return { agent, status };
33
+ }
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { createCredentialVault } from "./credential-store.js";
5
+ import { discoverNativeAuth, nativeAuthSources } from "./native-auth-import.js";
6
+ import { normalizeLabel } from "../credentials/records.js";
7
+ const fingerprint = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
8
+ /** Bounded, expiring previews. No tokens or token hashes are sent to the browser.
9
+ * Confirmation re-reads the source, refusing logins changed since the preview. */
10
+ export class NativeAuthPreviewService {
11
+ credsDir;
12
+ previews = new Map();
13
+ constructor(credsDir) {
14
+ this.credsDir = credsDir;
15
+ }
16
+ async preview(rawLabel) {
17
+ if (rawLabel !== undefined && (typeof rawLabel !== "string" || rawLabel.length > 100))
18
+ throw new Error("Invalid credential label.");
19
+ const label = normalizeLabel(rawLabel);
20
+ const vault = createCredentialVault(this.credsDir);
21
+ const items = [];
22
+ const fingerprints = new Map();
23
+ for (const agent of Object.keys(nativeAuthSources)) {
24
+ const found = discoverNativeAuth(agent);
25
+ if (found.status !== "found") {
26
+ items.push({ agent, status: found.status });
27
+ continue;
28
+ }
29
+ const conflict = Boolean(await vault.readRecord(found.provider, label));
30
+ items.push({ agent, provider: found.provider, kind: found.credential.type, status: conflict ? "conflict" : "ready" });
31
+ if (!conflict)
32
+ fingerprints.set(agent, fingerprint(found));
33
+ }
34
+ for (const [id, entry] of this.previews)
35
+ if (entry.expires < Date.now())
36
+ this.previews.delete(id);
37
+ if (this.previews.size >= 32)
38
+ this.previews.delete(this.previews.keys().next().value);
39
+ const previewId = randomUUID();
40
+ this.previews.set(previewId, { label, expires: Date.now() + 5 * 60_000, fingerprints });
41
+ return { previewId, label, items };
42
+ }
43
+ async import(previewId, agents, sync) {
44
+ if (sync !== "node" && sync !== "account")
45
+ throw new Error("Choose node or account availability.");
46
+ const preview = typeof previewId === "string" ? this.previews.get(previewId) : undefined;
47
+ if (!preview || preview.expires < Date.now())
48
+ throw new Error("Preview expired or belongs to another machine. Scan again.");
49
+ if (!Array.isArray(agents) || !agents.length || agents.length > 3 || agents.some((agent) => !preview.fingerprints.has(agent)))
50
+ throw new Error("Select logins from the preview.");
51
+ this.previews.delete(previewId);
52
+ const vault = createCredentialVault(this.credsDir);
53
+ const items = [];
54
+ for (const agent of new Set(agents)) {
55
+ const found = discoverNativeAuth(agent);
56
+ if (found.status !== "found" || fingerprint(found) !== preview.fingerprints.get(agent)) {
57
+ items.push({ agent, status: "changed" });
58
+ continue;
59
+ }
60
+ const inserted = await vault.putRecordIfAbsent({ provider: found.provider, label: preview.label,
61
+ sync, origin: "agent-native", source: { kind: "stored", cred: found.credential } });
62
+ items.push({ agent, status: inserted ? "imported" : "conflict" });
63
+ }
64
+ return { items };
65
+ }
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.16.21-staging.2",
3
+ "version": "0.16.21-staging.4",
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.",