@bivy/bivy 0.16.18-staging.13 → 0.16.18-staging.3
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/dist/agents/claude-code/runtime.js +8 -9
- package/dist/agents/pi/runtime.js +2 -4
- package/dist/credentials/resolver.js +22 -14
- package/dist/runtime/agent-service.js +2 -2
- package/dist/runtime/pi-oauth.js +1 -5
- package/dist/runtime/process.js +3 -4
- package/dist/runtime/protocol.js +5 -6
- package/dist/runtime/remote.js +2 -2
- package/dist/server.js +7 -27
- package/dist/session/run-terminal.js +3 -5
- package/dist/terminal.js +2 -45
- package/package.json +1 -1
- package/dist/automation-template.js +0 -21
- package/dist/credentials/selected-store.js +0 -44
- package/dist/credentials/selection.js +0 -25
- package/dist/credentials/session.js +0 -48
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
|
1044
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
139
|
-
: await runtime.createSession({ workspace,
|
|
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);
|
package/dist/runtime/pi-oauth.js
CHANGED
|
@@ -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(
|
|
29
|
+
credentials: piCredentialStore(store),
|
|
34
30
|
modelsPath: path.join(opts.piDir, "models.json"),
|
|
35
31
|
allowModelNetwork: opts.allowModelNetwork ?? false,
|
|
36
32
|
});
|
package/dist/runtime/process.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
541
|
+
const session = new ProcessSession(this.options, options.workspace, options.sessionFile);
|
|
543
542
|
this.sessions.push(session);
|
|
544
543
|
return { session };
|
|
545
544
|
}
|
package/dist/runtime/protocol.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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) {
|
package/dist/runtime/remote.js
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = {
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
355
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
-
|
|
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,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
|
-
}
|