@agents-ensemble/core 0.20.0 → 0.22.0
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/assets/pi/pi-cli-env-wrapper.cmd +28 -0
- package/assets/pi/pi-cli-env-wrapper.sh +19 -0
- package/assets/pi/settings.json +4 -0
- package/dist/acp/acp-preset-bins.d.ts +3 -3
- package/dist/acp/acp-preset-bins.d.ts.map +1 -1
- package/dist/acp/acp-preset-bins.js +3 -3
- package/dist/acp/acp-preset-bins.js.map +1 -1
- package/dist/acp/index.d.ts +3 -1
- package/dist/acp/index.d.ts.map +1 -1
- package/dist/acp/index.js +2 -1
- package/dist/acp/index.js.map +1 -1
- package/dist/acp/pi-acp-wrapper.d.ts +3 -0
- package/dist/acp/pi-acp-wrapper.d.ts.map +1 -0
- package/dist/acp/pi-acp-wrapper.js +41 -0
- package/dist/acp/pi-acp-wrapper.js.map +1 -0
- package/dist/acp/pi-builtin-config.d.ts +10 -0
- package/dist/acp/pi-builtin-config.d.ts.map +1 -0
- package/dist/acp/pi-builtin-config.js +36 -0
- package/dist/acp/pi-builtin-config.js.map +1 -0
- package/dist/acp/pi-runtime.d.ts +23 -0
- package/dist/acp/pi-runtime.d.ts.map +1 -0
- package/dist/acp/pi-runtime.js +160 -0
- package/dist/acp/pi-runtime.js.map +1 -0
- package/dist/acp/pi-session-map.d.ts +36 -0
- package/dist/acp/pi-session-map.d.ts.map +1 -0
- package/dist/acp/pi-session-map.js +266 -0
- package/dist/acp/pi-session-map.js.map +1 -0
- package/dist/acp/resolve-acp-spawn.d.ts +9 -3
- package/dist/acp/resolve-acp-spawn.d.ts.map +1 -1
- package/dist/acp/resolve-acp-spawn.js +41 -9
- package/dist/acp/resolve-acp-spawn.js.map +1 -1
- package/dist/acp/resolve-bundled-acp-bin.d.ts +2 -2
- package/dist/acp/resolve-bundled-acp-bin.d.ts.map +1 -1
- package/dist/acp/resolve-bundled-acp-bin.js +4 -4
- package/dist/acp/resolve-bundled-acp-bin.js.map +1 -1
- package/dist/acp/validate-acp-preset-prerequisites.d.ts +1 -1
- package/dist/acp/validate-acp-preset-prerequisites.d.ts.map +1 -1
- package/dist/acp/validate-acp-preset-prerequisites.js +30 -10
- package/dist/acp/validate-acp-preset-prerequisites.js.map +1 -1
- package/dist/conductor/conductor-agent.d.ts +15 -3
- package/dist/conductor/conductor-agent.d.ts.map +1 -1
- package/dist/conductor/conductor-auth.d.ts +4 -3
- package/dist/conductor/conductor-auth.d.ts.map +1 -1
- package/dist/conductor/conductor-auth.js +20 -4
- package/dist/conductor/conductor-auth.js.map +1 -1
- package/dist/conductor/conductor-dispatch-lifecycle.d.ts +85 -0
- package/dist/conductor/conductor-dispatch-lifecycle.d.ts.map +1 -0
- package/dist/conductor/conductor-dispatch-lifecycle.js +190 -0
- package/dist/conductor/conductor-dispatch-lifecycle.js.map +1 -0
- package/dist/conductor/conductor-history.d.ts +15 -0
- package/dist/conductor/conductor-history.d.ts.map +1 -1
- package/dist/conductor/conductor-history.js +38 -7
- package/dist/conductor/conductor-history.js.map +1 -1
- package/dist/conductor/conductor-path-safety.d.ts +23 -0
- package/dist/conductor/conductor-path-safety.d.ts.map +1 -0
- package/dist/conductor/conductor-path-safety.js +319 -0
- package/dist/conductor/conductor-path-safety.js.map +1 -0
- package/dist/conductor/conductor-pi-auth.d.ts +57 -2
- package/dist/conductor/conductor-pi-auth.d.ts.map +1 -1
- package/dist/conductor/conductor-pi-auth.js +718 -40
- package/dist/conductor/conductor-pi-auth.js.map +1 -1
- package/dist/conductor/conductor-recovery-inbox.d.ts +73 -0
- package/dist/conductor/conductor-recovery-inbox.d.ts.map +1 -0
- package/dist/conductor/conductor-recovery-inbox.js +181 -0
- package/dist/conductor/conductor-recovery-inbox.js.map +1 -0
- package/dist/conductor/conductor-send-reconnect.d.ts +33 -4
- package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
- package/dist/conductor/conductor-send-reconnect.js +89 -24
- package/dist/conductor/conductor-send-reconnect.js.map +1 -1
- package/dist/conductor/conductor-session-driver.d.ts +34 -1
- package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
- package/dist/conductor/conductor-session-driver.js +529 -75
- package/dist/conductor/conductor-session-driver.js.map +1 -1
- package/dist/conductor/conductor-session.d.ts +4 -0
- package/dist/conductor/conductor-session.d.ts.map +1 -1
- package/dist/conductor/conductor-session.js +416 -145
- package/dist/conductor/conductor-session.js.map +1 -1
- package/dist/conductor/conductor-setup.d.ts +30 -0
- package/dist/conductor/conductor-setup.d.ts.map +1 -0
- package/dist/conductor/conductor-setup.js +243 -0
- package/dist/conductor/conductor-setup.js.map +1 -0
- package/dist/conductor/cursor-bootstrap-state.d.ts +9 -0
- package/dist/conductor/cursor-bootstrap-state.d.ts.map +1 -0
- package/dist/conductor/cursor-bootstrap-state.js +11 -0
- package/dist/conductor/cursor-bootstrap-state.js.map +1 -0
- package/dist/conductor/cursor-run-snapshots.d.ts +30 -0
- package/dist/conductor/cursor-run-snapshots.d.ts.map +1 -0
- package/dist/conductor/cursor-run-snapshots.js +120 -0
- package/dist/conductor/cursor-run-snapshots.js.map +1 -0
- package/dist/conductor/index.d.ts +18 -7
- package/dist/conductor/index.d.ts.map +1 -1
- package/dist/conductor/index.js +9 -4
- package/dist/conductor/index.js.map +1 -1
- package/dist/conductor/operator-command.d.ts +5 -0
- package/dist/conductor/operator-command.d.ts.map +1 -1
- package/dist/conductor/operator-command.js +6 -0
- package/dist/conductor/operator-command.js.map +1 -1
- package/dist/conductor/operator-input-binding.d.ts +17 -0
- package/dist/conductor/operator-input-binding.d.ts.map +1 -1
- package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts +45 -0
- package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts.map +1 -0
- package/dist/conductor/pi-auth-recovery-snapshot-store.js +116 -0
- package/dist/conductor/pi-auth-recovery-snapshot-store.js.map +1 -0
- package/dist/conductor/pi-conductor-agent.d.ts +6 -3
- package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
- package/dist/conductor/pi-conductor-agent.js +276 -53
- package/dist/conductor/pi-conductor-agent.js.map +1 -1
- package/dist/conductor/pi-conductor-session-file.d.ts +13 -0
- package/dist/conductor/pi-conductor-session-file.d.ts.map +1 -0
- package/dist/conductor/pi-conductor-session-file.js +21 -0
- package/dist/conductor/pi-conductor-session-file.js.map +1 -0
- package/dist/conductor/pi-resource-loader.d.ts +6 -0
- package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
- package/dist/conductor/pi-resource-loader.js +26 -12
- package/dist/conductor/pi-resource-loader.js.map +1 -1
- package/dist/conductor/pi-user-root-lock.d.ts +8 -0
- package/dist/conductor/pi-user-root-lock.d.ts.map +1 -0
- package/dist/conductor/pi-user-root-lock.js +53 -0
- package/dist/conductor/pi-user-root-lock.js.map +1 -0
- package/dist/conductor/recovery-inbox-command.d.ts +29 -0
- package/dist/conductor/recovery-inbox-command.d.ts.map +1 -0
- package/dist/conductor/recovery-inbox-command.js +248 -0
- package/dist/conductor/recovery-inbox-command.js.map +1 -0
- package/dist/conductor/session/events/index.d.ts +1 -1
- package/dist/conductor/session/events/index.d.ts.map +1 -1
- package/dist/conductor/session/events/index.js.map +1 -1
- package/dist/conductor/session/events/session-event.d.ts +43 -1
- package/dist/conductor/session/events/session-event.d.ts.map +1 -1
- package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
- package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
- package/dist/conductor/session/events/session-log-event-groups.js +2 -0
- package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
- package/dist/conductor/session/events/session-log-event.d.ts +4 -0
- package/dist/conductor/session/events/session-log-event.d.ts.map +1 -1
- package/dist/conductor/session/format-session-event.js +57 -5
- package/dist/conductor/session/format-session-event.js.map +1 -1
- package/dist/conductor/session/select-dispatch-batch.d.ts +2 -0
- package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
- package/dist/conductor/session/select-dispatch-batch.js +36 -2
- package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
- package/dist/conductor/session/session-event-queue.d.ts +14 -0
- package/dist/conductor/session/session-event-queue.d.ts.map +1 -1
- package/dist/conductor/session/session-event-queue.js +82 -10
- package/dist/conductor/session/session-event-queue.js.map +1 -1
- package/dist/conductor/session/session-event.d.ts +1 -1
- package/dist/conductor/session/session-event.d.ts.map +1 -1
- package/dist/conductor/session-policy.d.ts.map +1 -1
- package/dist/conductor/session-policy.js +8 -4
- package/dist/conductor/session-policy.js.map +1 -1
- package/dist/conductor/submit-operator-input.d.ts +3 -0
- package/dist/conductor/submit-operator-input.d.ts.map +1 -1
- package/dist/conductor/submit-operator-input.js +21 -0
- package/dist/conductor/submit-operator-input.js.map +1 -1
- package/dist/config/deep-merge.d.ts.map +1 -1
- package/dist/config/deep-merge.js +13 -3
- package/dist/config/deep-merge.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/doctor-settings.d.ts +59 -0
- package/dist/config/doctor-settings.d.ts.map +1 -0
- package/dist/config/doctor-settings.js +343 -0
- package/dist/config/doctor-settings.js.map +1 -0
- package/dist/config/ensemble-config-digest.d.ts +2 -4
- package/dist/config/ensemble-config-digest.d.ts.map +1 -1
- package/dist/config/ensemble-config-digest.js +2 -4
- package/dist/config/ensemble-config-digest.js.map +1 -1
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/load-ensemble-config.d.ts.map +1 -1
- package/dist/config/load-ensemble-config.js +8 -1
- package/dist/config/load-ensemble-config.js.map +1 -1
- package/dist/config/parse-config.d.ts.map +1 -1
- package/dist/config/parse-config.js +34 -14
- package/dist/config/parse-config.js.map +1 -1
- package/dist/config/resolve-settings.d.ts +3 -1
- package/dist/config/resolve-settings.d.ts.map +1 -1
- package/dist/config/resolve-settings.js +7 -4
- package/dist/config/resolve-settings.js.map +1 -1
- package/dist/config/types.d.ts +14 -5
- package/dist/config/types.d.ts.map +1 -1
- package/dist/dispatch/attach-worker.js +1 -1
- package/dist/dispatch/attach-worker.js.map +1 -1
- package/dist/dispatch/worker-acp-session.d.ts +21 -3
- package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
- package/dist/dispatch/worker-acp-session.js +50 -13
- package/dist/dispatch/worker-acp-session.js.map +1 -1
- package/dist/ensemble-paths.d.ts +19 -19
- package/dist/ensemble-paths.d.ts.map +1 -1
- package/dist/ensemble-paths.js +40 -22
- package/dist/ensemble-paths.js.map +1 -1
- package/dist/issue/resolve-issue-url.d.ts +4 -0
- package/dist/issue/resolve-issue-url.d.ts.map +1 -1
- package/dist/issue/resolve-issue-url.js +2 -2
- package/dist/issue/resolve-issue-url.js.map +1 -1
- package/dist/operator-runtime.d.ts +19 -4
- package/dist/operator-runtime.d.ts.map +1 -1
- package/dist/operator-runtime.js +13 -2
- package/dist/operator-runtime.js.map +1 -1
- package/dist/profile/types.d.ts +4 -0
- package/dist/profile/types.d.ts.map +1 -1
- package/dist/profile/types.js +29 -12
- package/dist/profile/types.js.map +1 -1
- package/dist/representation/escape-terminal-control-characters.d.ts +9 -0
- package/dist/representation/escape-terminal-control-characters.d.ts.map +1 -0
- package/dist/representation/escape-terminal-control-characters.js +34 -0
- package/dist/representation/escape-terminal-control-characters.js.map +1 -0
- package/dist/representation/index.d.ts +1 -0
- package/dist/representation/index.d.ts.map +1 -1
- package/dist/representation/index.js +1 -0
- package/dist/representation/index.js.map +1 -1
- package/dist/runtime/types.d.ts +9 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/worker-runtime.d.ts +14 -1
- package/dist/runtime/worker-runtime.d.ts.map +1 -1
- package/dist/runtime/worker-runtime.js +197 -50
- package/dist/runtime/worker-runtime.js.map +1 -1
- package/dist/runtime/worker-session.d.ts +2 -1
- package/dist/runtime/worker-session.d.ts.map +1 -1
- package/dist/runtime/worker-session.js +1 -0
- package/dist/runtime/worker-session.js.map +1 -1
- package/dist/session/export-session-bundle.d.ts +77 -0
- package/dist/session/export-session-bundle.d.ts.map +1 -0
- package/dist/session/export-session-bundle.js +640 -0
- package/dist/session/export-session-bundle.js.map +1 -0
- package/dist/session/index.d.ts +6 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +4 -1
- package/dist/session/index.js.map +1 -1
- package/dist/session/open-question-control.d.ts +9 -0
- package/dist/session/open-question-control.d.ts.map +1 -1
- package/dist/session/open-question-control.js +26 -1
- package/dist/session/open-question-control.js.map +1 -1
- package/dist/session/open-questions-store.d.ts +1 -1
- package/dist/session/open-questions-store.d.ts.map +1 -1
- package/dist/session/open-questions-store.js +3 -6
- package/dist/session/open-questions-store.js.map +1 -1
- package/dist/session/persisted-file.d.ts +5 -0
- package/dist/session/persisted-file.d.ts.map +1 -0
- package/dist/session/persisted-file.js +56 -0
- package/dist/session/persisted-file.js.map +1 -0
- package/dist/session/session-resume-store.d.ts +79 -10
- package/dist/session/session-resume-store.d.ts.map +1 -1
- package/dist/session/session-resume-store.js +353 -83
- package/dist/session/session-resume-store.js.map +1 -1
- package/dist/session/session-snapshot-paths.d.ts +11 -0
- package/dist/session/session-snapshot-paths.d.ts.map +1 -0
- package/dist/session/session-snapshot-paths.js +41 -0
- package/dist/session/session-snapshot-paths.js.map +1 -0
- package/dist/session/session-state-store.d.ts +2 -2
- package/dist/session/session-state-store.d.ts.map +1 -1
- package/dist/session/session-state-store.js +5 -16
- package/dist/session/session-state-store.js.map +1 -1
- package/dist/session/tool-call-snapshot-history.d.ts +12 -1
- package/dist/session/tool-call-snapshot-history.d.ts.map +1 -1
- package/dist/session/tool-call-snapshot-history.js +75 -70
- package/dist/session/tool-call-snapshot-history.js.map +1 -1
- package/dist/session/worker-session-history.d.ts +51 -0
- package/dist/session/worker-session-history.d.ts.map +1 -0
- package/dist/session/worker-session-history.js +654 -0
- package/dist/session/worker-session-history.js.map +1 -0
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/worktree/worktree.d.ts +6 -1
- package/dist/worktree/worktree.d.ts.map +1 -1
- package/dist/worktree/worktree.js +123 -2
- package/dist/worktree/worktree.js.map +1 -1
- package/package.json +9 -4
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile,
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { chmod, lstat, mkdir, open, readFile, rename, unlink, } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
5
|
import { ModelRegistry, ModelRuntime, SettingsManager, } from '@earendil-works/pi-coding-agent';
|
|
5
6
|
import { findEnvKeys, getEnvApiKey, getModels, getProviders, } from '@earendil-works/pi-ai/compat';
|
|
6
|
-
import {
|
|
7
|
+
import { withPiUserRootLock } from './pi-user-root-lock.js';
|
|
8
|
+
import { canonicalPath, getProjectWriteBoundaryPaths, isPathWithin, isPathWithinAnyRoot, } from './conductor-path-safety.js';
|
|
9
|
+
import { loadPiResources, readPiSettingsFile, resolvePiResourceRoots, } from './pi-resource-loader.js';
|
|
7
10
|
import { getPiHeadlessSetting } from './pi-headless-settings.js';
|
|
8
11
|
/** Create the Pi 1.x model runtime and registry for the resolved resource roots. */
|
|
9
12
|
export async function createPiConductorAuthContext(options, resources) {
|
|
13
|
+
return createPiConductorAuthContextWithAuthPath(options, resources);
|
|
14
|
+
}
|
|
15
|
+
async function createPiConductorAuthContextWithAuthPath(options, resources, runtimeAuthPath) {
|
|
10
16
|
const roots = resolvePiResourceRoots(options);
|
|
11
17
|
const authPath = join(roots.agentDir, 'auth.json');
|
|
12
18
|
const modelsPath = join(roots.agentDir, 'models.json');
|
|
13
19
|
const resolvedResources = resources ?? (await loadPiResources(options));
|
|
14
|
-
const modelRuntime = await createPiModelRuntime(options, resolvedResources);
|
|
20
|
+
const modelRuntime = await createPiModelRuntime(options, resolvedResources, runtimeAuthPath ?? authPath);
|
|
15
21
|
return {
|
|
16
22
|
agentDir: roots.agentDir,
|
|
17
23
|
authPath,
|
|
@@ -28,18 +34,58 @@ export async function resolvePiConductorProvider(options) {
|
|
|
28
34
|
}
|
|
29
35
|
/** Login to a Pi provider. API-key login persists only in the user auth file. */
|
|
30
36
|
export async function loginPiConductor(options) {
|
|
37
|
+
const roots = resolvePiResourceRoots(options);
|
|
38
|
+
const authPath = join(roots.agentDir, 'auth.json');
|
|
39
|
+
const getProtectedProjectRoots = () => getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]);
|
|
40
|
+
const protectedProjectRoots = await getProtectedProjectRoots();
|
|
41
|
+
const runtimeAuthPath = await resolveSafePiUserAuthPath(authPath, protectedProjectRoots);
|
|
31
42
|
const resources = await loadPiResources(options);
|
|
32
|
-
const context = await
|
|
43
|
+
const context = await createPiConductorAuthContextWithAuthPath(options, resources, runtimeAuthPath);
|
|
33
44
|
const provider = await resolveConductorProviderOrThrow(options, resources, context);
|
|
34
45
|
const oauthProvider = Boolean(context.modelRuntime.getProvider(provider)?.auth.oauth);
|
|
35
|
-
if (oauthProvider) {
|
|
46
|
+
if (oauthProvider && options.apiKey === undefined) {
|
|
36
47
|
if (!options.oauthCallbacks) {
|
|
37
48
|
throw new Error(`Pi provider "${provider}" uses OAuth. Run this command from a TTY so the OAuth login flow can interact with you.`);
|
|
38
49
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
// Pi 1.2.0's OpenAI ChatGPT OAuth is the only built-in flow that requests
|
|
51
|
+
// a device ID. Other providers must not depend on user settings being writable.
|
|
52
|
+
const requiresDeviceId = provider === 'openai';
|
|
53
|
+
const settingsPath = join(roots.agentDir, 'settings.json');
|
|
54
|
+
let loginOptions;
|
|
55
|
+
if (requiresDeviceId) {
|
|
56
|
+
const deviceId = await withPiUserRootLock(roots.agentDir, options.cwd, [roots.projectDir], async () => {
|
|
57
|
+
// The hardlink split is a settings write too; keep it in the same
|
|
58
|
+
// read/create/persist transaction as the device ID.
|
|
59
|
+
await ensurePiUserSettingsSafeForOAuth(settingsPath, await getProtectedProjectRoots());
|
|
60
|
+
const securedAuthPath = await ensurePrivatePiUserAuthFile(context.authPath, await getProtectedProjectRoots());
|
|
61
|
+
if (securedAuthPath !== runtimeAuthPath) {
|
|
62
|
+
throw new Error('Pi user auth.json target changed while preparing OAuth login; no credentials were written. Retry login after checking the auth.json symlink.');
|
|
63
|
+
}
|
|
64
|
+
const settingsManager = SettingsManager.create(options.cwd, roots.agentDir);
|
|
65
|
+
const deviceId = settingsManager.getOrCreateDeviceId();
|
|
66
|
+
await settingsManager.flush();
|
|
67
|
+
const settingsErrors = settingsManager
|
|
68
|
+
.drainErrors()
|
|
69
|
+
.filter(({ scope }) => scope === 'global');
|
|
70
|
+
if (settingsErrors.length > 0) {
|
|
71
|
+
const details = settingsErrors
|
|
72
|
+
.map(({ error }) => error.message)
|
|
73
|
+
.join('; ');
|
|
74
|
+
throw new Error(`Failed to persist the Pi OAuth device ID to user settings.json; OAuth login was not started: ${details}`);
|
|
75
|
+
}
|
|
76
|
+
return deviceId;
|
|
77
|
+
});
|
|
78
|
+
loginOptions = {
|
|
79
|
+
getDeviceId: () => deviceId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const securedAuthPath = await ensurePrivatePiUserAuthFile(context.authPath, await getProtectedProjectRoots());
|
|
84
|
+
if (securedAuthPath !== runtimeAuthPath) {
|
|
85
|
+
throw new Error('Pi user auth.json target changed while preparing OAuth login; no credentials were written. Retry login after checking the auth.json symlink.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await context.modelRuntime.login(provider, 'oauth', toPiAuthInteraction(options.oauthCallbacks), loginOptions);
|
|
43
89
|
return {
|
|
44
90
|
backend: 'pi',
|
|
45
91
|
provider,
|
|
@@ -49,9 +95,9 @@ export async function loginPiConductor(options) {
|
|
|
49
95
|
}
|
|
50
96
|
const apiKey = options.apiKey?.trim();
|
|
51
97
|
if (!apiKey) {
|
|
52
|
-
throw new Error(`Pi provider "${provider}" requires an API key.
|
|
98
|
+
throw new Error(`Pi provider "${provider}" requires an API key. Use --api-key-prompt from a TTY or --api-key-stdin with piped input to store one, or set ${formatPiProviderEnvHint(provider)} for request-time authentication.`);
|
|
53
99
|
}
|
|
54
|
-
await writePiApiKey(context.authPath, provider, apiKey);
|
|
100
|
+
await writePiApiKey(context.authPath, await getProtectedProjectRoots(), provider, apiKey);
|
|
55
101
|
await context.modelRuntime.setRuntimeApiKey(provider, apiKey);
|
|
56
102
|
return {
|
|
57
103
|
backend: 'pi',
|
|
@@ -69,9 +115,19 @@ export async function isPiConductorOAuthProvider(options) {
|
|
|
69
115
|
}
|
|
70
116
|
/** Remove a Pi provider's user-layer credential. */
|
|
71
117
|
export async function logoutPiConductor(options) {
|
|
118
|
+
const roots = resolvePiResourceRoots(options);
|
|
119
|
+
const authPath = join(roots.agentDir, 'auth.json');
|
|
120
|
+
const protectedProjectRoots = await getProjectWriteBoundaryPaths(options.cwd, [
|
|
121
|
+
roots.projectDir,
|
|
122
|
+
]);
|
|
123
|
+
const runtimeAuthPath = await resolveSafePiUserAuthPath(authPath, protectedProjectRoots);
|
|
72
124
|
const resources = await loadPiResources(options);
|
|
73
|
-
const context = await
|
|
125
|
+
const context = await createPiConductorAuthContextWithAuthPath(options, resources, runtimeAuthPath);
|
|
74
126
|
const provider = await resolveConductorProviderOrThrow(options, resources, context);
|
|
127
|
+
const securedAuthPath = await ensurePrivatePiUserAuthFile(context.authPath, await getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]));
|
|
128
|
+
if (securedAuthPath !== runtimeAuthPath) {
|
|
129
|
+
throw new Error('Pi user auth.json target changed while preparing logout; no credentials were written. Retry after checking the auth.json symlink.');
|
|
130
|
+
}
|
|
75
131
|
await context.modelRuntime.logout(provider);
|
|
76
132
|
return {
|
|
77
133
|
backend: 'pi',
|
|
@@ -79,6 +135,82 @@ export async function logoutPiConductor(options) {
|
|
|
79
135
|
authPath: context.authPath,
|
|
80
136
|
};
|
|
81
137
|
}
|
|
138
|
+
/** Register a Pi `!command` in the selected settings.json apiKeys entry. */
|
|
139
|
+
export async function setPiConductorCommandAuth(options) {
|
|
140
|
+
const provider = requireCommandAuthProvider(options.provider);
|
|
141
|
+
validatePiCommandCredential(options.command);
|
|
142
|
+
const target = requireCommandAuthTarget(options.target);
|
|
143
|
+
const roots = resolvePiResourceRoots(options);
|
|
144
|
+
const settingsPath = join(target === 'project' ? roots.projectDir : roots.agentDir, 'settings.json');
|
|
145
|
+
const userSettingsPath = join(roots.agentDir, 'settings.json');
|
|
146
|
+
const projectSettingsPath = join(roots.projectDir, 'settings.json');
|
|
147
|
+
const [userSettings, projectSettings] = await Promise.all([
|
|
148
|
+
readPiSettingsFile(userSettingsPath),
|
|
149
|
+
roots.agentDir === roots.projectDir
|
|
150
|
+
? Promise.resolve(undefined)
|
|
151
|
+
: readPiSettingsFile(projectSettingsPath),
|
|
152
|
+
]);
|
|
153
|
+
const effectiveScalarApiKey = projectSettings !== undefined &&
|
|
154
|
+
Object.prototype.hasOwnProperty.call(projectSettings, 'apiKey')
|
|
155
|
+
? projectSettings.apiKey
|
|
156
|
+
: userSettings.apiKey;
|
|
157
|
+
if (typeof effectiveScalarApiKey === 'string') {
|
|
158
|
+
throw new Error('Pi settings apiKey takes precedence over apiKeys[provider]; remove or change the scalar setting before registering a provider command.');
|
|
159
|
+
}
|
|
160
|
+
if (target === 'user' &&
|
|
161
|
+
roots.agentDir !== roots.projectDir &&
|
|
162
|
+
isRecord(projectSettings?.apiKeys) &&
|
|
163
|
+
Object.prototype.hasOwnProperty.call(projectSettings.apiKeys, provider)) {
|
|
164
|
+
throw new Error('Project Pi settings already define apiKeys for this provider; use --target project to update that entry.');
|
|
165
|
+
}
|
|
166
|
+
if (target === 'user') {
|
|
167
|
+
const protectedProjectRoots = await getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]);
|
|
168
|
+
if (await isPathWithinAnyRoot(settingsPath, protectedProjectRoots)) {
|
|
169
|
+
throw new Error('Refusing to write user Pi settings.json inside a project-owned resource.');
|
|
170
|
+
}
|
|
171
|
+
await mkdir(roots.agentDir, { recursive: true, mode: 0o700 });
|
|
172
|
+
await withPiUserRootLock(roots.agentDir, options.cwd, [roots.projectDir], async () => {
|
|
173
|
+
const currentProjectRoots = await getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]);
|
|
174
|
+
const targetPath = await resolveSafePiUserSettingsPath(settingsPath, currentProjectRoots);
|
|
175
|
+
await writePiSettingsCommand(targetPath, provider, options.command);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
await mkdir(roots.projectDir, { recursive: true });
|
|
180
|
+
const targetPath = await resolveSafePiProjectSettingsPath(settingsPath, roots.projectDir);
|
|
181
|
+
await writePiSettingsCommand(targetPath, provider, options.command);
|
|
182
|
+
}
|
|
183
|
+
return { backend: 'pi', provider, target, settingsPath };
|
|
184
|
+
}
|
|
185
|
+
/** Remove only a command value from the selected settings.json apiKeys entry. */
|
|
186
|
+
export async function clearPiConductorCommandAuth(options) {
|
|
187
|
+
const provider = requireCommandAuthProvider(options.provider);
|
|
188
|
+
const target = requireCommandAuthTarget(options.target);
|
|
189
|
+
const roots = resolvePiResourceRoots(options);
|
|
190
|
+
const targetRoot = target === 'project' ? roots.projectDir : roots.agentDir;
|
|
191
|
+
const settingsPath = join(targetRoot, 'settings.json');
|
|
192
|
+
if (target === 'user') {
|
|
193
|
+
const protectedProjectRoots = await getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]);
|
|
194
|
+
if (await isPathWithinAnyRoot(settingsPath, protectedProjectRoots)) {
|
|
195
|
+
throw new Error('Refusing to update user Pi settings.json inside a project-owned resource.');
|
|
196
|
+
}
|
|
197
|
+
if (!(await pathExists(roots.agentDir))) {
|
|
198
|
+
return { backend: 'pi', provider, target, settingsPath, cleared: false };
|
|
199
|
+
}
|
|
200
|
+
const cleared = await withPiUserRootLock(roots.agentDir, options.cwd, [roots.projectDir], async () => {
|
|
201
|
+
const currentProjectRoots = await getProjectWriteBoundaryPaths(options.cwd, [roots.projectDir]);
|
|
202
|
+
const targetPath = await resolveSafePiUserSettingsPath(settingsPath, currentProjectRoots);
|
|
203
|
+
return clearPiSettingsCommand(targetPath, provider);
|
|
204
|
+
});
|
|
205
|
+
return { backend: 'pi', provider, target, settingsPath, cleared };
|
|
206
|
+
}
|
|
207
|
+
if (!(await pathExists(roots.projectDir))) {
|
|
208
|
+
return { backend: 'pi', provider, target, settingsPath, cleared: false };
|
|
209
|
+
}
|
|
210
|
+
const targetPath = await resolveSafePiProjectSettingsPath(settingsPath, roots.projectDir);
|
|
211
|
+
const cleared = await clearPiSettingsCommand(targetPath, provider);
|
|
212
|
+
return { backend: 'pi', provider, target, settingsPath, cleared };
|
|
213
|
+
}
|
|
82
214
|
/** Return non-secret Pi auth metadata for the selected/default provider. */
|
|
83
215
|
export async function getPiConductorAuthStatus(options) {
|
|
84
216
|
const resources = await loadPiResources(options);
|
|
@@ -153,23 +285,27 @@ export async function resolvePiConductorApiKey(options) {
|
|
|
153
285
|
if (settingsKey !== undefined && isPiCommandConfigValue(settingsKey)) {
|
|
154
286
|
options.modelRuntime.registerProvider(options.provider, { apiKey: settingsKey });
|
|
155
287
|
}
|
|
288
|
+
else if (modelsKey !== undefined && isPiCommandConfigValue(modelsKey)) {
|
|
289
|
+
// models.json command providers are deliberately not registered while a
|
|
290
|
+
// conductor session is being assembled: ModelRuntime.refresh() checks
|
|
291
|
+
// provider auth and can execute their command. Register the selected value
|
|
292
|
+
// only when the request path asks for its credential.
|
|
293
|
+
options.modelRuntime.registerProvider(options.provider, { apiKey: modelsKey });
|
|
294
|
+
}
|
|
156
295
|
// ModelRuntime owns the Pi 1.x credential store, OAuth refresh, and locking.
|
|
157
296
|
// Resolve it on every request instead of capturing the key at construction.
|
|
158
|
-
let runtimeError;
|
|
159
297
|
try {
|
|
160
298
|
const runtimeAuth = await options.modelRuntime.getAuth(options.provider);
|
|
161
299
|
if (runtimeAuth?.auth.apiKey !== undefined)
|
|
162
300
|
return runtimeAuth.auth.apiKey;
|
|
163
301
|
}
|
|
164
|
-
catch
|
|
165
|
-
runtimeError = error;
|
|
302
|
+
catch {
|
|
166
303
|
// Non-command references can still use the ensemble fallback chain. A
|
|
167
|
-
// command reference is reported below
|
|
168
|
-
// as an API key.
|
|
304
|
+
// command reference is reported below without preserving the raw error.
|
|
169
305
|
}
|
|
170
306
|
const commandKey = [settingsKey, modelsKey].find((value) => value !== undefined && isPiCommandConfigValue(value));
|
|
171
307
|
if (commandKey !== undefined) {
|
|
172
|
-
throw formatPiCommandResolutionError(
|
|
308
|
+
throw formatPiCommandResolutionError();
|
|
173
309
|
}
|
|
174
310
|
if (settingsKey !== undefined)
|
|
175
311
|
return settingsKey;
|
|
@@ -177,6 +313,54 @@ export async function resolvePiConductorApiKey(options) {
|
|
|
177
313
|
return modelsKey;
|
|
178
314
|
return getEnvApiKey(options.provider, toProviderEnv(env));
|
|
179
315
|
}
|
|
316
|
+
/** Prepare configured Pi commands when a provider request asks ModelRuntime for auth. */
|
|
317
|
+
export async function preparePiConductorCommandAuthForRequest(options) {
|
|
318
|
+
const env = options.env ?? process.env;
|
|
319
|
+
const projectEntry = options.resources.authLayers.at(-1)?.[options.provider];
|
|
320
|
+
const projectKey = options.resources.roots.agentDir !== options.resources.roots.projectDir
|
|
321
|
+
? resolvePiAuthEntry(projectEntry, env)
|
|
322
|
+
: undefined;
|
|
323
|
+
if (options.resources.roots.agentDir !== options.resources.roots.projectDir) {
|
|
324
|
+
if (isPiOAuthAuthEntry(projectEntry)) {
|
|
325
|
+
throw new Error(formatProjectOAuthAuthError(options.provider));
|
|
326
|
+
}
|
|
327
|
+
if (projectKey !== undefined) {
|
|
328
|
+
if (isPiCommandConfigValue(projectKey)) {
|
|
329
|
+
await resolvePiProjectCommandApiKey(options.modelRuntime, options.provider, projectKey);
|
|
330
|
+
}
|
|
331
|
+
// Any project auth entry remains above user/settings/model credentials.
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const settingsKey = resolvePiSettingsApiKey(options.resources.settings, options.provider, env);
|
|
336
|
+
const modelsKey = resolvePiModelsApiKey(options.resources.models.providers[options.provider], env);
|
|
337
|
+
if (settingsKey !== undefined && isPiCommandConfigValue(settingsKey)) {
|
|
338
|
+
options.modelRuntime.registerProvider(options.provider, {
|
|
339
|
+
apiKey: settingsKey,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else if (modelsKey !== undefined && isPiCommandConfigValue(modelsKey)) {
|
|
343
|
+
options.modelRuntime.registerProvider(options.provider, {
|
|
344
|
+
apiKey: modelsKey,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Whether resolving this provider's configured Pi credential can run a command. */
|
|
349
|
+
export function hasPiConductorCommandCredential(resources, provider, env = process.env) {
|
|
350
|
+
if (resources.roots.agentDir !== resources.roots.projectDir) {
|
|
351
|
+
const projectEntry = resources.authLayers.at(-1)?.[provider];
|
|
352
|
+
if (isPiOAuthAuthEntry(projectEntry))
|
|
353
|
+
return false;
|
|
354
|
+
const projectKey = resolvePiAuthEntry(projectEntry, env);
|
|
355
|
+
if (projectKey !== undefined)
|
|
356
|
+
return isPiCommandConfigValue(projectKey);
|
|
357
|
+
}
|
|
358
|
+
const settingsKey = resolvePiSettingsApiKey(resources.settings, provider, env);
|
|
359
|
+
if (settingsKey !== undefined)
|
|
360
|
+
return isPiCommandConfigValue(settingsKey);
|
|
361
|
+
const modelsKey = resolvePiModelsApiKey(resources.models.providers[provider], env);
|
|
362
|
+
return modelsKey !== undefined && isPiCommandConfigValue(modelsKey);
|
|
363
|
+
}
|
|
180
364
|
/** Return whether a provider has configured auth without exposing its value. */
|
|
181
365
|
export function hasPiProviderAuth(provider, context, resources, env = process.env) {
|
|
182
366
|
const status = getPiProviderAuthStatus(provider, context, resources, env);
|
|
@@ -346,14 +530,21 @@ async function resolveStatusProviders(options, resources, context) {
|
|
|
346
530
|
const configured = resolvePiConductorProviderFromResources(options, resources, context);
|
|
347
531
|
if (configured)
|
|
348
532
|
return [configured];
|
|
533
|
+
const settingsApiKeys = getPiHeadlessSetting(resources.settings, 'apiKeys', 'auth-fallback');
|
|
349
534
|
const providers = new Set([
|
|
350
535
|
...(await context.modelRuntime.listCredentials()).map((entry) => entry.providerId),
|
|
351
536
|
...context.modelRegistry.getAll().map((model) => model.provider),
|
|
352
537
|
...Object.keys(resources.models.providers),
|
|
538
|
+
...(isRecord(settingsApiKeys)
|
|
539
|
+
? Object.entries(settingsApiKeys)
|
|
540
|
+
.filter(([, value]) => typeof value === 'string')
|
|
541
|
+
.map(([provider]) => provider)
|
|
542
|
+
: []),
|
|
353
543
|
]);
|
|
354
544
|
return [...providers].sort();
|
|
355
545
|
}
|
|
356
546
|
function getPiProviderAuthStatus(provider, context, resources, env) {
|
|
547
|
+
const settingsCommand = getPiSettingsCommandSource(resources, provider, env);
|
|
357
548
|
if (resources.roots.agentDir !== resources.roots.projectDir) {
|
|
358
549
|
const projectEntry = resources.authLayers.at(-1)?.[provider];
|
|
359
550
|
if (isPiOAuthAuthEntry(projectEntry)) {
|
|
@@ -361,24 +552,156 @@ function getPiProviderAuthStatus(provider, context, resources, env) {
|
|
|
361
552
|
provider,
|
|
362
553
|
configured: false,
|
|
363
554
|
source: 'project_oauth_unsupported',
|
|
555
|
+
...(settingsCommand
|
|
556
|
+
? {
|
|
557
|
+
commandConfiguration: {
|
|
558
|
+
source: 'settings_json',
|
|
559
|
+
scope: settingsCommand.scope,
|
|
560
|
+
path: settingsCommand.path,
|
|
561
|
+
effective: false,
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
364
565
|
};
|
|
365
566
|
}
|
|
366
567
|
const projectKey = resolvePiAuthEntry(projectEntry, env);
|
|
367
568
|
if (projectKey !== undefined) {
|
|
368
|
-
return {
|
|
569
|
+
return {
|
|
570
|
+
provider,
|
|
571
|
+
configured: true,
|
|
572
|
+
source: 'project_stored',
|
|
573
|
+
...getKnownAuthMode(projectEntry),
|
|
574
|
+
scope: 'project',
|
|
575
|
+
configurationPath: join(resources.roots.projectDir, 'auth.json'),
|
|
576
|
+
...(settingsCommand
|
|
577
|
+
? {
|
|
578
|
+
commandConfiguration: {
|
|
579
|
+
source: 'settings_json',
|
|
580
|
+
scope: settingsCommand.scope,
|
|
581
|
+
path: settingsCommand.path,
|
|
582
|
+
effective: false,
|
|
583
|
+
},
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
586
|
+
};
|
|
369
587
|
}
|
|
370
588
|
}
|
|
371
589
|
const registryStatus = context.modelRuntime.getProviderAuthStatus(provider);
|
|
372
|
-
if (registryStatus.
|
|
373
|
-
|
|
590
|
+
if (registryStatus.source === 'stored' || registryStatus.source === 'runtime') {
|
|
591
|
+
const stored = registryStatus.source === 'stored';
|
|
592
|
+
const runtimeSettings = stored
|
|
593
|
+
? undefined
|
|
594
|
+
: getPiSettingsApiKeysSource(resources, provider, env);
|
|
595
|
+
return {
|
|
596
|
+
...normalizePiAuthStatus(provider, registryStatus, true),
|
|
597
|
+
...(stored
|
|
598
|
+
? getKnownAuthMode(resources.authLayers[0]?.[provider])
|
|
599
|
+
: { mode: 'api_key' }),
|
|
600
|
+
...(stored
|
|
601
|
+
? { scope: 'user', configurationPath: context.authPath }
|
|
602
|
+
: runtimeSettings
|
|
603
|
+
? {
|
|
604
|
+
scope: runtimeSettings.scope,
|
|
605
|
+
configurationPath: runtimeSettings.path,
|
|
606
|
+
}
|
|
607
|
+
: {}),
|
|
608
|
+
...(settingsCommand
|
|
609
|
+
? {
|
|
610
|
+
commandConfiguration: {
|
|
611
|
+
source: 'settings_json',
|
|
612
|
+
scope: settingsCommand.scope,
|
|
613
|
+
path: settingsCommand.path,
|
|
614
|
+
effective: false,
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
: {}),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (settingsCommand?.selected) {
|
|
621
|
+
return {
|
|
622
|
+
provider,
|
|
623
|
+
configured: true,
|
|
624
|
+
source: 'settings_json',
|
|
625
|
+
mode: 'command',
|
|
626
|
+
scope: settingsCommand.scope,
|
|
627
|
+
configurationPath: settingsCommand.path,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
if (registryStatus.configured) {
|
|
631
|
+
const modelsCommand = registryStatus.source === 'models_json_command'
|
|
632
|
+
? getPiModelsCommandSource(resources, provider, env)
|
|
633
|
+
: undefined;
|
|
634
|
+
return {
|
|
635
|
+
...normalizePiAuthStatus(provider, registryStatus, true),
|
|
636
|
+
...(modelsCommand
|
|
637
|
+
? {
|
|
638
|
+
mode: 'command',
|
|
639
|
+
scope: modelsCommand.scope,
|
|
640
|
+
configurationPath: modelsCommand.path,
|
|
641
|
+
}
|
|
642
|
+
: {}),
|
|
643
|
+
...(settingsCommand
|
|
644
|
+
? {
|
|
645
|
+
commandConfiguration: {
|
|
646
|
+
source: 'settings_json',
|
|
647
|
+
scope: settingsCommand.scope,
|
|
648
|
+
path: settingsCommand.path,
|
|
649
|
+
effective: false,
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
: {}),
|
|
653
|
+
};
|
|
374
654
|
}
|
|
375
655
|
const settingsKey = resolvePiSettingsApiKey(resources.settings, provider, env);
|
|
376
656
|
if (settingsKey !== undefined) {
|
|
377
|
-
|
|
657
|
+
const settingsSource = getPiSettingsApiKeySource(resources, provider, env);
|
|
658
|
+
return {
|
|
659
|
+
provider,
|
|
660
|
+
configured: true,
|
|
661
|
+
source: 'settings_json',
|
|
662
|
+
...(settingsSource
|
|
663
|
+
? {
|
|
664
|
+
scope: settingsSource.scope,
|
|
665
|
+
configurationPath: settingsSource.path,
|
|
666
|
+
}
|
|
667
|
+
: {}),
|
|
668
|
+
...(settingsCommand
|
|
669
|
+
? {
|
|
670
|
+
commandConfiguration: {
|
|
671
|
+
source: 'settings_json',
|
|
672
|
+
scope: settingsCommand.scope,
|
|
673
|
+
path: settingsCommand.path,
|
|
674
|
+
effective: false,
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
: {}),
|
|
678
|
+
};
|
|
378
679
|
}
|
|
379
680
|
const modelsKey = resolvePiModelsApiKey(resources.models.providers[provider], env);
|
|
380
681
|
if (modelsKey !== undefined) {
|
|
381
|
-
|
|
682
|
+
const modelsSource = getPiModelsApiKeySource(resources, provider, env);
|
|
683
|
+
return {
|
|
684
|
+
provider,
|
|
685
|
+
configured: true,
|
|
686
|
+
source: 'models_json_key',
|
|
687
|
+
...(modelsSource
|
|
688
|
+
? {
|
|
689
|
+
scope: modelsSource.scope,
|
|
690
|
+
configurationPath: modelsSource.path,
|
|
691
|
+
...(isPiCommandConfigValue(modelsKey) ? { mode: 'command' } : {}),
|
|
692
|
+
}
|
|
693
|
+
: {}),
|
|
694
|
+
...(settingsCommand
|
|
695
|
+
? {
|
|
696
|
+
commandConfiguration: {
|
|
697
|
+
source: 'settings_json',
|
|
698
|
+
scope: settingsCommand.scope,
|
|
699
|
+
path: settingsCommand.path,
|
|
700
|
+
effective: false,
|
|
701
|
+
},
|
|
702
|
+
}
|
|
703
|
+
: {}),
|
|
704
|
+
};
|
|
382
705
|
}
|
|
383
706
|
const envKey = getEnvApiKey(provider, toProviderEnv(env));
|
|
384
707
|
if (envKey !== undefined) {
|
|
@@ -387,9 +710,31 @@ function getPiProviderAuthStatus(provider, context, resources, env) {
|
|
|
387
710
|
configured: true,
|
|
388
711
|
source: 'environment',
|
|
389
712
|
label: formatPiProviderEnvHint(provider),
|
|
713
|
+
...(settingsCommand
|
|
714
|
+
? {
|
|
715
|
+
commandConfiguration: {
|
|
716
|
+
source: 'settings_json',
|
|
717
|
+
scope: settingsCommand.scope,
|
|
718
|
+
path: settingsCommand.path,
|
|
719
|
+
effective: false,
|
|
720
|
+
},
|
|
721
|
+
}
|
|
722
|
+
: {}),
|
|
390
723
|
};
|
|
391
724
|
}
|
|
392
|
-
return
|
|
725
|
+
return {
|
|
726
|
+
...normalizePiAuthStatus(provider, registryStatus, false),
|
|
727
|
+
...(settingsCommand
|
|
728
|
+
? {
|
|
729
|
+
commandConfiguration: {
|
|
730
|
+
source: 'settings_json',
|
|
731
|
+
scope: settingsCommand.scope,
|
|
732
|
+
path: settingsCommand.path,
|
|
733
|
+
effective: false,
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
: {}),
|
|
737
|
+
};
|
|
393
738
|
}
|
|
394
739
|
function normalizePiAuthStatus(provider, status, configured) {
|
|
395
740
|
return {
|
|
@@ -399,6 +744,117 @@ function normalizePiAuthStatus(provider, status, configured) {
|
|
|
399
744
|
...(status.label ? { label: status.label } : {}),
|
|
400
745
|
};
|
|
401
746
|
}
|
|
747
|
+
function getPiSettingsApiKeySource(resources, provider, env) {
|
|
748
|
+
const scalarValue = getPiHeadlessSetting(resources.settings, 'apiKey', 'auth-fallback');
|
|
749
|
+
const usesScalar = typeof scalarValue === 'string';
|
|
750
|
+
const apiKeys = getPiHeadlessSetting(resources.settings, 'apiKeys', 'auth-fallback');
|
|
751
|
+
const providerValue = !usesScalar &&
|
|
752
|
+
isRecord(apiKeys) &&
|
|
753
|
+
Object.prototype.hasOwnProperty.call(apiKeys, provider)
|
|
754
|
+
? apiKeys[provider]
|
|
755
|
+
: undefined;
|
|
756
|
+
const selected = usesScalar ? scalarValue : providerValue;
|
|
757
|
+
if (typeof selected !== 'string')
|
|
758
|
+
return undefined;
|
|
759
|
+
return getPiSettingsLayerSource(resources, provider, env, usesScalar ? 'apiKey' : 'apiKeys', expandEnvReference(selected, env));
|
|
760
|
+
}
|
|
761
|
+
function getPiSettingsApiKeysSource(resources, provider, env) {
|
|
762
|
+
const apiKeys = getPiHeadlessSetting(resources.settings, 'apiKeys', 'auth-fallback');
|
|
763
|
+
if (!isRecord(apiKeys) || !Object.prototype.hasOwnProperty.call(apiKeys, provider)) {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
const selected = apiKeys[provider];
|
|
767
|
+
if (typeof selected !== 'string')
|
|
768
|
+
return undefined;
|
|
769
|
+
return getPiSettingsLayerSource(resources, provider, env, 'apiKeys', expandEnvReference(selected, env));
|
|
770
|
+
}
|
|
771
|
+
function getPiSettingsLayerSource(resources, provider, env, field, resolved) {
|
|
772
|
+
if (resolved === undefined)
|
|
773
|
+
return undefined;
|
|
774
|
+
for (let index = resources.settingsLayers.length - 1; index >= 0; index -= 1) {
|
|
775
|
+
const layer = resources.settingsLayers[index];
|
|
776
|
+
const layerApiKeys = getPiHeadlessSetting(layer, 'apiKeys', 'auth-fallback');
|
|
777
|
+
const layerValue = field === 'apiKey'
|
|
778
|
+
? getPiHeadlessSetting(layer, 'apiKey', 'auth-fallback')
|
|
779
|
+
: isRecord(layerApiKeys) &&
|
|
780
|
+
Object.prototype.hasOwnProperty.call(layerApiKeys, provider)
|
|
781
|
+
? layerApiKeys[provider]
|
|
782
|
+
: undefined;
|
|
783
|
+
if (typeof layerValue === 'string' &&
|
|
784
|
+
expandEnvReference(layerValue, env) === resolved) {
|
|
785
|
+
const isProject = index === resources.settingsLayers.length - 1 &&
|
|
786
|
+
resources.roots.agentDir !== resources.roots.projectDir;
|
|
787
|
+
return {
|
|
788
|
+
scope: isProject ? 'project' : 'user',
|
|
789
|
+
path: join(isProject ? resources.roots.projectDir : resources.roots.agentDir, 'settings.json'),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
function getPiSettingsCommandSource(resources, provider, env) {
|
|
796
|
+
const scalarApiKey = getPiHeadlessSetting(resources.settings, 'apiKey', 'auth-fallback');
|
|
797
|
+
if (typeof scalarApiKey === 'string') {
|
|
798
|
+
const resolvedScalar = expandEnvReference(scalarApiKey, env);
|
|
799
|
+
if (resolvedScalar !== undefined && isPiCommandConfigValue(resolvedScalar)) {
|
|
800
|
+
const source = getPiSettingsLayerSource(resources, provider, env, 'apiKey', resolvedScalar);
|
|
801
|
+
return source ? { ...source, selected: true } : undefined;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const apiKeys = getPiHeadlessSetting(resources.settings, 'apiKeys', 'auth-fallback');
|
|
805
|
+
if (!isRecord(apiKeys) ||
|
|
806
|
+
!Object.prototype.hasOwnProperty.call(apiKeys, provider) ||
|
|
807
|
+
typeof apiKeys[provider] !== 'string') {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
const resolvedProviderKey = expandEnvReference(apiKeys[provider], env);
|
|
811
|
+
if (resolvedProviderKey === undefined || !isPiCommandConfigValue(resolvedProviderKey)) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
const source = getPiSettingsLayerSource(resources, provider, env, 'apiKeys', resolvedProviderKey);
|
|
815
|
+
return source
|
|
816
|
+
? {
|
|
817
|
+
...source,
|
|
818
|
+
selected: typeof scalarApiKey !== 'string',
|
|
819
|
+
}
|
|
820
|
+
: undefined;
|
|
821
|
+
}
|
|
822
|
+
function getPiModelsApiKeySource(resources, provider, env) {
|
|
823
|
+
const selected = resolvePiModelsApiKey(resources.models.providers[provider], env);
|
|
824
|
+
if (selected === undefined)
|
|
825
|
+
return undefined;
|
|
826
|
+
for (let index = resources.modelsLayers.length - 1; index >= 0; index -= 1) {
|
|
827
|
+
const layerValue = resources.modelsLayers[index]?.providers[provider]?.apiKey;
|
|
828
|
+
if (typeof layerValue === 'string' &&
|
|
829
|
+
expandEnvReference(layerValue, env) === selected) {
|
|
830
|
+
const isProject = index === resources.modelsLayers.length - 1 &&
|
|
831
|
+
resources.roots.agentDir !== resources.roots.projectDir;
|
|
832
|
+
return {
|
|
833
|
+
scope: isProject ? 'project' : 'user',
|
|
834
|
+
path: join(isProject ? resources.roots.projectDir : resources.roots.agentDir, 'models.json'),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
function getPiModelsCommandSource(resources, provider, env) {
|
|
841
|
+
const selected = resolvePiModelsApiKey(resources.models.providers[provider], env);
|
|
842
|
+
if (selected === undefined || !isPiCommandConfigValue(selected))
|
|
843
|
+
return undefined;
|
|
844
|
+
return getPiModelsApiKeySource(resources, provider, env);
|
|
845
|
+
}
|
|
846
|
+
function getKnownAuthMode(entry) {
|
|
847
|
+
if (!isRecord(entry))
|
|
848
|
+
return undefined;
|
|
849
|
+
if (entry.type === 'oauth')
|
|
850
|
+
return { mode: 'oauth' };
|
|
851
|
+
if (entry.type === 'api_key') {
|
|
852
|
+
const key = [entry.key, entry.apiKey, entry.token, entry.accessToken, entry.access]
|
|
853
|
+
.find((value) => typeof value === 'string');
|
|
854
|
+
return { mode: key?.startsWith('!') ? 'command' : 'api_key' };
|
|
855
|
+
}
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
402
858
|
function addPiModelEntry(entries, model) {
|
|
403
859
|
const id = `${model.provider}/${model.id}`;
|
|
404
860
|
entries.set(id, {
|
|
@@ -445,6 +901,8 @@ function resolvePiSettingsApiKey(settings, provider, env) {
|
|
|
445
901
|
const apiKeys = getPiHeadlessSetting(settings, 'apiKeys', 'auth-fallback');
|
|
446
902
|
if (!isRecord(apiKeys))
|
|
447
903
|
return undefined;
|
|
904
|
+
if (!Object.prototype.hasOwnProperty.call(apiKeys, provider))
|
|
905
|
+
return undefined;
|
|
448
906
|
const value = apiKeys[provider];
|
|
449
907
|
return typeof value === 'string' ? expandEnvReference(value, env) : undefined;
|
|
450
908
|
}
|
|
@@ -462,9 +920,8 @@ function expandEnvReference(value, env) {
|
|
|
462
920
|
function isPiCommandConfigValue(value) {
|
|
463
921
|
return value.startsWith('!');
|
|
464
922
|
}
|
|
465
|
-
function formatPiCommandResolutionError(
|
|
466
|
-
|
|
467
|
-
return new Error(`Failed to resolve API key for Pi provider "${provider}" from shell command: ${command.slice(1)}${causeMessage}`, cause instanceof Error ? { cause } : undefined);
|
|
923
|
+
function formatPiCommandResolutionError() {
|
|
924
|
+
return new Error('API key auth failed for provider: failed to resolve API key for Pi provider from the configured command.');
|
|
468
925
|
}
|
|
469
926
|
async function resolvePiProjectCommandApiKey(modelRuntime, provider, command) {
|
|
470
927
|
const isolatedProvider = `__agents_ensemble_project_auth__${provider}`;
|
|
@@ -478,19 +935,19 @@ async function resolvePiProjectCommandApiKey(modelRuntime, provider, command) {
|
|
|
478
935
|
return runtimeAuth.auth.apiKey;
|
|
479
936
|
}
|
|
480
937
|
}
|
|
481
|
-
catch
|
|
482
|
-
throw formatPiCommandResolutionError(
|
|
938
|
+
catch {
|
|
939
|
+
throw formatPiCommandResolutionError();
|
|
483
940
|
}
|
|
484
|
-
throw formatPiCommandResolutionError(
|
|
941
|
+
throw formatPiCommandResolutionError();
|
|
485
942
|
}
|
|
486
943
|
function toProviderEnv(env) {
|
|
487
944
|
return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string'));
|
|
488
945
|
}
|
|
489
946
|
/** Build the Pi 1.x runtime from the already-resolved ensemble resource view. */
|
|
490
|
-
async function createPiModelRuntime(options, resources) {
|
|
947
|
+
async function createPiModelRuntime(options, resources, authPath) {
|
|
491
948
|
const roots = resolvePiResourceRoots(options);
|
|
492
949
|
const runtime = await ModelRuntime.create({
|
|
493
|
-
authPath
|
|
950
|
+
authPath,
|
|
494
951
|
// The ensemble resource loader has already merged user and project models.
|
|
495
952
|
// Avoid letting ModelRuntime read an unmerged second view of models.json.
|
|
496
953
|
modelsPath: null,
|
|
@@ -499,7 +956,17 @@ async function createPiModelRuntime(options, resources) {
|
|
|
499
956
|
});
|
|
500
957
|
const env = options.env ?? process.env;
|
|
501
958
|
for (const [provider, config] of Object.entries(resources.models.providers)) {
|
|
502
|
-
|
|
959
|
+
const runtimeConfig = toPiRuntimeProviderConfig(config, env);
|
|
960
|
+
const modelApiKey = resolvePiModelsApiKey(config, env);
|
|
961
|
+
if (modelApiKey !== undefined && isPiCommandConfigValue(modelApiKey)) {
|
|
962
|
+
// registerProvider() starts a ModelRuntime availability refresh. Keep a
|
|
963
|
+
// models.json command out of that startup refresh and register it later,
|
|
964
|
+
// when the selected provider request resolves its auth.
|
|
965
|
+
runtime.registerProvider(provider, { ...runtimeConfig, apiKey: undefined });
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
runtime.registerProvider(provider, runtimeConfig);
|
|
969
|
+
}
|
|
503
970
|
}
|
|
504
971
|
if (roots.agentDir !== roots.projectDir) {
|
|
505
972
|
for (const [provider, entry] of Object.entries(resources.authLayers.at(-1) ?? {})) {
|
|
@@ -555,20 +1022,224 @@ function toPiRuntimeProviderConfig(config, env) {
|
|
|
555
1022
|
...(models ? { models } : {}),
|
|
556
1023
|
};
|
|
557
1024
|
}
|
|
558
|
-
async function writePiApiKey(authPath, provider, apiKey) {
|
|
559
|
-
await mkdir(dirname(authPath), { recursive: true });
|
|
1025
|
+
async function writePiApiKey(authPath, protectedProjectRoots, provider, apiKey) {
|
|
1026
|
+
await mkdir(dirname(authPath), { recursive: true, mode: 0o700 });
|
|
1027
|
+
const targetPath = await resolveSafePiUserAuthPath(authPath, protectedProjectRoots);
|
|
560
1028
|
let auth = {};
|
|
561
1029
|
try {
|
|
562
|
-
const parsed = JSON.parse(await readFile(
|
|
1030
|
+
const parsed = JSON.parse(await readFile(targetPath, 'utf8'));
|
|
563
1031
|
if (isRecord(parsed))
|
|
564
1032
|
auth = parsed;
|
|
565
1033
|
}
|
|
566
1034
|
catch (error) {
|
|
567
|
-
if (!
|
|
1035
|
+
if (!isMissingFile(error))
|
|
568
1036
|
throw error;
|
|
569
1037
|
}
|
|
570
1038
|
auth[provider] = { type: 'api_key', key: apiKey };
|
|
571
|
-
await
|
|
1039
|
+
await atomicWritePrivatePiUserFile(targetPath, `${JSON.stringify(auth, null, 2)}\n`);
|
|
1040
|
+
}
|
|
1041
|
+
async function resolveSafePiUserAuthPath(authPath, protectedProjectRoots) {
|
|
1042
|
+
if (await isPathWithinAnyRoot(authPath, protectedProjectRoots)) {
|
|
1043
|
+
throw new Error(`Refusing to use Pi user auth.json at ${authPath} because its configured path or a symlink target enters the project Pi resource root or project .ensemble layer, or resolves to a target linked from either project layer. Configure a user agentDir outside those project paths.`);
|
|
1044
|
+
}
|
|
1045
|
+
return canonicalPath(authPath);
|
|
1046
|
+
}
|
|
1047
|
+
async function resolveSafePiUserSettingsPath(settingsPath, protectedProjectRoots) {
|
|
1048
|
+
if (await isPathWithinAnyRoot(settingsPath, protectedProjectRoots)) {
|
|
1049
|
+
throw new Error(`Refusing to use Pi user settings.json at ${settingsPath} because its configured path or a symlink target enters the project Pi resource root or project .ensemble layer, or resolves to a target linked from either project layer. Configure a user agentDir outside those project paths.`);
|
|
1050
|
+
}
|
|
1051
|
+
return canonicalPath(settingsPath);
|
|
1052
|
+
}
|
|
1053
|
+
async function resolveSafePiProjectSettingsPath(settingsPath, projectDir) {
|
|
1054
|
+
const projectRoot = await canonicalPath(projectDir);
|
|
1055
|
+
const targetPath = await canonicalPath(settingsPath);
|
|
1056
|
+
if (!(await isPathWithin(projectRoot, targetPath))) {
|
|
1057
|
+
throw new Error('Refusing to write Pi project settings.json outside the configured project resource root.');
|
|
1058
|
+
}
|
|
1059
|
+
await assertRegularSettingsTarget(targetPath, settingsPath);
|
|
1060
|
+
return targetPath;
|
|
1061
|
+
}
|
|
1062
|
+
async function assertRegularSettingsTarget(targetPath, displayPath) {
|
|
1063
|
+
let details;
|
|
1064
|
+
try {
|
|
1065
|
+
details = await lstat(targetPath);
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
if (isMissingFile(error))
|
|
1069
|
+
return;
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
if (!details.isFile()) {
|
|
1073
|
+
throw new Error(`Refusing to update Pi settings at ${displayPath} because the destination is not a regular file.`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function writePiSettingsCommand(settingsPath, provider, command) {
|
|
1077
|
+
await assertRegularSettingsTarget(settingsPath, settingsPath);
|
|
1078
|
+
const settings = await readPiSettingsFile(settingsPath);
|
|
1079
|
+
const existingApiKeys = settings.apiKeys;
|
|
1080
|
+
if (existingApiKeys !== undefined && !isRecord(existingApiKeys)) {
|
|
1081
|
+
throw new Error('Pi settings apiKeys must be an object; the file was left unchanged.');
|
|
1082
|
+
}
|
|
1083
|
+
const apiKeys = { ...existingApiKeys };
|
|
1084
|
+
Object.defineProperty(apiKeys, provider, {
|
|
1085
|
+
configurable: true,
|
|
1086
|
+
enumerable: true,
|
|
1087
|
+
value: command,
|
|
1088
|
+
writable: true,
|
|
1089
|
+
});
|
|
1090
|
+
const updated = { ...settings, apiKeys };
|
|
1091
|
+
await atomicWritePrivatePiUserFile(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
|
|
1092
|
+
}
|
|
1093
|
+
async function clearPiSettingsCommand(settingsPath, provider) {
|
|
1094
|
+
await assertRegularSettingsTarget(settingsPath, settingsPath);
|
|
1095
|
+
let settings;
|
|
1096
|
+
try {
|
|
1097
|
+
settings = await readPiSettingsFile(settingsPath);
|
|
1098
|
+
}
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
if (isMissingFile(error))
|
|
1101
|
+
return false;
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
if (settings.apiKeys !== undefined && !isRecord(settings.apiKeys)) {
|
|
1105
|
+
throw new Error('Pi settings apiKeys must be an object; the file was left unchanged.');
|
|
1106
|
+
}
|
|
1107
|
+
const apiKeys = settings.apiKeys;
|
|
1108
|
+
if (!apiKeys ||
|
|
1109
|
+
!Object.prototype.hasOwnProperty.call(apiKeys, provider) ||
|
|
1110
|
+
typeof apiKeys[provider] !== 'string' ||
|
|
1111
|
+
!isPiCommandConfigValue(apiKeys[provider])) {
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
const updatedApiKeys = { ...apiKeys };
|
|
1115
|
+
delete updatedApiKeys[provider];
|
|
1116
|
+
const updated = { ...settings };
|
|
1117
|
+
if (Object.keys(updatedApiKeys).length === 0) {
|
|
1118
|
+
delete updated.apiKeys;
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
updated.apiKeys = updatedApiKeys;
|
|
1122
|
+
}
|
|
1123
|
+
await atomicWritePrivatePiUserFile(settingsPath, `${JSON.stringify(updated, null, 2)}\n`);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
function requireCommandAuthProvider(provider) {
|
|
1127
|
+
const normalized = normalizeProvider(provider);
|
|
1128
|
+
if (!normalized ||
|
|
1129
|
+
normalized !== provider ||
|
|
1130
|
+
/[\u0000-\u001f\u007f]/u.test(normalized)) {
|
|
1131
|
+
throw new Error('A valid Pi provider ID is required for command credentials.');
|
|
1132
|
+
}
|
|
1133
|
+
return normalized;
|
|
1134
|
+
}
|
|
1135
|
+
function requireCommandAuthTarget(target) {
|
|
1136
|
+
if (target === undefined || target === 'user')
|
|
1137
|
+
return 'user';
|
|
1138
|
+
if (target === 'project')
|
|
1139
|
+
return 'project';
|
|
1140
|
+
throw new Error('Pi command credentials can target only user or project settings.');
|
|
1141
|
+
}
|
|
1142
|
+
function validatePiCommandCredential(command) {
|
|
1143
|
+
if (typeof command !== 'string' ||
|
|
1144
|
+
!command.startsWith('!') ||
|
|
1145
|
+
command.slice(1).trim() === '' ||
|
|
1146
|
+
command.includes('\0')) {
|
|
1147
|
+
throw new Error('A Pi command credential must start with ! and include a non-empty command.');
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function pathExists(path) {
|
|
1151
|
+
try {
|
|
1152
|
+
await lstat(path);
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
if (isMissingFile(error))
|
|
1157
|
+
return false;
|
|
1158
|
+
throw error;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function ensurePiUserSettingsSafeForOAuth(settingsPath, protectedProjectRoots) {
|
|
1162
|
+
const targetPath = await resolveSafePiUserSettingsPath(settingsPath, protectedProjectRoots);
|
|
1163
|
+
let details;
|
|
1164
|
+
try {
|
|
1165
|
+
details = await lstat(targetPath);
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
if (isMissingFile(error))
|
|
1169
|
+
return;
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
if (!details.isFile()) {
|
|
1173
|
+
throw new Error(`Refusing to use Pi user settings.json at ${settingsPath} because it is not a regular file.`);
|
|
1174
|
+
}
|
|
1175
|
+
const needsPrivateModeReset = process.platform !== 'win32' && (details.mode & 0o777) !== 0o600;
|
|
1176
|
+
if (details.nlink > 1) {
|
|
1177
|
+
const contents = await readFile(targetPath, 'utf8');
|
|
1178
|
+
const safeTargetPath = await resolveSafePiUserSettingsPath(settingsPath, protectedProjectRoots);
|
|
1179
|
+
if (safeTargetPath !== targetPath) {
|
|
1180
|
+
throw new Error('Pi user settings.json target changed while preparing OAuth login; no credentials were written. Retry login after checking the settings.json symlink.');
|
|
1181
|
+
}
|
|
1182
|
+
await atomicWritePrivatePiUserFile(targetPath, contents);
|
|
1183
|
+
}
|
|
1184
|
+
else if (needsPrivateModeReset) {
|
|
1185
|
+
await chmod(targetPath, 0o600);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async function atomicWritePrivatePiUserFile(path, contents) {
|
|
1189
|
+
const directory = dirname(path);
|
|
1190
|
+
const temporaryPath = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
|
|
1191
|
+
let file;
|
|
1192
|
+
try {
|
|
1193
|
+
file = await open(temporaryPath, 'wx', 0o600);
|
|
1194
|
+
// Set the final private mode before any credential bytes reach the file.
|
|
1195
|
+
await file.chmod(0o600);
|
|
1196
|
+
await file.writeFile(contents, 'utf8');
|
|
1197
|
+
await file.sync();
|
|
1198
|
+
await file.close();
|
|
1199
|
+
file = undefined;
|
|
1200
|
+
await rename(temporaryPath, path);
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
await file?.close().catch(() => undefined);
|
|
1204
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function ensurePrivatePiUserAuthFile(authPath, protectedProjectRoots) {
|
|
1209
|
+
// The Pi auth store only applies its mode when creating a file. Replace any
|
|
1210
|
+
// permissive or hard-linked user file before OAuth or logout can mutate it.
|
|
1211
|
+
const targetPath = await resolveSafePiUserAuthPath(authPath, protectedProjectRoots);
|
|
1212
|
+
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
1213
|
+
const safeTargetPath = await resolveSafePiUserAuthPath(authPath, protectedProjectRoots);
|
|
1214
|
+
let details;
|
|
1215
|
+
try {
|
|
1216
|
+
details = await lstat(safeTargetPath);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
if (!isMissingFile(error))
|
|
1220
|
+
throw error;
|
|
1221
|
+
await atomicWritePrivatePiUserFile(safeTargetPath, '{}\n');
|
|
1222
|
+
return safeTargetPath;
|
|
1223
|
+
}
|
|
1224
|
+
if (!details.isFile()) {
|
|
1225
|
+
throw new Error(`Refusing to use Pi user auth.json at ${authPath} because it is not a regular file.`);
|
|
1226
|
+
}
|
|
1227
|
+
// Windows mode bits cannot distinguish owner/group/others ACLs. Treating a
|
|
1228
|
+
// writable Windows file as non-private by mode alone would replace it on
|
|
1229
|
+
// every OAuth login or logout and discard any file-specific ACL. Hardlinks
|
|
1230
|
+
// still require replacement on every platform before runtime mutation.
|
|
1231
|
+
const needsPrivateModeReset = process.platform !== 'win32' && (details.mode & 0o777) !== 0o600;
|
|
1232
|
+
if (details.nlink > 1 || needsPrivateModeReset) {
|
|
1233
|
+
const contents = await readFile(safeTargetPath, 'utf8');
|
|
1234
|
+
await atomicWritePrivatePiUserFile(safeTargetPath, contents);
|
|
1235
|
+
}
|
|
1236
|
+
return safeTargetPath;
|
|
1237
|
+
}
|
|
1238
|
+
function isMissingFile(error) {
|
|
1239
|
+
return (typeof error === 'object' &&
|
|
1240
|
+
error !== null &&
|
|
1241
|
+
'code' in error &&
|
|
1242
|
+
error.code === 'ENOENT');
|
|
572
1243
|
}
|
|
573
1244
|
function toPiAuthInteraction(callbacks) {
|
|
574
1245
|
return {
|
|
@@ -581,17 +1252,24 @@ function toPiAuthInteraction(callbacks) {
|
|
|
581
1252
|
id: option.id,
|
|
582
1253
|
label: option.label,
|
|
583
1254
|
})),
|
|
1255
|
+
...(prompt.signal ? { signal: prompt.signal } : {}),
|
|
584
1256
|
});
|
|
585
1257
|
if (selected === undefined)
|
|
586
1258
|
throw new Error('OAuth login was cancelled.');
|
|
587
1259
|
return selected;
|
|
588
1260
|
}
|
|
589
1261
|
if (prompt.type === 'manual_code' && callbacks.onManualCodeInput) {
|
|
590
|
-
return callbacks.onManualCodeInput(
|
|
1262
|
+
return callbacks.onManualCodeInput({
|
|
1263
|
+
message: prompt.message,
|
|
1264
|
+
...(prompt.placeholder ? { placeholder: prompt.placeholder } : {}),
|
|
1265
|
+
...(prompt.signal ? { signal: prompt.signal } : {}),
|
|
1266
|
+
});
|
|
591
1267
|
}
|
|
592
1268
|
return callbacks.onPrompt({
|
|
593
1269
|
message: prompt.message,
|
|
1270
|
+
type: prompt.type,
|
|
594
1271
|
...(prompt.placeholder ? { placeholder: prompt.placeholder } : {}),
|
|
1272
|
+
...(prompt.signal ? { signal: prompt.signal } : {}),
|
|
595
1273
|
});
|
|
596
1274
|
},
|
|
597
1275
|
notify: (event) => {
|