@bivy/bivy 0.16.18-staging.13 → 0.16.18-staging.2

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.
@@ -12,7 +12,6 @@
12
12
  // permission callback, which maps cleanly onto our generic `toolInterceptor`.
13
13
  // * The SDK is loaded with a dynamic import so it stays an *optional*
14
14
  // dependency: a Bivy install only needs it when this runtime is selected.
15
- import { withSessionCredentials, credentialEnvFallback } from "../../credentials/session.js";
16
15
  import { createRequire } from "node:module";
17
16
  import { randomUUID } from "node:crypto";
18
17
  import { EventEmitter } from "node:events";
@@ -783,7 +782,7 @@ class ClaudeSession {
783
782
  async interactiveTuiCommand() {
784
783
  if (!claudeCliAvailable())
785
784
  return null;
786
- const env = await this.resolveCredentialEnv().catch(credentialEnvFallback);
785
+ const env = await this.resolveCredentialEnv().catch(() => ({}));
787
786
  return { command: "claude", args: ["--resume", this.sessionFile], env };
788
787
  }
789
788
  getMessages() {
@@ -957,7 +956,7 @@ class ClaudeSession {
957
956
  if (this.query)
958
957
  return this.refreshSupportedModels();
959
958
  try {
960
- const env = { ...process.env, ...depCacheEnv(this.cwd), ...this.runtimeOptions.env, ...(await this.resolveCredentialEnv().catch(credentialEnvFallback)) };
959
+ const env = { ...process.env, ...depCacheEnv(this.cwd), ...this.runtimeOptions.env, ...(await this.resolveCredentialEnv().catch(() => ({}))) };
961
960
  if (anthropicCredentialPreflight(env))
962
961
  return; // no credential — keep FALLBACK_MODELS
963
962
  await this.ensureStarted();
@@ -990,7 +989,7 @@ class ClaudeSession {
990
989
  // refresh it immediately even if its expiry claims it is still valid.
991
990
  // The resolver compares this under the vault lock, making concurrent
992
991
  // failures converge on one rotation.
993
- const credEnv = await this.resolveCredentialEnv(rejectedToken).catch(credentialEnvFallback);
992
+ const credEnv = await this.resolveCredentialEnv(rejectedToken).catch(() => ({}));
994
993
  const nextToken = authTokenFromEnv(credEnv);
995
994
  if (!nextToken || nextToken === this.spawnedToken)
996
995
  return false;
@@ -1040,8 +1039,8 @@ class ClaudeSession {
1040
1039
  try {
1041
1040
  cred = await store.getCredential(provider, { workspace: this.cwd, ...(rejectedToken ? { rejectedToken } : {}) });
1042
1041
  }
1043
- catch (error) {
1044
- return credentialEnvFallback(error);
1042
+ catch {
1043
+ return {};
1045
1044
  }
1046
1045
  if (!cred)
1047
1046
  return {};
@@ -1315,7 +1314,7 @@ class ClaudeSession {
1315
1314
  // reach the SDK, surface an actionable message instead of letting it spawn
1316
1315
  // and fail its first request with an opaque `401 Unauthorized`.
1317
1316
  if (!this.query) {
1318
- const env = { ...process.env, ...depCacheEnv(this.cwd), ...this.runtimeOptions.env, ...(await this.resolveCredentialEnv().catch(credentialEnvFallback)) };
1317
+ const env = { ...process.env, ...depCacheEnv(this.cwd), ...this.runtimeOptions.env, ...(await this.resolveCredentialEnv().catch(() => ({}))) };
1319
1318
  const preflightError = anthropicCredentialPreflight(env);
1320
1319
  if (preflightError) {
1321
1320
  this.messages.push({ role: "user", content: hasImages ? content : prompt, timestamp: Date.now() });
@@ -1508,12 +1507,12 @@ export class ClaudeCodeRuntime {
1508
1507
  return [{ id: "anthropic", name: "Anthropic", oauth: true, models: FALLBACK_MODELS }];
1509
1508
  }
1510
1509
  async createSession(options) {
1511
- const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider);
1510
+ const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor, options.toolProvider);
1512
1511
  this.sessions.push(session);
1513
1512
  return { session };
1514
1513
  }
1515
1514
  async openSession(options) {
1516
- const session = new ClaudeSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
1515
+ const session = new ClaudeSession(this.options, options.workspace, options.toolInterceptor, options.toolProvider, options.sessionFile);
1517
1516
  this.sessions.push(session);
1518
1517
  return {
1519
1518
  session,
@@ -188,8 +188,7 @@ class PiSession {
188
188
  */
189
189
  async interactiveTuiCommand() {
190
190
  const file = this.sessionFile;
191
- // The shared native auth.json cannot represent a session-local assignment.
192
- if (!file || Object.keys(this.tui.credentialLabels ?? {}).length)
191
+ if (!file)
193
192
  return null;
194
193
  // Pi's own TUI reads its plaintext auth.json store, so project the vault to
195
194
  // disk (refreshed) for the hand-off. Best-effort: an empty auth.json just
@@ -424,7 +423,7 @@ export class PiRuntime {
424
423
  modelsPath: path.join(piDir, "models.json"),
425
424
  allowModelNetwork,
426
425
  })
427
- : await createPiModelRuntime({ credsDir, piDir, allowModelNetwork, workspace: sessionManager.getCwd() || options.workspace, credentialLabels: options.credentialLabels });
426
+ : await createPiModelRuntime({ credsDir, piDir, allowModelNetwork });
428
427
  const backgroundShells = new BackgroundShellTracker();
429
428
  const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
430
429
  const sessionId = sessionManager.getSessionId();
@@ -452,7 +451,6 @@ export class PiRuntime {
452
451
  sessionManager,
453
452
  });
454
453
  const tui = {
455
- credentialLabels: options.credentialLabels,
456
454
  credsDir,
457
455
  piDir,
458
456
  sessionsDir: this.options.sessionsDir,
@@ -10,13 +10,23 @@
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 "./store.js";
14
- import { selectCredential } from "./selection.js";
15
- export { projectIdsFromWorkspace } from "./selection.js";
15
+ import { resolveCredential } from "./records.js";
16
16
  import { loadPresets, defaultPresetsPath } from "./presets.js";
17
- import { credentialEnvFallback } from "./session.js";
18
17
  /** Refresh an OAuth token this many ms before it expires (clock-skew guard). */
19
18
  const OAUTH_REFRESH_SKEW_MS = 60_000;
19
+ /** Stable project identifiers discoverable without importing repo/session code. */
20
+ export function projectIdsFromWorkspace(workspace) {
21
+ const resolved = path.resolve(workspace);
22
+ const ids = new Set([resolved, path.basename(resolved)]);
23
+ for (const part of resolved.split(path.sep)) {
24
+ const split = part.indexOf("__");
25
+ if (split > 0 && split < part.length - 2)
26
+ ids.add(`${part.slice(0, split)}/${part.slice(split + 2)}`);
27
+ }
28
+ return [...ids];
29
+ }
20
30
  /** Resolver over Bivy's credential store, with OAuth refresh-on-read via the bridge. */
21
31
  export class NodeCredentialResolver {
22
32
  credsDir;
@@ -49,7 +59,14 @@ export class NodeCredentialResolver {
49
59
  // than guessing.
50
60
  const records = await this.store.listRecords().catch(() => []);
51
61
  const presets = this.presets();
52
- const selection = selectCredential(id, records, presets, context);
62
+ // Project assignments are ordinary preset mappings named `project:<id>`.
63
+ // Bivy-managed clones encode owner/repo as owner__repo in their workspace
64
+ // path; direct local workspaces also match their absolute path/basename.
65
+ const explicitProject = context?.project?.trim();
66
+ const workspace = context?.workspace?.trim();
67
+ const projectCandidates = [explicitProject, ...(workspace ? projectIdsFromWorkspace(workspace) : [])].filter((value) => Boolean(value));
68
+ const projectPreset = projectCandidates.map((value) => `project:${value}`).find((name) => presets.presets?.[name]?.[id]);
69
+ const selection = resolveCredential(id, records, presets, { ...(projectPreset ? { preset: projectPreset } : {}), ...(context?.preferLabel ? { preferLabel: context.preferLabel } : {}) });
53
70
  if (!selection)
54
71
  return undefined;
55
72
  const source = selection.record.source;
@@ -167,20 +184,11 @@ export async function buildAgentCredentialEnv(store, providers, activeProvider,
167
184
  try {
168
185
  cred = await store.getCredential(id, workspace ? { workspace } : undefined);
169
186
  }
170
- catch (error) {
171
- credentialEnvFallback(error);
187
+ catch {
172
188
  continue;
173
189
  }
174
190
  if (!cred)
175
191
  continue;
176
- // Session-pinned Anthropic auth clears the competing ambient login even
177
- // when the agent has not advertised its active model provider yet.
178
- if (cred.provider === "anthropic" && cred.env) {
179
- for (const key of ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"]) {
180
- if (cred.env[key] === "")
181
- env[key] = "";
182
- }
183
- }
184
192
  const isActive = !!active && cred.provider === active;
185
193
  if (cred.kind === "oauth") {
186
194
  // OAuth *subscription* tokens are provider-specific and are not accepted
@@ -135,8 +135,8 @@ export class AgentService {
135
135
  const toolProvider = message.options.toolSpecs?.length ? this.makeToolProvider(svc, message.options.toolSpecs) : undefined;
136
136
  const workspace = message.options.workspace ?? process.cwd();
137
137
  const result = message.op === "open" && message.options.sessionFile
138
- ? await runtime.openSession({ workspace, credentialLabels: message.options.credentialLabels, sessionFile: message.options.sessionFile, toolInterceptor, toolProvider })
139
- : await runtime.createSession({ workspace, credentialLabels: message.options.credentialLabels, toolInterceptor, toolProvider });
138
+ ? await runtime.openSession({ workspace, sessionFile: message.options.sessionFile, toolInterceptor, toolProvider })
139
+ : await runtime.createSession({ workspace, toolInterceptor, toolProvider });
140
140
  svc.session = result.session;
141
141
  svc.id = result.session.id;
142
142
  svc.lastSent = this.mirror(result.session);
@@ -7,8 +7,8 @@
7
7
  // not guesses. Owning these lets Bivy run the OAuth login + token refresh itself,
8
8
  // so no credential operation depends on Pi.
9
9
  //
10
- // Covered (fully Bivy-owned): Anthropic (Claude Pro/Max), OpenAI Codex (ChatGPT;
11
- // device-code), xAI (Grok). GitHub Copilot (two-stage token + dynamic base URL entangled with
10
+ // Covered (fully Bivy-owned): Anthropic (Claude Pro/Max), OpenAI Codex (ChatGPT),
11
+ // xAI (Grok). GitHub Copilot (two-stage token + dynamic base URL entangled with
12
12
  // Pi's request layer) and Radius (self-describing gateway) are intentionally not
13
13
  // reimplemented here.
14
14
  /** Anthropic uses a JSON token body; OpenAI/xAI use form-encoded. */
@@ -17,16 +17,12 @@ export const MODEL_OAUTH_PROVIDERS = {
17
17
  id: "anthropic",
18
18
  displayName: "Anthropic (Claude Pro/Max)",
19
19
  clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
20
- // Claude Code's `setup-token` flow avoids localhost entirely: Anthropic hosts
21
- // the callback page and shows a code the user can paste back into Bivy. The
22
- // resulting long-lived token is inference-only, which is exactly what agent
23
- // model requests need and works well from a remote PWA/headless node.
24
- scopes: "user:inference",
20
+ scopes: "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload",
25
21
  flow: "auth_code",
26
22
  tokenEncoding: "json",
27
- authorizeUrl: "https://claude.com/cai/oauth/authorize",
23
+ authorizeUrl: "https://claude.ai/oauth/authorize",
28
24
  tokenUrl: "https://platform.claude.com/v1/oauth/token",
29
- redirectUri: "https://platform.claude.com/oauth/code/callback",
25
+ callback: { port: 53692, path: "/callback", redirectHost: "localhost" },
30
26
  authorizeParams: { code: "true" },
31
27
  stateIsVerifier: true,
32
28
  refreshSkewMs: 5 * 60 * 1000,
@@ -37,13 +33,11 @@ export const MODEL_OAUTH_PROVIDERS = {
37
33
  displayName: "OpenAI (ChatGPT Plus/Pro)",
38
34
  clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
39
35
  scopes: "openid profile email offline_access",
40
- flow: "openai_codex_device_code",
36
+ flow: "auth_code",
41
37
  tokenEncoding: "form",
42
38
  authorizeUrl: "https://auth.openai.com/oauth/authorize",
43
39
  tokenUrl: "https://auth.openai.com/oauth/token",
44
40
  callback: { port: 1455, path: "/auth/callback", redirectHost: "localhost" },
45
- deviceAuthUrl: "https://auth.openai.com/api/accounts/deviceauth/usercode",
46
- deviceTokenUrl: "https://auth.openai.com/api/accounts/deviceauth/token",
47
41
  authorizeParams: {
48
42
  id_token_add_organizations: "true",
49
43
  codex_cli_simplified_flow: "true",
@@ -199,7 +199,7 @@ function startCallbackServer(host, port, pathName, expectedState, signal) {
199
199
  async function loginAuthCode(provider, interaction) {
200
200
  const { verifier, challenge } = createPkce();
201
201
  const state = provider.stateIsVerifier ? verifier : randomBytes(16).toString("hex");
202
- const redirectUri = provider.redirectUri ?? `http://${provider.callback.redirectHost}:${provider.callback.port}${provider.callback.path}`;
202
+ const redirectUri = `http://${provider.callback.redirectHost}:${provider.callback.port}${provider.callback.path}`;
203
203
  const authorizeUrl = buildAuthorizeUrl(provider, { challenge, state, redirectUri });
204
204
  interaction.notify({
205
205
  type: "auth_url",
@@ -300,88 +300,6 @@ async function loginDeviceCode(provider, interaction) {
300
300
  return tokensFrom(provider, payload);
301
301
  }
302
302
  }
303
- async function postJsonObject(url, body, signal) {
304
- const res = await fetch(url, {
305
- method: "POST",
306
- headers: { "content-type": "application/json", accept: "application/json" },
307
- body: JSON.stringify(body),
308
- signal,
309
- });
310
- const text = await res.text();
311
- let payload = {};
312
- try {
313
- payload = JSON.parse(text);
314
- }
315
- catch { /* non-JSON error body */ }
316
- return { status: res.status, ok: res.ok, payload, text };
317
- }
318
- function nestedOAuthErrorCode(payload) {
319
- const error = payload.error;
320
- if (typeof error === "string")
321
- return error;
322
- if (error && typeof error === "object") {
323
- const code = error.code;
324
- return typeof code === "string" ? code : "";
325
- }
326
- return "";
327
- }
328
- async function loginOpenAICodexDeviceCode(provider, interaction) {
329
- const started = await postJsonObject(provider.deviceAuthUrl, { client_id: provider.clientId }, interaction.signal);
330
- if (!started.ok)
331
- throw new Error(`OpenAI Codex device authorization failed (${started.status}): ${started.text.slice(0, 200)}`);
332
- const deviceAuthId = typeof started.payload.device_auth_id === "string" ? started.payload.device_auth_id : "";
333
- const userCode = typeof started.payload.user_code === "string" ? started.payload.user_code : "";
334
- const rawInterval = Number(started.payload.interval);
335
- const interval = Number.isFinite(rawInterval) && rawInterval >= 0 ? rawInterval : 5;
336
- if (!deviceAuthId || !userCode)
337
- throw new Error(`Invalid OpenAI Codex device authorization response: ${JSON.stringify(started.payload)}`);
338
- const expiresInSeconds = 15 * 60;
339
- interaction.notify({
340
- type: "device_code",
341
- userCode,
342
- verificationUri: "https://auth.openai.com/codex/device",
343
- intervalSeconds: interval,
344
- expiresInSeconds,
345
- });
346
- const deadline = Date.now() + expiresInSeconds * 1000;
347
- let authorizationCode = "";
348
- let codeVerifier = "";
349
- let waitMs = interval * 1000;
350
- await sleep(waitMs);
351
- while (Date.now() <= deadline) {
352
- if (interaction.signal?.aborted)
353
- throw new Error("Login aborted");
354
- const polled = await postJsonObject(provider.deviceTokenUrl, { device_auth_id: deviceAuthId, user_code: userCode }, interaction.signal);
355
- if (polled.ok) {
356
- authorizationCode = typeof polled.payload.authorization_code === "string" ? polled.payload.authorization_code : "";
357
- codeVerifier = typeof polled.payload.code_verifier === "string" ? polled.payload.code_verifier : "";
358
- if (!authorizationCode || !codeVerifier)
359
- throw new Error(`Invalid OpenAI Codex device token response: ${JSON.stringify(polled.payload)}`);
360
- break;
361
- }
362
- const errorCode = nestedOAuthErrorCode(polled.payload);
363
- if (polled.status === 403 || polled.status === 404 || errorCode === "deviceauth_authorization_pending") {
364
- await sleep(waitMs);
365
- continue;
366
- }
367
- if (errorCode === "slow_down") {
368
- waitMs += 5000;
369
- await sleep(waitMs);
370
- continue;
371
- }
372
- throw new Error(`OpenAI Codex device authorization failed (${polled.status}): ${polled.text.slice(0, 200)}`);
373
- }
374
- if (!authorizationCode || !codeVerifier)
375
- throw new Error("OpenAI Codex device login timed out. Please try again.");
376
- const payload = await postToken(provider.tokenUrl, provider.tokenEncoding, {
377
- grant_type: "authorization_code",
378
- client_id: provider.clientId,
379
- code: authorizationCode,
380
- code_verifier: codeVerifier,
381
- redirect_uri: "https://auth.openai.com/deviceauth/callback",
382
- });
383
- return tokensFrom(provider, payload);
384
- }
385
303
  // --- Public API --------------------------------------------------------------
386
304
  /** Provider ids Bivy can natively drive a subscription login for. */
387
305
  export { isNativeOAuthProvider, nativeOAuthProviderIds } from "./model-oauth-providers.js";
@@ -394,9 +312,7 @@ export async function loginModelOAuth(credsDir, providerId, interaction, label =
394
312
  const provider = getModelOAuthProvider(providerId);
395
313
  if (!provider)
396
314
  throw new Error(`Provider "${providerId}" does not support subscription login`);
397
- const tokens = provider.flow === "device_code" ? await loginDeviceCode(provider, interaction)
398
- : provider.flow === "openai_codex_device_code" ? await loginOpenAICodexDeviceCode(provider, interaction)
399
- : await loginAuthCode(provider, interaction);
315
+ const tokens = provider.flow === "device_code" ? await loginDeviceCode(provider, interaction) : await loginAuthCode(provider, interaction);
400
316
  const credential = { type: "oauth", access: tokens.access, refresh: tokens.refresh, expires: tokens.expires, refreshedAt: tokens.refreshedAt, ...(tokens.accountId ? { accountId: tokens.accountId } : {}) };
401
317
  await createCredentialVault(credsDir).modifyRecord(providerId, label, async () => credential);
402
318
  }
@@ -11,7 +11,6 @@
11
11
  import path from "node:path";
12
12
  import { createCredentialVault } from "./credential-store.js";
13
13
  import { isNativeOAuthProvider } from "./oauth/model-oauth-providers.js";
14
- import { selectedCredentialStore } from "../credentials/selected-store.js";
15
14
  /** Adapt Bivy's store to pi-ai's structurally-identical CredentialStore for injection. */
16
15
  export function piCredentialStore(store) {
17
16
  return store;
@@ -26,11 +25,8 @@ export function piCredentialStore(store) {
26
25
  export async function createPiModelRuntime(opts) {
27
26
  const store = opts.store ?? createCredentialVault(opts.credsDir);
28
27
  const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
29
- const selected = selectedCredentialStore(store, opts.credsDir, { workspace: opts.workspace, credentialLabels: opts.credentialLabels });
30
- for (const provider of Object.keys(opts.credentialLabels ?? {}))
31
- await selected.read(provider);
32
28
  return ModelRuntime.create({
33
- credentials: piCredentialStore(selected),
29
+ credentials: piCredentialStore(store),
34
30
  modelsPath: path.join(opts.piDir, "models.json"),
35
31
  allowModelNetwork: opts.allowModelNetwork ?? false,
36
32
  });
@@ -5,7 +5,6 @@ import { randomUUID } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import { stripAnsi } from "./ansi.js";
7
7
  import { buildAgentCredentialEnv } from "./credentials.js";
8
- import { withSessionCredentials, credentialEnvFallback } from "../credentials/session.js";
9
8
  import { egressEnv, sessionEgressEnv } from "../harness/egress.js";
10
9
  import { depCacheEnv } from "../harness/dep-cache.js";
11
10
  import { bivySessionEnv } from "./session-env.js";
@@ -305,7 +304,7 @@ class ProcessSession {
305
304
  // added after this session started) reach the agent. The vault wins over any
306
305
  // ambient key so Bivy's shared sign-in is authoritative.
307
306
  const credentialEnv = this.runtimeOptions.credentials
308
- ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(credentialEnvFallback)
307
+ ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(() => ({}))
309
308
  : {};
310
309
  // Optional prepare step (e.g. Codex materializes its auth.json from the vault
311
310
  // and pins CODEX_HOME). Runs after credentials, before preflight/spawn; its
@@ -531,7 +530,7 @@ export class ProcessRuntime {
531
530
  return [...byProvider.values()];
532
531
  }
533
532
  async createSession(options) {
534
- const session = new ProcessSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace);
533
+ const session = new ProcessSession(this.options, options.workspace);
535
534
  this.sessions.push(session);
536
535
  return { session, warning: "Generic CLI runtime streams stdout/stderr only; approvals, model picker, and resume depend on the underlying agent protocol." };
537
536
  }
@@ -539,7 +538,7 @@ export class ProcessRuntime {
539
538
  // Resumable runtimes bind the agent's session id so each prompt continues it
540
539
  // (see resumeArgs); non-resumable ones ignore the ref and start fresh.
541
540
  if (this.options.resumable) {
542
- const session = new ProcessSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, options.sessionFile);
541
+ const session = new ProcessSession(this.options, options.workspace, options.sessionFile);
543
542
  this.sessions.push(session);
544
543
  return { session };
545
544
  }
@@ -4,7 +4,6 @@ import { spawn } from "node:child_process";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
6
  import { buildAgentCredentialEnv } from "./credentials.js";
7
- import { withSessionCredentials, credentialEnvFallback } from "../credentials/session.js";
8
7
  import { bivySessionEnv } from "./session-env.js";
9
8
  import { mergeAgentCommands } from "./slash-commands.js";
10
9
  import { withExactCapabilitySurface } from "./types.js";
@@ -319,7 +318,7 @@ class ProtocolSession {
319
318
  if (!hook)
320
319
  return null;
321
320
  const credentialEnv = this.runtimeOptions.credentials
322
- ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(credentialEnvFallback)
321
+ ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(() => ({}))
323
322
  : {};
324
323
  let prepareEnv = this.prepareEnv;
325
324
  if (this.runtimeOptions.prepare) {
@@ -341,7 +340,7 @@ class ProtocolSession {
341
340
  if (this.child)
342
341
  return;
343
342
  const credentialEnv = this.runtimeOptions.credentials
344
- ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(credentialEnvFallback)
343
+ ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(() => ({}))
345
344
  : {};
346
345
  // Optional prepare step, run before the child spawns because a shim reads its
347
346
  // credential at launch (e.g. Codex mints ~/.codex/auth.json from the vault and
@@ -441,7 +440,7 @@ class ProtocolSession {
441
440
  if (!this.runtimeOptions.prepare && !this.runtimeOptions.preflight)
442
441
  return undefined;
443
442
  const credentialEnv = this.runtimeOptions.credentials
444
- ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(credentialEnvFallback)
443
+ ? await buildAgentCredentialEnv(this.runtimeOptions.credentials, undefined, this.currentModelProvider, this.cwd).catch(() => ({}))
445
444
  : {};
446
445
  if (this.runtimeOptions.prepare) {
447
446
  this.prepareEnv =
@@ -994,7 +993,7 @@ export class ProtocolRuntime {
994
993
  return this.options.catalog ?? [];
995
994
  }
996
995
  async createSession(options) {
997
- const session = new ProtocolSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, this.capabilities, options.toolInterceptor);
996
+ const session = new ProtocolSession(this.options, options.workspace, this.capabilities, options.toolInterceptor);
998
997
  try {
999
998
  await session.start();
1000
999
  this.sessions.push(session);
@@ -1011,7 +1010,7 @@ export class ProtocolRuntime {
1011
1010
  // Adopt the caller's canonical id (a reopen of a known session) so the
1012
1011
  // resumed session keeps its original id instead of taking `sessionFile` (the
1013
1012
  // agent's own ref) as its id — see OpenSessionOptions.canonicalId.
1014
- const session = new ProtocolSession(await withSessionCredentials(this.options, options.credentialLabels), options.workspace, this.capabilities, options.toolInterceptor, options.sessionFile, options.canonicalId);
1013
+ const session = new ProtocolSession(this.options, options.workspace, this.capabilities, options.toolInterceptor, options.sessionFile, options.canonicalId);
1015
1014
  try {
1016
1015
  await session.start();
1017
1016
  if (!this.options.resumable && !this.capabilities.resume) {
@@ -543,12 +543,12 @@ export class RemoteRuntime {
543
543
  async createSession(options) {
544
544
  const transport = await this.config.connect();
545
545
  const toolSpecs = options.toolProvider?.list();
546
- return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "create", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
546
+ return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "create", options: { workspace: options.workspace, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
547
547
  }
548
548
  async openSession(options) {
549
549
  const transport = await this.config.connect();
550
550
  const toolSpecs = options.toolProvider?.list();
551
- return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "open", options: { credentialLabels: options.credentialLabels, workspace: options.workspace, sessionFile: options.sessionFile, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
551
+ return startRemoteSession(transport, { runtime: this.id, sandbox: this.config.sandbox, op: "open", options: { workspace: options.workspace, sessionFile: options.sessionFile, hasToolInterceptor: Boolean(options.toolInterceptor), ...(toolSpecs?.length ? { toolSpecs } : {}) } }, { toolInterceptor: options.toolInterceptor, toolProvider: options.toolProvider });
552
552
  }
553
553
  /**
554
554
  * Re-attach to a session already live on the agent service (Stage 2 routing).
package/dist/server.js CHANGED
@@ -36,7 +36,6 @@ import { InMemoryLocationRegistry } from "./runtime/location-registry.js";
36
36
  import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } from "./runtime/control-plane-location.js";
37
37
  import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
38
38
  import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
39
- import { decodeAutomationTemplate } from "./automation-template.js";
40
39
  import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
41
40
  import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
42
41
  import { probeAnthropicAccess } from "./runtime/anthropic-preflight.js";
@@ -4137,7 +4136,6 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
4137
4136
  // which now adopts the existing remote branch rather than colliding.
4138
4137
  const existing = findIssueSession(source);
4139
4138
  if (existing?.worktree && fs.existsSync(existing.worktree.path)) {
4140
- assertSessionAccounts(existing, overrides.credentialLabels);
4141
4139
  const currentSandbox = existing.sandbox ?? sandboxTier();
4142
4140
  const safety = projectSafety(existing.worktree.path, overrides.sandbox ?? currentSandbox, overrides.approvalMode);
4143
4141
  if (safety.sandbox !== currentSandbox) {
@@ -4210,7 +4208,6 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
4210
4208
  runtimeId: directives.runtimeId,
4211
4209
  sandbox: safety.sandbox,
4212
4210
  approvalMode: safety.approval,
4213
- credentialLabels: overrides.credentialLabels,
4214
4211
  });
4215
4212
  record.githubIssueUrl = `https://github.com/${cfg.owner}/${cfg.repo}/issues/${issue.number}`;
4216
4213
  // Title the session from the issue up front so it never shows as "Untitled
@@ -4808,11 +4805,6 @@ function linearSessionSource(externalId) {
4808
4805
  * starting, and return false so the caller falls through. Returns true only when
4809
4806
  * it fully handled the item.
4810
4807
  */
4811
- function assertSessionAccounts(record, labels) {
4812
- if (Object.entries(labels ?? {}).some(([provider, label]) => record.credentialLabels?.[provider] !== label)) {
4813
- throw new Error("This session uses different provider accounts; start a new session to use the automation's selections");
4814
- }
4815
- }
4816
4808
  async function continueCorrelatedSession(item, prompt, report, opts) {
4817
4809
  if (item.targetKind !== "existing_session" || !item.targetSessionId)
4818
4810
  return false;
@@ -4839,7 +4831,6 @@ async function continueCorrelatedSession(item, prompt, report, opts) {
4839
4831
  }
4840
4832
  return false;
4841
4833
  }
4842
- assertSessionAccounts(record, opts?.credentialLabels);
4843
4834
  const branch = record.worktree?.branch;
4844
4835
  if (opts?.resumeOnMissing) {
4845
4836
  // Durable work targeting an existing Session waits for its current turn to
@@ -4917,16 +4908,13 @@ async function executeWorkItem(item, report, signal) {
4917
4908
  // assigned node can read. The envelope prefix is Bivy's own and never appears
4918
4909
  // on issue/Slack/Linear bodies, so decrypt whenever it's present regardless of
4919
4910
  // source.
4920
- let credentialLabels;
4921
4911
  if (item.body?.startsWith("bivy-room-v1:")) {
4922
4912
  const [, nodeId, ...payload] = item.body.split(":");
4923
4913
  if (nodeId !== identity.nodeId || payload.length === 0) {
4924
4914
  throw new Error("automation instructions were encrypted for a different node");
4925
4915
  }
4926
4916
  try {
4927
- const template = decodeAutomationTemplate(open(pairingStore.roomKey(), payload.join(":")));
4928
- credentialLabels = template.credentialLabels;
4929
- item = { ...item, body: template.instructions };
4917
+ item = { ...item, body: open(pairingStore.roomKey(), payload.join(":")) };
4930
4918
  }
4931
4919
  catch {
4932
4920
  throw new Error("could not decrypt automation instructions on this node");
@@ -4995,7 +4983,6 @@ async function executeWorkItem(item, report, signal) {
4995
4983
  await runIssueTask(cfg, issue, {
4996
4984
  runtimeId: item.runtimeId,
4997
4985
  model: item.model,
4998
- credentialLabels,
4999
4986
  sandbox: normalizeSandboxTier(item.sandbox),
5000
4987
  approvalMode: approvalModeFrom(item.approvalMode),
5001
4988
  onEvidence: report,
@@ -5020,7 +5007,7 @@ async function executeWorkItem(item, report, signal) {
5020
5007
  throw new Error(`Linear work item has an invalid repo "${repoSlug}"`);
5021
5008
  // Case B: a re-dispatch the control plane correlated to an existing session
5022
5009
  // continues it as a normal chat instead of starting cold (mirrors GitHub).
5023
- if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue, item.body), report, { resumeOnMissing: item.targetKind === "existing_session", signal, credentialLabels }))
5010
+ if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue, item.body), report, { resumeOnMissing: item.targetKind === "existing_session", signal }))
5024
5011
  return;
5025
5012
  const githubToken = await resolveGitHubToken();
5026
5013
  if (!githubToken)
@@ -5036,7 +5023,6 @@ async function executeWorkItem(item, report, signal) {
5036
5023
  makeActive: false,
5037
5024
  source: linearSessionSource(item.externalId),
5038
5025
  runtimeId: item.runtimeId || nodeConfiguredDefaultAgent(),
5039
- credentialLabels,
5040
5026
  sandbox: safety.sandbox,
5041
5027
  approvalMode: safety.approval,
5042
5028
  });
@@ -5082,7 +5068,7 @@ async function executeWorkItem(item, report, signal) {
5082
5068
  // Scheduled runs targeting an existing session are STRICT: the message must
5083
5069
  // land in that session (resumed from disk if needed), never silently in a new
5084
5070
  // one — so a session that can't be resumed fails the run instead.
5085
- if (await continueCorrelatedSession(item, request, report, { resumeOnMissing: item.source === "schedule" || item.targetKind === "existing_session", isMessage, signal, credentialLabels }))
5071
+ if (await continueCorrelatedSession(item, request, report, { resumeOnMissing: item.source === "schedule" || item.targetKind === "existing_session", isMessage, signal }))
5086
5072
  return;
5087
5073
  const requestedSandbox = normalizeSandboxTier(item.sandbox);
5088
5074
  // Prepare an explicit repository before resolving its policy. Otherwise a
@@ -5098,7 +5084,6 @@ async function executeWorkItem(item, report, signal) {
5098
5084
  const sessionOpts = {
5099
5085
  makeActive: false,
5100
5086
  title: item.title,
5101
- credentialLabels,
5102
5087
  runtimeId: item.runtimeId,
5103
5088
  sandbox,
5104
5089
  };
@@ -6318,7 +6303,6 @@ function persistSessionMetadata(record, status = sessionStatus(record)) {
6318
6303
  delegationDepth: record.delegationDepth,
6319
6304
  runtimeId: record.runtimeId,
6320
6305
  sandbox: record.sandbox,
6321
- credentialLabels: record.credentialLabels,
6322
6306
  agentName: getRuntime(record.runtimeId).displayName,
6323
6307
  contract: record.contract,
6324
6308
  status,
@@ -7586,7 +7570,7 @@ async function refreshRecordAfterTui(record) {
7586
7570
  const workspace = record.worktree?.path || oldSession.cwd || record.workspace;
7587
7571
  // Refreshing an EXISTING record: its id is already known, so attach_to_chat
7588
7572
  // (see toolProvider's SessionIdRef doc) can be wired live, not deferred.
7589
- const runtimeSessionOptions = { credentialLabels: record.credentialLabels, workspace, toolProvider: integrations.toolProvider({ current: record.id }), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
7573
+ const runtimeSessionOptions = { workspace, toolProvider: integrations.toolProvider({ current: record.id }), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
7590
7574
  const { session, warning } = await runtimeHost.openSession(rt, { ...runtimeSessionOptions, sessionFile: record.sessionFile });
7591
7575
  record.session = session;
7592
7576
  record.sessionFile = session.sessionFile ?? record.sessionFile;
@@ -7821,7 +7805,6 @@ async function recoverRecordAfterAbort(record) {
7821
7805
  const workspace = record.worktree?.path || oldSession.cwd || record.workspace;
7822
7806
  const runtimeSessionOptions = {
7823
7807
  workspace,
7824
- credentialLabels: record.credentialLabels,
7825
7808
  toolProvider: integrations.toolProvider({ current: record.id }),
7826
7809
  ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}),
7827
7810
  };
@@ -7921,7 +7904,6 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
7921
7904
  const restoredWorktree = requestedSessionFile ? restoredWorktreeFromMetadata(storedMeta) : undefined;
7922
7905
  const existing = requestedSessionFile ? (openSessions.get(requestedSessionFile) ?? (storedMeta?.id ? openSessions.get(storedMeta.id) : undefined)) : undefined;
7923
7906
  if (existing) {
7924
- assertSessionAccounts(existing, opts.credentialLabels);
7925
7907
  // Reopening an already-open session must NOT bump its last-active time —
7926
7908
  // that only tracks real user/agent activity, not focus. (Was touchSession.)
7927
7909
  if (makeActive)
@@ -8011,8 +7993,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8011
7993
  // built now, up front — so hand it this box instead of a session id and fill
8012
7994
  // `.current` in the moment `sessionId` is (see toolProvider's SessionIdRef doc).
8013
7995
  const attachSessionIdRef = {};
8014
- const credentialLabels = opts.credentialLabels ?? storedMeta?.credentialLabels;
8015
- const runtimeSessionOptions = { credentialLabels, workspace: runtimeWorkspace, toolProvider: integrations.toolProvider(attachSessionIdRef), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
7996
+ const runtimeSessionOptions = { workspace: runtimeWorkspace, toolProvider: integrations.toolProvider(attachSessionIdRef), ...(rt.capabilities.toolInterception ? { toolInterceptor: guardianInterceptor } : {}) };
8016
7997
  // Stage 2/3: prefer re-attaching to a still-live remote session — routed to its
8017
7998
  // OWN agent service — over re-opening a fresh copy from disk. Falls back to
8018
7999
  // open/create when nothing live is there.
@@ -8073,7 +8054,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8073
8054
  // Rehydrating (rather than leaving this undefined for a resumed session)
8074
8055
  // also avoids a persistSessionMetadata call later silently clobbering the
8075
8056
  // stored contract with undefined via its `{...prev, ...input}` merge.
8076
- const record = { id: sessionId, session, runtimeId: rt.id, credentialLabels, sandbox: sessionSandbox, approvalMode: sessionSafety.approval, automationRunId: storedMeta?.automationRunId, delegationDepth: storedMeta?.delegationDepth, workspace: sessionWorkspace, sessionFile: session.sessionFile, agentServiceAddress: attachedAddress ?? rt.agentServiceAddress, worktree, source, prUrl: storedMeta?.prUrl, prs: storedMeta?.prs, contract: storedMeta?.contract, lastTouchedAt: resumedLastActive ?? Date.now(), warning: modelFallbackMessage, ephemeral: opts.ephemeral, workspaceState: runGit(["status", "--porcelain", "--untracked-files=normal"], sessionWorkspace) ? "dirty" : "clean" };
8057
+ const record = { id: sessionId, session, runtimeId: rt.id, sandbox: sessionSandbox, approvalMode: sessionSafety.approval, automationRunId: storedMeta?.automationRunId, delegationDepth: storedMeta?.delegationDepth, workspace: sessionWorkspace, sessionFile: session.sessionFile, agentServiceAddress: attachedAddress ?? rt.agentServiceAddress, worktree, source, prUrl: storedMeta?.prUrl, prs: storedMeta?.prs, contract: storedMeta?.contract, lastTouchedAt: resumedLastActive ?? Date.now(), warning: modelFallbackMessage, ephemeral: opts.ephemeral, workspaceState: runGit(["status", "--porcelain", "--untracked-files=normal"], sessionWorkspace) ? "dirty" : "clean" };
8077
8058
  // Migration: a session resumed/reopened from before this feature (or from a
8078
8059
  // node that predates it) has no stored contract. Stamp an honest one now
8079
8060
  // from currently-observed facts rather than leaving it blank forever or
@@ -8390,7 +8371,7 @@ async function createWorkspaceSession(workspace, opts = {}) {
8390
8371
  await fetchOrigin(workspace);
8391
8372
  return createGitWorkspaceSession(workspace, parsed, opts);
8392
8373
  }
8393
- const record = await createSession(workspace, undefined, { runtimeId: opts.runtimeId, credentialLabels: opts.credentialLabels, sandbox: opts.sandbox, makeActive: opts.makeActive });
8374
+ const record = await createSession(workspace, undefined, { runtimeId: opts.runtimeId, sandbox: opts.sandbox, makeActive: opts.makeActive });
8394
8375
  applyInitialSessionName(record, opts);
8395
8376
  return record;
8396
8377
  }
@@ -8408,7 +8389,6 @@ async function createGitWorkspaceSession(repoDir, parsed, opts = {}) {
8408
8389
  worktree: { branch, base },
8409
8390
  source: `repo:${parsed.slug}`,
8410
8391
  runtimeId: opts.runtimeId,
8411
- credentialLabels: opts.credentialLabels,
8412
8392
  sandbox: opts.sandbox,
8413
8393
  makeActive: opts.makeActive,
8414
8394
  });
@@ -345,14 +345,12 @@ export function createRunTerminals(deps) {
345
345
  const discover = SESSION_DISCOVERY_BY_AGENT[agent];
346
346
  if (!discover)
347
347
  return undefined;
348
- const attempts = Math.max(1, Math.floor(deps.takeoverDiscoveryAttempts ?? TAKEOVER_DISCOVERY_ATTEMPTS));
349
- const delayMs = Math.max(0, Math.floor(deps.takeoverDiscoveryDelayMs ?? TAKEOVER_DISCOVERY_DELAY_MS));
350
- for (let attempt = 0; attempt < attempts; attempt++) {
348
+ for (let attempt = 0; attempt < TAKEOVER_DISCOVERY_ATTEMPTS; attempt++) {
351
349
  const ref = await discover(workspace, createdAt);
352
350
  if (ref)
353
351
  return ref;
354
- if (delayMs > 0 && attempt + 1 < attempts) {
355
- await new Promise((resolve) => setTimeout(resolve, delayMs));
352
+ if (attempt + 1 < TAKEOVER_DISCOVERY_ATTEMPTS) {
353
+ await new Promise((resolve) => setTimeout(resolve, TAKEOVER_DISCOVERY_DELAY_MS));
356
354
  }
357
355
  }
358
356
  return undefined;
package/dist/terminal.js CHANGED
@@ -196,8 +196,6 @@ export class TerminalManager {
196
196
  env,
197
197
  });
198
198
  const now = Date.now();
199
- let resolveExit;
200
- const exitPromise = new Promise((resolve) => { resolveExit = resolve; });
201
199
  const entry = {
202
200
  proc,
203
201
  workspace: options.workspace,
@@ -211,8 +209,6 @@ export class TerminalManager {
211
209
  flushTimer: null,
212
210
  closed: false,
213
211
  onData: options.onData,
214
- exitPromise,
215
- resolveExit,
216
212
  };
217
213
  // Register the opener as a sized client so a later, smaller client shrinks
218
214
  // the PTY to the min of the two rather than clobbering the opener's size.
@@ -273,12 +269,7 @@ export class TerminalManager {
273
269
  flush();
274
270
  entry.closed = true;
275
271
  this.terminals.delete(id);
276
- try {
277
- options.onExit(exitCode, signal, entry.buffer);
278
- }
279
- finally {
280
- entry.resolveExit();
281
- }
272
+ options.onExit(exitCode, signal, entry.buffer);
282
273
  });
283
274
  return id;
284
275
  }
@@ -356,19 +347,6 @@ export class TerminalManager {
356
347
  const entry = this.terminals.get(id);
357
348
  if (!entry)
358
349
  return false;
359
- this.closeEntry(id, entry);
360
- return true;
361
- }
362
- /** Close a terminal and wait for the underlying PTY process to report exit. */
363
- async closeAndWait(id, timeoutMs = 2000) {
364
- const entry = this.terminals.get(id);
365
- if (!entry)
366
- return false;
367
- this.closeEntry(id, entry);
368
- await waitForExit(entry.exitPromise, timeoutMs);
369
- return true;
370
- }
371
- closeEntry(id, entry) {
372
350
  this.terminals.delete(id);
373
351
  // Drop any queued output — the client asked to close, so don't emit a
374
352
  // trailing batch (which would fire onData for a terminal it has torn down).
@@ -385,6 +363,7 @@ export class TerminalManager {
385
363
  catch {
386
364
  // already gone
387
365
  }
366
+ return true;
388
367
  }
389
368
  has(id) {
390
369
  return this.terminals.has(id);
@@ -442,28 +421,6 @@ export class TerminalManager {
442
421
  for (const id of [...this.terminals.keys()])
443
422
  this.close(id);
444
423
  }
445
- /** Kill every terminal and wait for node-pty to release its child handles. */
446
- async disposeAllAndWait(timeoutMs = 2000) {
447
- const exits = [];
448
- for (const [id, entry] of [...this.terminals]) {
449
- this.closeEntry(id, entry);
450
- exits.push(waitForExit(entry.exitPromise, timeoutMs));
451
- }
452
- await Promise.all(exits);
453
- }
454
- }
455
- async function waitForExit(exitPromise, timeoutMs) {
456
- let timer;
457
- try {
458
- await Promise.race([
459
- exitPromise,
460
- new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); }),
461
- ]);
462
- }
463
- finally {
464
- if (timer)
465
- clearTimeout(timer);
466
- }
467
424
  }
468
425
  function clampDim(value, fallback) {
469
426
  const n = Math.floor(Number(value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.16.18-staging.13",
3
+ "version": "0.16.18-staging.2",
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.",
@@ -1,21 +0,0 @@
1
- // SPDX-License-Identifier: AGPL-3.0-only
2
- // Wire format mirrored in src/automation-template.ts (node has no core dependency).
3
- const PREFIX = "bivy-automation-v1\n";
4
- /** Account labels, never credentials, travel inside the encrypted template. */
5
- export function encodeAutomationTemplate(instructions, credentialLabels) {
6
- return Object.keys(credentialLabels).length || instructions.startsWith(PREFIX)
7
- ? PREFIX + JSON.stringify({ instructions, credentialLabels })
8
- : instructions;
9
- }
10
- /** Legacy plaintext templates remain valid. Malformed structured templates fail closed. */
11
- export function decodeAutomationTemplate(value) {
12
- if (!value.startsWith(PREFIX))
13
- return { instructions: value, credentialLabels: {} };
14
- const data = JSON.parse(value.slice(PREFIX.length));
15
- if (!data || typeof data.instructions !== "string" || !data.credentialLabels ||
16
- typeof data.credentialLabels !== "object" || Array.isArray(data.credentialLabels) ||
17
- Object.entries(data.credentialLabels).some(([provider, label]) => !provider.trim() || provider !== provider.trim().toLowerCase() || typeof label !== "string" || !label.trim())) {
18
- throw new Error("Invalid automation account selections");
19
- }
20
- return { instructions: data.instructions, credentialLabels: data.credentialLabels };
21
- }
@@ -1,44 +0,0 @@
1
- import { defaultPresetsPath, loadPresets } from "./presets.js";
2
- import { selectCredential } from "./selection.js";
3
- import { CredentialSelectionError } from "./session.js";
4
- /** A provider-addressed view of the vault for consumers without labeled accounts.
5
- * Resolve on every operation; never copy a work credential into the default slot.
6
- * Refresh writes stay attached to the selected record's label and metadata.
7
- */
8
- export function selectedCredentialStore(store, credsDir, context) {
9
- const selectRecord = (provider, records, presets) => {
10
- const record = selectCredential(provider, records, presets, context)?.record;
11
- const label = context?.credentialLabels?.[provider];
12
- if (label && (!record || record.source.kind !== "stored")) {
13
- throw new CredentialSelectionError(`Selected account “${label}” for ${provider} is unavailable on this machine`);
14
- }
15
- return record;
16
- };
17
- const select = async (provider) => selectRecord(provider, await store.listRecords(), loadPresets(defaultPresetsPath(credsDir)));
18
- return {
19
- async read(provider) {
20
- const record = await select(provider);
21
- if (record?.source.kind !== "stored")
22
- return undefined;
23
- const { updatedAt: _updatedAt, ...credential } = record.source.cred;
24
- return credential;
25
- },
26
- async list() {
27
- const records = await store.listRecords();
28
- const presets = loadPresets(defaultPresetsPath(credsDir));
29
- return [...new Set([...records.map((r) => r.provider), ...Object.keys(context?.credentialLabels ?? {})])].flatMap((providerId) => {
30
- const record = selectRecord(providerId, records, presets);
31
- if (record?.source.kind !== "stored")
32
- return [];
33
- const credential = record.source.cred;
34
- return [{ providerId, type: credential.type, ...(credential.type === "oauth" ? { expiresAt: credential.expires } : {}) }];
35
- });
36
- },
37
- async modify(provider, fn) {
38
- const record = await select(provider);
39
- if (!record)
40
- throw new Error(`No account selected for ${provider}`);
41
- return store.modifyRecord(provider, record.label, fn);
42
- },
43
- };
44
- }
@@ -1,25 +0,0 @@
1
- // SPDX-License-Identifier: AGPL-3.0-only
2
- import path from "node:path";
3
- import { resolveCredential } from "./records.js";
4
- /** Stable project identifiers discoverable without importing repo/session code. */
5
- export function projectIdsFromWorkspace(workspace) {
6
- const resolved = path.resolve(workspace);
7
- const ids = new Set([resolved, path.basename(resolved)]);
8
- for (const part of resolved.split(path.sep)) {
9
- const split = part.indexOf("__");
10
- if (split > 0 && split < part.length - 2)
11
- ids.add(`${part.slice(0, split)}/${part.slice(split + 2)}`);
12
- }
13
- return [...ids];
14
- }
15
- export function selectCredential(provider, records, presets, context) {
16
- const id = provider.trim().toLowerCase();
17
- const workspace = context?.workspace?.trim();
18
- const projects = [context?.project?.trim(), ...(workspace ? projectIdsFromWorkspace(workspace) : [])].filter(Boolean);
19
- const projectPreset = projects.map((value) => `project:${value}`).find((name) => presets.presets?.[name]?.[id]);
20
- const preferLabel = context?.credentialLabels?.[id] ?? context?.preferLabel;
21
- return resolveCredential(id, records, presets, {
22
- ...(projectPreset ? { preset: projectPreset } : {}),
23
- ...(preferLabel ? { preferLabel } : {}),
24
- });
25
- }
@@ -1,48 +0,0 @@
1
- export class CredentialSelectionError extends Error {
2
- }
3
- /** Preserve historical best-effort auth only when no explicit account failed. */
4
- export function credentialEnvFallback(error) {
5
- if (error instanceof CredentialSelectionError)
6
- throw error;
7
- return {};
8
- }
9
- /** Bind a private provider→label map, without changing shared vault assignments. */
10
- export async function withSessionCredentials(options, labels) {
11
- if (!labels || !Object.keys(labels).length)
12
- return options;
13
- const store = options.credentials;
14
- if (!store)
15
- throw new CredentialSelectionError("This agent manages its own login and cannot use automation account overrides");
16
- const selected = { ...labels };
17
- const credentials = {
18
- listConfigured: async () => [...new Set([...(await store.listConfigured?.().catch(() => []) ?? []), ...Object.keys(selected)])],
19
- async getCredential(provider, context) {
20
- const label = selected[provider.trim().toLowerCase()];
21
- let credential;
22
- try {
23
- credential = await store.getCredential(provider, { ...context, ...(label ? { preferLabel: label } : {}) });
24
- }
25
- catch (error) {
26
- if (label)
27
- throw new CredentialSelectionError(`Could not read selected account “${label}” for ${provider}`);
28
- throw error;
29
- }
30
- if (label && !credential)
31
- throw new CredentialSelectionError(`Selected account “${label}” for ${provider} is unavailable on this machine`);
32
- if (label && credential?.kind === "oauth" && credential.provider !== "anthropic") {
33
- throw new CredentialSelectionError(`This agent cannot use a ${provider} subscription override; use an API key or a Bivy-managed model agent`);
34
- }
35
- // Do not let an inherited alternative Anthropic login outrank the pin.
36
- if (label && credential?.provider === "anthropic")
37
- return {
38
- ...credential,
39
- env: { ...credential.env, [credential.kind === "oauth" ? "ANTHROPIC_API_KEY" : "CLAUDE_CODE_OAUTH_TOKEN"]: "" },
40
- };
41
- return credential;
42
- },
43
- };
44
- // Fail before starting a subprocess, including overrides for a second provider.
45
- for (const provider of Object.keys(selected))
46
- await credentials.getCredential(provider);
47
- return { ...options, credentials };
48
- }