@agents-ensemble/core 0.8.0 → 0.9.1
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/conductor/conductor-pi-auth.d.ts +5 -5
- package/dist/conductor/conductor-pi-auth.d.ts.map +1 -1
- package/dist/conductor/conductor-pi-auth.js +261 -80
- package/dist/conductor/conductor-pi-auth.js.map +1 -1
- package/dist/conductor/conductor-session.d.ts.map +1 -1
- package/dist/conductor/conductor-session.js +8 -3
- package/dist/conductor/conductor-session.js.map +1 -1
- package/dist/conductor/conductor-tool-pi-adapter.d.ts +6 -8
- package/dist/conductor/conductor-tool-pi-adapter.d.ts.map +1 -1
- package/dist/conductor/conductor-tool-pi-adapter.js +5 -8
- package/dist/conductor/conductor-tool-pi-adapter.js.map +1 -1
- package/dist/conductor/index.d.ts +1 -4
- package/dist/conductor/index.d.ts.map +1 -1
- package/dist/conductor/index.js +1 -2
- package/dist/conductor/index.js.map +1 -1
- package/dist/conductor/pi-conductor-agent.d.ts +22 -6
- package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
- package/dist/conductor/pi-conductor-agent.js +296 -101
- package/dist/conductor/pi-conductor-agent.js.map +1 -1
- package/dist/conductor/pi-resource-loader.d.ts +5 -5
- package/dist/conductor/pi-resource-loader.js +5 -5
- package/dist/mcp/load-mcp-config.d.ts +8 -0
- package/dist/mcp/load-mcp-config.d.ts.map +1 -1
- package/dist/mcp/load-mcp-config.js +24 -0
- package/dist/mcp/load-mcp-config.js.map +1 -1
- package/dist/session/session-sidecar.d.ts +6 -0
- package/dist/session/session-sidecar.d.ts.map +1 -1
- package/dist/session/session-sidecar.js +17 -0
- package/dist/session/session-sidecar.js.map +1 -1
- package/package.json +4 -5
- package/dist/conductor/pi-conductor-session.d.ts +0 -20
- package/dist/conductor/pi-conductor-session.d.ts.map +0 -1
- package/dist/conductor/pi-conductor-session.js +0 -60
- package/dist/conductor/pi-conductor-session.js.map +0 -1
- package/dist/conductor/pi-mcp-bridge.d.ts +0 -65
- package/dist/conductor/pi-mcp-bridge.d.ts.map +0 -1
- package/dist/conductor/pi-mcp-bridge.js +0 -259
- package/dist/conductor/pi-mcp-bridge.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ModelRegistry, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { type OAuthLoginCallbacks } from '@earendil-works/pi-ai/compat';
|
|
3
3
|
import { type PiResourceLoaderOptions, type PiResources } from './pi-resource-loader.js';
|
|
4
4
|
/** Auth and model services for the Pi conductor's user resource layer. */
|
|
@@ -6,7 +6,7 @@ export interface PiConductorAuthContext {
|
|
|
6
6
|
agentDir: string;
|
|
7
7
|
authPath: string;
|
|
8
8
|
modelsPath: string;
|
|
9
|
-
|
|
9
|
+
modelRuntime: ModelRuntime;
|
|
10
10
|
modelRegistry: ModelRegistry;
|
|
11
11
|
}
|
|
12
12
|
export interface PiProviderAuthStatus {
|
|
@@ -41,8 +41,8 @@ export interface PiConductorAuthOptions extends PiResourceLoaderOptions {
|
|
|
41
41
|
provider?: string;
|
|
42
42
|
modelId?: string;
|
|
43
43
|
}
|
|
44
|
-
/** Create the Pi
|
|
45
|
-
export declare function createPiConductorAuthContext(options: PiResourceLoaderOptions): PiConductorAuthContext
|
|
44
|
+
/** Create the Pi 1.x model runtime and registry for the resolved resource roots. */
|
|
45
|
+
export declare function createPiConductorAuthContext(options: PiResourceLoaderOptions, resources?: PiResources): Promise<PiConductorAuthContext>;
|
|
46
46
|
/** Resolve the provider used by `ensemble auth` from an explicit/default model. */
|
|
47
47
|
export declare function resolvePiConductorProvider(options: PiConductorAuthOptions): Promise<string>;
|
|
48
48
|
/** Login to a Pi provider. API-key login persists only in the user auth file. */
|
|
@@ -60,7 +60,7 @@ export declare function getPiConductorAuthStatus(options: PiConductorAuthOptions
|
|
|
60
60
|
export declare function listPiConductorModels(options?: PiConductorAuthOptions): Promise<PiConductorModelListEntry[]>;
|
|
61
61
|
/** Resolve a key while retaining project-over-user auth precedence. */
|
|
62
62
|
export declare function resolvePiConductorApiKey(options: {
|
|
63
|
-
|
|
63
|
+
modelRuntime: ModelRuntime;
|
|
64
64
|
resources: Pick<PiResources, 'roots' | 'authLayers' | 'settings' | 'models'>;
|
|
65
65
|
provider: string;
|
|
66
66
|
explicitApiKey?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conductor-pi-auth.d.ts","sourceRoot":"","sources":["../../src/conductor/conductor-pi-auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"conductor-pi-auth.d.ts","sourceRoot":"","sources":["../../src/conductor/conductor-pi-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { ModelRegistry, ModelRuntime, } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { findEnvKeys, getEnvApiKey, getModels, getProviders, } from '@earendil-works/pi-ai/compat';
|
|
5
6
|
import { loadPiResources, resolvePiResourceRoots, } from './pi-resource-loader.js';
|
|
6
|
-
/** Create the Pi
|
|
7
|
-
export function createPiConductorAuthContext(options) {
|
|
7
|
+
/** Create the Pi 1.x model runtime and registry for the resolved resource roots. */
|
|
8
|
+
export async function createPiConductorAuthContext(options, resources) {
|
|
8
9
|
const roots = resolvePiResourceRoots(options);
|
|
9
10
|
const authPath = join(roots.agentDir, 'auth.json');
|
|
10
11
|
const modelsPath = join(roots.agentDir, 'models.json');
|
|
11
|
-
const
|
|
12
|
+
const resolvedResources = resources ?? (await loadPiResources(options));
|
|
13
|
+
const modelRuntime = await createPiModelRuntime(options, resolvedResources);
|
|
12
14
|
return {
|
|
13
15
|
agentDir: roots.agentDir,
|
|
14
16
|
authPath,
|
|
15
17
|
modelsPath,
|
|
16
|
-
|
|
17
|
-
modelRegistry: ModelRegistry
|
|
18
|
+
modelRuntime,
|
|
19
|
+
modelRegistry: new ModelRegistry(modelRuntime),
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
/** Resolve the provider used by `ensemble auth` from an explicit/default model. */
|
|
21
23
|
export async function resolvePiConductorProvider(options) {
|
|
22
24
|
const resources = await loadPiResources(options);
|
|
23
|
-
const context = createPiConductorAuthContext(options);
|
|
25
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
24
26
|
return resolvePiConductorProviderFromResources(options, resources, context);
|
|
25
27
|
}
|
|
26
28
|
/** Login to a Pi provider. API-key login persists only in the user auth file. */
|
|
27
29
|
export async function loginPiConductor(options) {
|
|
28
30
|
const resources = await loadPiResources(options);
|
|
29
|
-
const context = createPiConductorAuthContext(options);
|
|
30
|
-
const provider = resolveConductorProviderOrThrow(options, resources, context);
|
|
31
|
-
const oauthProvider = context.
|
|
32
|
-
.getOAuthProviders()
|
|
33
|
-
.some((candidate) => candidate.id === provider);
|
|
31
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
32
|
+
const provider = await resolveConductorProviderOrThrow(options, resources, context);
|
|
33
|
+
const oauthProvider = Boolean(context.modelRuntime.getProvider(provider)?.auth.oauth);
|
|
34
34
|
if (oauthProvider) {
|
|
35
35
|
if (!options.oauthCallbacks) {
|
|
36
36
|
throw new Error(`Pi provider "${provider}" uses OAuth. Run this command from a TTY so the OAuth login flow can interact with you.`);
|
|
37
37
|
}
|
|
38
|
-
await context.
|
|
38
|
+
await context.modelRuntime.login(provider, 'oauth', toPiAuthInteraction(options.oauthCallbacks));
|
|
39
39
|
return {
|
|
40
40
|
backend: 'pi',
|
|
41
41
|
provider,
|
|
@@ -47,7 +47,8 @@ export async function loginPiConductor(options) {
|
|
|
47
47
|
if (!apiKey) {
|
|
48
48
|
throw new Error(`Pi provider "${provider}" requires an API key. Pass a secret API-key prompt value or set ${formatPiProviderEnvHint(provider)}.`);
|
|
49
49
|
}
|
|
50
|
-
context.
|
|
50
|
+
await writePiApiKey(context.authPath, provider, apiKey);
|
|
51
|
+
await context.modelRuntime.setRuntimeApiKey(provider, apiKey);
|
|
51
52
|
return {
|
|
52
53
|
backend: 'pi',
|
|
53
54
|
provider,
|
|
@@ -58,18 +59,16 @@ export async function loginPiConductor(options) {
|
|
|
58
59
|
/** Whether the selected Pi provider exposes Pi's native OAuth login flow. */
|
|
59
60
|
export async function isPiConductorOAuthProvider(options) {
|
|
60
61
|
const resources = await loadPiResources(options);
|
|
61
|
-
const context = createPiConductorAuthContext(options);
|
|
62
|
-
const provider = resolveConductorProviderOrThrow(options, resources, context);
|
|
63
|
-
return context.
|
|
64
|
-
.getOAuthProviders()
|
|
65
|
-
.some((candidate) => candidate.id === provider);
|
|
62
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
63
|
+
const provider = await resolveConductorProviderOrThrow(options, resources, context);
|
|
64
|
+
return Boolean(context.modelRuntime.getProvider(provider)?.auth.oauth);
|
|
66
65
|
}
|
|
67
66
|
/** Remove a Pi provider's user-layer credential. */
|
|
68
67
|
export async function logoutPiConductor(options) {
|
|
69
68
|
const resources = await loadPiResources(options);
|
|
70
|
-
const context = createPiConductorAuthContext(options);
|
|
71
|
-
const provider = resolveConductorProviderOrThrow(options, resources, context);
|
|
72
|
-
context.
|
|
69
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
70
|
+
const provider = await resolveConductorProviderOrThrow(options, resources, context);
|
|
71
|
+
await context.modelRuntime.logout(provider);
|
|
73
72
|
return {
|
|
74
73
|
backend: 'pi',
|
|
75
74
|
provider,
|
|
@@ -79,8 +78,8 @@ export async function logoutPiConductor(options) {
|
|
|
79
78
|
/** Return non-secret Pi auth metadata for the selected/default provider. */
|
|
80
79
|
export async function getPiConductorAuthStatus(options) {
|
|
81
80
|
const resources = await loadPiResources(options);
|
|
82
|
-
const context = createPiConductorAuthContext(options);
|
|
83
|
-
const providers = resolveStatusProviders(options, resources, context);
|
|
81
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
82
|
+
const providers = await resolveStatusProviders(options, resources, context);
|
|
84
83
|
return {
|
|
85
84
|
backend: 'pi',
|
|
86
85
|
authPath: context.authPath,
|
|
@@ -90,12 +89,11 @@ export async function getPiConductorAuthStatus(options) {
|
|
|
90
89
|
/** List only Pi models whose provider has usable configured auth. */
|
|
91
90
|
export async function listPiConductorModels(options = { cwd: process.cwd() }) {
|
|
92
91
|
const resources = await loadPiResources(options);
|
|
93
|
-
const context = createPiConductorAuthContext(options);
|
|
92
|
+
const context = await createPiConductorAuthContext(options, resources);
|
|
94
93
|
const requestedProvider = normalizeProvider(options.provider) ?? providerFromModelId(options.modelId);
|
|
95
94
|
const entries = new Map();
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// through the merged project-over-user auth view before listing models.
|
|
95
|
+
// Filter the complete Pi 1.x runtime catalog through the merged
|
|
96
|
+
// project-over-user auth view before listing models.
|
|
99
97
|
for (const model of context.modelRegistry.getAll()) {
|
|
100
98
|
if (requestedProvider && model.provider !== requestedProvider)
|
|
101
99
|
continue;
|
|
@@ -104,18 +102,8 @@ export async function listPiConductorModels(options = { cwd: process.cwd() }) {
|
|
|
104
102
|
}
|
|
105
103
|
addPiModelEntry(entries, model);
|
|
106
104
|
}
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
for (const [provider, config] of Object.entries(resources.models.providers)) {
|
|
110
|
-
if (requestedProvider && provider !== requestedProvider)
|
|
111
|
-
continue;
|
|
112
|
-
if (!hasPiProviderAuth(provider, context, resources, options.env ?? process.env)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
for (const model of config.models ?? []) {
|
|
116
|
-
addPiCustomModelEntry(entries, provider, model);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
105
|
+
// The runtime is built from the merged models resource, so custom models are
|
|
106
|
+
// already included in the registry above.
|
|
119
107
|
const result = [...entries.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
120
108
|
if (result.length === 0) {
|
|
121
109
|
const providerHint = requestedProvider ? ` for provider "${requestedProvider}"` : '';
|
|
@@ -131,27 +119,56 @@ export async function resolvePiConductorApiKey(options) {
|
|
|
131
119
|
if (options.explicitApiKey !== undefined)
|
|
132
120
|
return options.explicitApiKey;
|
|
133
121
|
// A project auth file intentionally remains above the user auth layer. When
|
|
134
|
-
// both roots point at the same file,
|
|
122
|
+
// both roots point at the same file, ModelRuntime is the canonical reader so
|
|
135
123
|
// OAuth refresh is not bypassed by the resource-layer compatibility parser.
|
|
124
|
+
const projectEntry = options.resources.authLayers.at(-1)?.[options.provider];
|
|
125
|
+
const projectKey = options.resources.roots.agentDir !== options.resources.roots.projectDir
|
|
126
|
+
? resolvePiAuthEntry(projectEntry, env)
|
|
127
|
+
: undefined;
|
|
128
|
+
const settingsKey = resolvePiSettingsApiKey(options.resources.settings, options.provider, env);
|
|
129
|
+
const modelsKey = resolvePiModelsApiKey(options.resources.models.providers[options.provider], env);
|
|
136
130
|
if (options.resources.roots.agentDir !== options.resources.roots.projectDir) {
|
|
137
|
-
const projectEntry = options.resources.authLayers.at(-1)?.[options.provider];
|
|
138
131
|
if (isPiOAuthAuthEntry(projectEntry)) {
|
|
139
132
|
throw new Error(formatProjectOAuthAuthError(options.provider));
|
|
140
133
|
}
|
|
141
|
-
|
|
142
|
-
if (projectKey !== undefined)
|
|
134
|
+
if (projectKey !== undefined && !isPiCommandConfigValue(projectKey)) {
|
|
143
135
|
return projectKey;
|
|
136
|
+
}
|
|
137
|
+
if (projectKey !== undefined) {
|
|
138
|
+
// A project auth entry intentionally outranks the user credential. Use
|
|
139
|
+
// the Pi resolver through an isolated provider id so ModelRuntime does
|
|
140
|
+
// not select the lower-precedence user auth.json entry first.
|
|
141
|
+
return resolvePiProjectCommandApiKey(options.modelRuntime, options.provider, projectKey);
|
|
142
|
+
}
|
|
144
143
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
// Pi resolves command references from the provider configuration at request
|
|
145
|
+
// time. Settings/auth are loaded by ensemble rather than Pi's resource
|
|
146
|
+
// loader, so register command references through the same public runtime API
|
|
147
|
+
// before asking the runtime for the request credential. Project auth keeps
|
|
148
|
+
// its higher precedence over settings and models.
|
|
149
|
+
if (settingsKey !== undefined && isPiCommandConfigValue(settingsKey)) {
|
|
150
|
+
options.modelRuntime.registerProvider(options.provider, { apiKey: settingsKey });
|
|
151
|
+
}
|
|
152
|
+
// ModelRuntime owns the Pi 1.x credential store, OAuth refresh, and locking.
|
|
153
|
+
// Resolve it on every request instead of capturing the key at construction.
|
|
154
|
+
let runtimeError;
|
|
155
|
+
try {
|
|
156
|
+
const runtimeAuth = await options.modelRuntime.getAuth(options.provider);
|
|
157
|
+
if (runtimeAuth?.auth.apiKey !== undefined)
|
|
158
|
+
return runtimeAuth.auth.apiKey;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
runtimeError = error;
|
|
162
|
+
// Non-command references can still use the ensemble fallback chain. A
|
|
163
|
+
// command reference is reported below instead of returning the raw command
|
|
164
|
+
// as an API key.
|
|
165
|
+
}
|
|
166
|
+
const commandKey = [settingsKey, modelsKey].find((value) => value !== undefined && isPiCommandConfigValue(value));
|
|
167
|
+
if (commandKey !== undefined) {
|
|
168
|
+
throw formatPiCommandResolutionError(options.provider, commandKey, runtimeError);
|
|
150
169
|
}
|
|
151
|
-
const settingsKey = resolvePiSettingsApiKey(options.resources.settings, options.provider, env);
|
|
152
170
|
if (settingsKey !== undefined)
|
|
153
171
|
return settingsKey;
|
|
154
|
-
const modelsKey = resolvePiModelsApiKey(options.resources.models.providers[options.provider], env);
|
|
155
172
|
if (modelsKey !== undefined)
|
|
156
173
|
return modelsKey;
|
|
157
174
|
return getEnvApiKey(options.provider, toProviderEnv(env));
|
|
@@ -163,18 +180,18 @@ export function hasPiProviderAuth(provider, context, resources, env = process.en
|
|
|
163
180
|
}
|
|
164
181
|
/** Synchronous readiness check used by CLI/e2e guards before async loading. */
|
|
165
182
|
export function hasPiConductorAuth(options) {
|
|
166
|
-
const context = createPiConductorAuthContext(options);
|
|
167
183
|
const env = options.env ?? process.env;
|
|
168
184
|
const roots = resolvePiResourceRoots(options);
|
|
185
|
+
const userAuth = readPiJsonSync(join(roots.agentDir, 'auth.json'));
|
|
169
186
|
const projectAuth = readPiJsonSync(join(roots.projectDir, 'auth.json'));
|
|
170
187
|
const settings = readPiSettingsSync(roots);
|
|
171
188
|
const models = readPiModelsSync(roots);
|
|
172
|
-
const selection = resolvePiConductorSelectionSync(options, settings, models,
|
|
189
|
+
const selection = resolvePiConductorSelectionSync(options, settings, models, userAuth);
|
|
173
190
|
if (selection.provider) {
|
|
174
191
|
return hasPiProviderAuthSync({
|
|
175
192
|
provider: selection.provider,
|
|
176
|
-
context,
|
|
177
193
|
roots,
|
|
194
|
+
userAuth,
|
|
178
195
|
projectAuth,
|
|
179
196
|
settings,
|
|
180
197
|
models,
|
|
@@ -187,8 +204,8 @@ export function hasPiConductorAuth(options) {
|
|
|
187
204
|
if (selection.modelId)
|
|
188
205
|
return false;
|
|
189
206
|
return hasAnyPiConductorAuthSync({
|
|
190
|
-
context,
|
|
191
207
|
roots,
|
|
208
|
+
userAuth,
|
|
192
209
|
projectAuth,
|
|
193
210
|
settings,
|
|
194
211
|
models,
|
|
@@ -199,7 +216,7 @@ export function hasPiConductorAuth(options) {
|
|
|
199
216
|
* Mirror PiConductorAgent's synchronous provider/model selection for guards.
|
|
200
217
|
* The async agent receives the same merged settings and model layers.
|
|
201
218
|
*/
|
|
202
|
-
function resolvePiConductorSelectionSync(options, settings, models,
|
|
219
|
+
function resolvePiConductorSelectionSync(options, settings, models, userAuth) {
|
|
203
220
|
const requestedModel = normalizeModelId(options.modelId);
|
|
204
221
|
const configuredModel = normalizeModelId(firstString(settings.defaultModel, settings.model, settings.modelId));
|
|
205
222
|
const selectedModel = requestedModel ?? configuredModel;
|
|
@@ -216,7 +233,7 @@ function resolvePiConductorSelectionSync(options, settings, models, context) {
|
|
|
216
233
|
provider = normalizeProvider(settings.defaultProvider);
|
|
217
234
|
if (!provider && modelId) {
|
|
218
235
|
const matches = new Set();
|
|
219
|
-
for (const model of
|
|
236
|
+
for (const model of knownPiModelsSync(models, userAuth)) {
|
|
220
237
|
if (model.id === modelId)
|
|
221
238
|
matches.add(model.provider);
|
|
222
239
|
}
|
|
@@ -241,14 +258,7 @@ function hasPiProviderAuthSync(options) {
|
|
|
241
258
|
}
|
|
242
259
|
if (resolvePiAuthEntry(projectEntry, options.env) !== undefined)
|
|
243
260
|
return true;
|
|
244
|
-
if (options.
|
|
245
|
-
return true;
|
|
246
|
-
// Keep readiness aligned with ModelRegistry, including command-backed or
|
|
247
|
-
// otherwise registry-native user models, while the explicit checks below
|
|
248
|
-
// honor the injected test/runtime environment.
|
|
249
|
-
if (options.context.modelRegistry
|
|
250
|
-
.getAvailable()
|
|
251
|
-
.some((model) => model.provider === options.provider)) {
|
|
261
|
+
if (resolvePiAuthEntry(options.userAuth?.[options.provider], options.env) !== undefined) {
|
|
252
262
|
return true;
|
|
253
263
|
}
|
|
254
264
|
if (resolvePiSettingsApiKey(options.settings, options.provider, options.env) !== undefined) {
|
|
@@ -260,14 +270,12 @@ function hasPiProviderAuthSync(options) {
|
|
|
260
270
|
return getEnvApiKey(options.provider, toProviderEnv(options.env)) !== undefined;
|
|
261
271
|
}
|
|
262
272
|
function hasAnyPiConductorAuthSync(options) {
|
|
263
|
-
if (options.context.modelRegistry.getAvailable().length > 0)
|
|
264
|
-
return true;
|
|
265
273
|
const providers = new Set([
|
|
266
|
-
...options.
|
|
267
|
-
...options.
|
|
274
|
+
...Object.keys(options.userAuth ?? {}),
|
|
275
|
+
...knownPiModelsSync(options.models, options.userAuth).map((model) => model.provider),
|
|
268
276
|
...Object.keys(options.models.providers),
|
|
269
277
|
...Object.keys(options.projectAuth ?? {}),
|
|
270
|
-
...
|
|
278
|
+
...getProviders(),
|
|
271
279
|
]);
|
|
272
280
|
for (const provider of providers) {
|
|
273
281
|
if (hasPiProviderAuthSync({ ...options, provider }))
|
|
@@ -291,7 +299,7 @@ function readPiModelsSync(roots) {
|
|
|
291
299
|
: {},
|
|
292
300
|
};
|
|
293
301
|
}
|
|
294
|
-
function resolveConductorProviderOrThrow(options, resources, context) {
|
|
302
|
+
async function resolveConductorProviderOrThrow(options, resources, context) {
|
|
295
303
|
const provider = resolvePiConductorProviderFromResources(options, resources, context);
|
|
296
304
|
if (provider)
|
|
297
305
|
return provider;
|
|
@@ -326,7 +334,7 @@ function resolvePiConductorProviderFromResources(options, resources, context) {
|
|
|
326
334
|
}
|
|
327
335
|
return '';
|
|
328
336
|
}
|
|
329
|
-
function resolveStatusProviders(options, resources, context) {
|
|
337
|
+
async function resolveStatusProviders(options, resources, context) {
|
|
330
338
|
const explicit = normalizeProvider(options.provider);
|
|
331
339
|
if (explicit)
|
|
332
340
|
return [explicit];
|
|
@@ -334,10 +342,9 @@ function resolveStatusProviders(options, resources, context) {
|
|
|
334
342
|
if (configured)
|
|
335
343
|
return [configured];
|
|
336
344
|
const providers = new Set([
|
|
337
|
-
...context.
|
|
345
|
+
...(await context.modelRuntime.listCredentials()).map((entry) => entry.providerId),
|
|
338
346
|
...context.modelRegistry.getAll().map((model) => model.provider),
|
|
339
347
|
...Object.keys(resources.models.providers),
|
|
340
|
-
...context.authStorage.getOAuthProviders().map((provider) => provider.id),
|
|
341
348
|
]);
|
|
342
349
|
return [...providers].sort();
|
|
343
350
|
}
|
|
@@ -356,7 +363,7 @@ function getPiProviderAuthStatus(provider, context, resources, env) {
|
|
|
356
363
|
return { provider, configured: true, source: 'project_stored' };
|
|
357
364
|
}
|
|
358
365
|
}
|
|
359
|
-
const registryStatus = context.
|
|
366
|
+
const registryStatus = context.modelRuntime.getProviderAuthStatus(provider);
|
|
360
367
|
if (registryStatus.configured || registryStatus.source === 'stored') {
|
|
361
368
|
return normalizePiAuthStatus(provider, registryStatus, true);
|
|
362
369
|
}
|
|
@@ -423,7 +430,7 @@ function isPiOAuthAuthEntry(entry) {
|
|
|
423
430
|
function formatProjectOAuthAuthError(provider) {
|
|
424
431
|
return (`Project Pi OAuth credentials for provider "${provider}" are not supported in ` +
|
|
425
432
|
'project .ensemble/pi/auth.json because that layer cannot refresh them safely. ' +
|
|
426
|
-
`Run ensemble auth login --provider ${provider} to store the credential in the user
|
|
433
|
+
`Run ensemble auth login --provider ${provider} to store the credential in the user Pi runtime.`);
|
|
427
434
|
}
|
|
428
435
|
function resolvePiSettingsApiKey(settings, provider, env) {
|
|
429
436
|
if (typeof settings.apiKey === 'string') {
|
|
@@ -440,14 +447,188 @@ function resolvePiModelsApiKey(providerConfig, env) {
|
|
|
440
447
|
: undefined;
|
|
441
448
|
}
|
|
442
449
|
function expandEnvReference(value, env) {
|
|
443
|
-
|
|
444
|
-
|
|
450
|
+
// Keep Pi command references opaque here. ModelRuntime's public provider
|
|
451
|
+
// registration path resolves `!command` with Pi's resolver at request time.
|
|
445
452
|
const variable = /^\$\{([^}]+)\}$/.exec(value) ?? /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
|
|
446
453
|
return variable ? env[variable[1]] : value;
|
|
447
454
|
}
|
|
455
|
+
function isPiCommandConfigValue(value) {
|
|
456
|
+
return value.startsWith('!');
|
|
457
|
+
}
|
|
458
|
+
function formatPiCommandResolutionError(provider, command, cause) {
|
|
459
|
+
const causeMessage = cause instanceof Error ? ` (${cause.message})` : '';
|
|
460
|
+
return new Error(`Failed to resolve API key for Pi provider "${provider}" from shell command: ${command.slice(1)}${causeMessage}`, cause instanceof Error ? { cause } : undefined);
|
|
461
|
+
}
|
|
462
|
+
async function resolvePiProjectCommandApiKey(modelRuntime, provider, command) {
|
|
463
|
+
const isolatedProvider = `__agents_ensemble_project_auth__${provider}`;
|
|
464
|
+
modelRuntime.registerProvider(isolatedProvider, { apiKey: command });
|
|
465
|
+
try {
|
|
466
|
+
const runtimeAuth = await modelRuntime.getAuth(isolatedProvider);
|
|
467
|
+
if (runtimeAuth?.auth.apiKey !== undefined) {
|
|
468
|
+
// Keep the resolved project credential as an in-memory runtime override
|
|
469
|
+
// so the actual conductor request also uses project-over-user auth.
|
|
470
|
+
await modelRuntime.setRuntimeApiKey(provider, runtimeAuth.auth.apiKey);
|
|
471
|
+
return runtimeAuth.auth.apiKey;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
throw formatPiCommandResolutionError(provider, command, error);
|
|
476
|
+
}
|
|
477
|
+
throw formatPiCommandResolutionError(provider, command, undefined);
|
|
478
|
+
}
|
|
448
479
|
function toProviderEnv(env) {
|
|
449
480
|
return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string'));
|
|
450
481
|
}
|
|
482
|
+
/** Build the Pi 1.x runtime from the already-resolved ensemble resource view. */
|
|
483
|
+
async function createPiModelRuntime(options, resources) {
|
|
484
|
+
const roots = resolvePiResourceRoots(options);
|
|
485
|
+
const runtime = await ModelRuntime.create({
|
|
486
|
+
authPath: join(roots.agentDir, 'auth.json'),
|
|
487
|
+
// The ensemble resource loader has already merged user and project models.
|
|
488
|
+
// Avoid letting ModelRuntime read an unmerged second view of models.json.
|
|
489
|
+
modelsPath: null,
|
|
490
|
+
allowModelNetwork: false,
|
|
491
|
+
refreshOnCreate: false,
|
|
492
|
+
});
|
|
493
|
+
const env = options.env ?? process.env;
|
|
494
|
+
for (const [provider, config] of Object.entries(resources.models.providers)) {
|
|
495
|
+
runtime.registerProvider(provider, toPiRuntimeProviderConfig(config, env));
|
|
496
|
+
}
|
|
497
|
+
if (roots.agentDir !== roots.projectDir) {
|
|
498
|
+
for (const [provider, entry] of Object.entries(resources.authLayers.at(-1) ?? {})) {
|
|
499
|
+
if (isPiOAuthAuthEntry(entry))
|
|
500
|
+
continue;
|
|
501
|
+
const key = resolvePiAuthEntry(entry, env);
|
|
502
|
+
if (key !== undefined && !isPiCommandConfigValue(key)) {
|
|
503
|
+
await runtime.setRuntimeApiKey(provider, key);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (isRecord(resources.settings.apiKeys)) {
|
|
508
|
+
for (const [provider, value] of Object.entries(resources.settings.apiKeys)) {
|
|
509
|
+
if (typeof value !== 'string')
|
|
510
|
+
continue;
|
|
511
|
+
const key = expandEnvReference(value, env);
|
|
512
|
+
if (key !== undefined && !isPiCommandConfigValue(key)) {
|
|
513
|
+
await runtime.setRuntimeApiKey(provider, key);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
await runtime.refresh({ allowNetwork: false });
|
|
518
|
+
return runtime;
|
|
519
|
+
}
|
|
520
|
+
function toPiRuntimeProviderConfig(config, env) {
|
|
521
|
+
const models = config.models?.map((model) => ({
|
|
522
|
+
type: 'chat',
|
|
523
|
+
id: model.id,
|
|
524
|
+
name: model.name ?? model.id,
|
|
525
|
+
...(model.api ?? config.api ? { api: model.api ?? config.api } : {}),
|
|
526
|
+
...(model.baseUrl ?? config.baseUrl
|
|
527
|
+
? { baseUrl: model.baseUrl ?? config.baseUrl }
|
|
528
|
+
: {}),
|
|
529
|
+
input: model.input ?? ['text'],
|
|
530
|
+
reasoning: model.reasoning ?? false,
|
|
531
|
+
...(model.thinkingLevelMap ? { thinkingLevelMap: model.thinkingLevelMap } : {}),
|
|
532
|
+
cost: model.cost ?? emptyPiModelCost(),
|
|
533
|
+
contextWindow: model.contextWindow ?? 128000,
|
|
534
|
+
maxTokens: model.maxTokens ?? 16384,
|
|
535
|
+
...(model.headers ? { headers: model.headers } : {}),
|
|
536
|
+
...(model.compat ? { compat: model.compat } : {}),
|
|
537
|
+
}));
|
|
538
|
+
return {
|
|
539
|
+
...(config.name ? { name: config.name } : {}),
|
|
540
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
541
|
+
...(config.apiKey
|
|
542
|
+
? { apiKey: expandEnvReference(config.apiKey, env) ?? undefined }
|
|
543
|
+
: {}),
|
|
544
|
+
...(config.api ? { api: config.api } : {}),
|
|
545
|
+
...(config.headers ? { headers: config.headers } : {}),
|
|
546
|
+
...(config.authHeader !== undefined ? { authHeader: config.authHeader } : {}),
|
|
547
|
+
...(models ? { models } : {}),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function writePiApiKey(authPath, provider, apiKey) {
|
|
551
|
+
await mkdir(dirname(authPath), { recursive: true });
|
|
552
|
+
let auth = {};
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(await readFile(authPath, 'utf8'));
|
|
555
|
+
if (isRecord(parsed))
|
|
556
|
+
auth = parsed;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
if (!isRecord(error) || error.code !== 'ENOENT')
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
auth[provider] = { type: 'api_key', key: apiKey };
|
|
563
|
+
await writeFile(authPath, `${JSON.stringify(auth, null, 2)}\n`, 'utf8');
|
|
564
|
+
}
|
|
565
|
+
function toPiAuthInteraction(callbacks) {
|
|
566
|
+
return {
|
|
567
|
+
signal: callbacks.signal,
|
|
568
|
+
prompt: async (prompt) => {
|
|
569
|
+
if (prompt.type === 'select') {
|
|
570
|
+
const selected = await callbacks.onSelect({
|
|
571
|
+
message: prompt.message,
|
|
572
|
+
options: prompt.options.map((option) => ({
|
|
573
|
+
id: option.id,
|
|
574
|
+
label: option.label,
|
|
575
|
+
})),
|
|
576
|
+
});
|
|
577
|
+
if (selected === undefined)
|
|
578
|
+
throw new Error('OAuth login was cancelled.');
|
|
579
|
+
return selected;
|
|
580
|
+
}
|
|
581
|
+
if (prompt.type === 'manual_code' && callbacks.onManualCodeInput) {
|
|
582
|
+
return callbacks.onManualCodeInput();
|
|
583
|
+
}
|
|
584
|
+
return callbacks.onPrompt({
|
|
585
|
+
message: prompt.message,
|
|
586
|
+
...(prompt.placeholder ? { placeholder: prompt.placeholder } : {}),
|
|
587
|
+
});
|
|
588
|
+
},
|
|
589
|
+
notify: (event) => {
|
|
590
|
+
if (event.type === 'auth_url') {
|
|
591
|
+
callbacks.onAuth({ url: event.url, instructions: event.instructions });
|
|
592
|
+
}
|
|
593
|
+
else if (event.type === 'device_code') {
|
|
594
|
+
callbacks.onDeviceCode({
|
|
595
|
+
userCode: event.userCode,
|
|
596
|
+
verificationUri: event.verificationUri,
|
|
597
|
+
...(event.intervalSeconds !== undefined
|
|
598
|
+
? { intervalSeconds: event.intervalSeconds }
|
|
599
|
+
: {}),
|
|
600
|
+
...(event.expiresInSeconds !== undefined
|
|
601
|
+
? { expiresInSeconds: event.expiresInSeconds }
|
|
602
|
+
: {}),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
else if (callbacks.onProgress) {
|
|
606
|
+
callbacks.onProgress(event.message);
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function knownPiModelsSync(models, _userAuth) {
|
|
612
|
+
const result = [];
|
|
613
|
+
for (const provider of getProviders()) {
|
|
614
|
+
try {
|
|
615
|
+
for (const model of getModels(provider) ?? [])
|
|
616
|
+
result.push(model);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
// A static provider catalog failure must not make the sync auth guard fail.
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const [provider, config] of Object.entries(models.providers)) {
|
|
623
|
+
for (const model of config.models ?? []) {
|
|
624
|
+
result.push({ provider, id: model.id, name: model.name });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
function emptyPiModelCost() {
|
|
630
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
631
|
+
}
|
|
451
632
|
function normalizeProvider(value) {
|
|
452
633
|
if (typeof value !== 'string')
|
|
453
634
|
return undefined;
|