@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.
Files changed (270) hide show
  1. package/assets/pi/pi-cli-env-wrapper.cmd +28 -0
  2. package/assets/pi/pi-cli-env-wrapper.sh +19 -0
  3. package/assets/pi/settings.json +4 -0
  4. package/dist/acp/acp-preset-bins.d.ts +3 -3
  5. package/dist/acp/acp-preset-bins.d.ts.map +1 -1
  6. package/dist/acp/acp-preset-bins.js +3 -3
  7. package/dist/acp/acp-preset-bins.js.map +1 -1
  8. package/dist/acp/index.d.ts +3 -1
  9. package/dist/acp/index.d.ts.map +1 -1
  10. package/dist/acp/index.js +2 -1
  11. package/dist/acp/index.js.map +1 -1
  12. package/dist/acp/pi-acp-wrapper.d.ts +3 -0
  13. package/dist/acp/pi-acp-wrapper.d.ts.map +1 -0
  14. package/dist/acp/pi-acp-wrapper.js +41 -0
  15. package/dist/acp/pi-acp-wrapper.js.map +1 -0
  16. package/dist/acp/pi-builtin-config.d.ts +10 -0
  17. package/dist/acp/pi-builtin-config.d.ts.map +1 -0
  18. package/dist/acp/pi-builtin-config.js +36 -0
  19. package/dist/acp/pi-builtin-config.js.map +1 -0
  20. package/dist/acp/pi-runtime.d.ts +23 -0
  21. package/dist/acp/pi-runtime.d.ts.map +1 -0
  22. package/dist/acp/pi-runtime.js +160 -0
  23. package/dist/acp/pi-runtime.js.map +1 -0
  24. package/dist/acp/pi-session-map.d.ts +36 -0
  25. package/dist/acp/pi-session-map.d.ts.map +1 -0
  26. package/dist/acp/pi-session-map.js +266 -0
  27. package/dist/acp/pi-session-map.js.map +1 -0
  28. package/dist/acp/resolve-acp-spawn.d.ts +9 -3
  29. package/dist/acp/resolve-acp-spawn.d.ts.map +1 -1
  30. package/dist/acp/resolve-acp-spawn.js +41 -9
  31. package/dist/acp/resolve-acp-spawn.js.map +1 -1
  32. package/dist/acp/resolve-bundled-acp-bin.d.ts +2 -2
  33. package/dist/acp/resolve-bundled-acp-bin.d.ts.map +1 -1
  34. package/dist/acp/resolve-bundled-acp-bin.js +4 -4
  35. package/dist/acp/resolve-bundled-acp-bin.js.map +1 -1
  36. package/dist/acp/validate-acp-preset-prerequisites.d.ts +1 -1
  37. package/dist/acp/validate-acp-preset-prerequisites.d.ts.map +1 -1
  38. package/dist/acp/validate-acp-preset-prerequisites.js +30 -10
  39. package/dist/acp/validate-acp-preset-prerequisites.js.map +1 -1
  40. package/dist/conductor/conductor-agent.d.ts +15 -3
  41. package/dist/conductor/conductor-agent.d.ts.map +1 -1
  42. package/dist/conductor/conductor-auth.d.ts +4 -3
  43. package/dist/conductor/conductor-auth.d.ts.map +1 -1
  44. package/dist/conductor/conductor-auth.js +20 -4
  45. package/dist/conductor/conductor-auth.js.map +1 -1
  46. package/dist/conductor/conductor-dispatch-lifecycle.d.ts +85 -0
  47. package/dist/conductor/conductor-dispatch-lifecycle.d.ts.map +1 -0
  48. package/dist/conductor/conductor-dispatch-lifecycle.js +190 -0
  49. package/dist/conductor/conductor-dispatch-lifecycle.js.map +1 -0
  50. package/dist/conductor/conductor-history.d.ts +15 -0
  51. package/dist/conductor/conductor-history.d.ts.map +1 -1
  52. package/dist/conductor/conductor-history.js +38 -7
  53. package/dist/conductor/conductor-history.js.map +1 -1
  54. package/dist/conductor/conductor-path-safety.d.ts +23 -0
  55. package/dist/conductor/conductor-path-safety.d.ts.map +1 -0
  56. package/dist/conductor/conductor-path-safety.js +319 -0
  57. package/dist/conductor/conductor-path-safety.js.map +1 -0
  58. package/dist/conductor/conductor-pi-auth.d.ts +57 -2
  59. package/dist/conductor/conductor-pi-auth.d.ts.map +1 -1
  60. package/dist/conductor/conductor-pi-auth.js +718 -40
  61. package/dist/conductor/conductor-pi-auth.js.map +1 -1
  62. package/dist/conductor/conductor-recovery-inbox.d.ts +73 -0
  63. package/dist/conductor/conductor-recovery-inbox.d.ts.map +1 -0
  64. package/dist/conductor/conductor-recovery-inbox.js +181 -0
  65. package/dist/conductor/conductor-recovery-inbox.js.map +1 -0
  66. package/dist/conductor/conductor-send-reconnect.d.ts +33 -4
  67. package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
  68. package/dist/conductor/conductor-send-reconnect.js +89 -24
  69. package/dist/conductor/conductor-send-reconnect.js.map +1 -1
  70. package/dist/conductor/conductor-session-driver.d.ts +34 -1
  71. package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
  72. package/dist/conductor/conductor-session-driver.js +529 -75
  73. package/dist/conductor/conductor-session-driver.js.map +1 -1
  74. package/dist/conductor/conductor-session.d.ts +4 -0
  75. package/dist/conductor/conductor-session.d.ts.map +1 -1
  76. package/dist/conductor/conductor-session.js +416 -145
  77. package/dist/conductor/conductor-session.js.map +1 -1
  78. package/dist/conductor/conductor-setup.d.ts +30 -0
  79. package/dist/conductor/conductor-setup.d.ts.map +1 -0
  80. package/dist/conductor/conductor-setup.js +243 -0
  81. package/dist/conductor/conductor-setup.js.map +1 -0
  82. package/dist/conductor/cursor-bootstrap-state.d.ts +9 -0
  83. package/dist/conductor/cursor-bootstrap-state.d.ts.map +1 -0
  84. package/dist/conductor/cursor-bootstrap-state.js +11 -0
  85. package/dist/conductor/cursor-bootstrap-state.js.map +1 -0
  86. package/dist/conductor/cursor-run-snapshots.d.ts +30 -0
  87. package/dist/conductor/cursor-run-snapshots.d.ts.map +1 -0
  88. package/dist/conductor/cursor-run-snapshots.js +120 -0
  89. package/dist/conductor/cursor-run-snapshots.js.map +1 -0
  90. package/dist/conductor/index.d.ts +18 -7
  91. package/dist/conductor/index.d.ts.map +1 -1
  92. package/dist/conductor/index.js +9 -4
  93. package/dist/conductor/index.js.map +1 -1
  94. package/dist/conductor/operator-command.d.ts +5 -0
  95. package/dist/conductor/operator-command.d.ts.map +1 -1
  96. package/dist/conductor/operator-command.js +6 -0
  97. package/dist/conductor/operator-command.js.map +1 -1
  98. package/dist/conductor/operator-input-binding.d.ts +17 -0
  99. package/dist/conductor/operator-input-binding.d.ts.map +1 -1
  100. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts +45 -0
  101. package/dist/conductor/pi-auth-recovery-snapshot-store.d.ts.map +1 -0
  102. package/dist/conductor/pi-auth-recovery-snapshot-store.js +116 -0
  103. package/dist/conductor/pi-auth-recovery-snapshot-store.js.map +1 -0
  104. package/dist/conductor/pi-conductor-agent.d.ts +6 -3
  105. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  106. package/dist/conductor/pi-conductor-agent.js +276 -53
  107. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  108. package/dist/conductor/pi-conductor-session-file.d.ts +13 -0
  109. package/dist/conductor/pi-conductor-session-file.d.ts.map +1 -0
  110. package/dist/conductor/pi-conductor-session-file.js +21 -0
  111. package/dist/conductor/pi-conductor-session-file.js.map +1 -0
  112. package/dist/conductor/pi-resource-loader.d.ts +6 -0
  113. package/dist/conductor/pi-resource-loader.d.ts.map +1 -1
  114. package/dist/conductor/pi-resource-loader.js +26 -12
  115. package/dist/conductor/pi-resource-loader.js.map +1 -1
  116. package/dist/conductor/pi-user-root-lock.d.ts +8 -0
  117. package/dist/conductor/pi-user-root-lock.d.ts.map +1 -0
  118. package/dist/conductor/pi-user-root-lock.js +53 -0
  119. package/dist/conductor/pi-user-root-lock.js.map +1 -0
  120. package/dist/conductor/recovery-inbox-command.d.ts +29 -0
  121. package/dist/conductor/recovery-inbox-command.d.ts.map +1 -0
  122. package/dist/conductor/recovery-inbox-command.js +248 -0
  123. package/dist/conductor/recovery-inbox-command.js.map +1 -0
  124. package/dist/conductor/session/events/index.d.ts +1 -1
  125. package/dist/conductor/session/events/index.d.ts.map +1 -1
  126. package/dist/conductor/session/events/index.js.map +1 -1
  127. package/dist/conductor/session/events/session-event.d.ts +43 -1
  128. package/dist/conductor/session/events/session-event.d.ts.map +1 -1
  129. package/dist/conductor/session/events/session-log-event-groups.d.ts +1 -1
  130. package/dist/conductor/session/events/session-log-event-groups.d.ts.map +1 -1
  131. package/dist/conductor/session/events/session-log-event-groups.js +2 -0
  132. package/dist/conductor/session/events/session-log-event-groups.js.map +1 -1
  133. package/dist/conductor/session/events/session-log-event.d.ts +4 -0
  134. package/dist/conductor/session/events/session-log-event.d.ts.map +1 -1
  135. package/dist/conductor/session/format-session-event.js +57 -5
  136. package/dist/conductor/session/format-session-event.js.map +1 -1
  137. package/dist/conductor/session/select-dispatch-batch.d.ts +2 -0
  138. package/dist/conductor/session/select-dispatch-batch.d.ts.map +1 -1
  139. package/dist/conductor/session/select-dispatch-batch.js +36 -2
  140. package/dist/conductor/session/select-dispatch-batch.js.map +1 -1
  141. package/dist/conductor/session/session-event-queue.d.ts +14 -0
  142. package/dist/conductor/session/session-event-queue.d.ts.map +1 -1
  143. package/dist/conductor/session/session-event-queue.js +82 -10
  144. package/dist/conductor/session/session-event-queue.js.map +1 -1
  145. package/dist/conductor/session/session-event.d.ts +1 -1
  146. package/dist/conductor/session/session-event.d.ts.map +1 -1
  147. package/dist/conductor/session-policy.d.ts.map +1 -1
  148. package/dist/conductor/session-policy.js +8 -4
  149. package/dist/conductor/session-policy.js.map +1 -1
  150. package/dist/conductor/submit-operator-input.d.ts +3 -0
  151. package/dist/conductor/submit-operator-input.d.ts.map +1 -1
  152. package/dist/conductor/submit-operator-input.js +21 -0
  153. package/dist/conductor/submit-operator-input.js.map +1 -1
  154. package/dist/config/deep-merge.d.ts.map +1 -1
  155. package/dist/config/deep-merge.js +13 -3
  156. package/dist/config/deep-merge.js.map +1 -1
  157. package/dist/config/defaults.d.ts.map +1 -1
  158. package/dist/config/defaults.js +1 -1
  159. package/dist/config/defaults.js.map +1 -1
  160. package/dist/config/doctor-settings.d.ts +59 -0
  161. package/dist/config/doctor-settings.d.ts.map +1 -0
  162. package/dist/config/doctor-settings.js +343 -0
  163. package/dist/config/doctor-settings.js.map +1 -0
  164. package/dist/config/ensemble-config-digest.d.ts +2 -4
  165. package/dist/config/ensemble-config-digest.d.ts.map +1 -1
  166. package/dist/config/ensemble-config-digest.js +2 -4
  167. package/dist/config/ensemble-config-digest.js.map +1 -1
  168. package/dist/config/index.d.ts +3 -1
  169. package/dist/config/index.d.ts.map +1 -1
  170. package/dist/config/index.js +1 -0
  171. package/dist/config/index.js.map +1 -1
  172. package/dist/config/load-ensemble-config.d.ts.map +1 -1
  173. package/dist/config/load-ensemble-config.js +8 -1
  174. package/dist/config/load-ensemble-config.js.map +1 -1
  175. package/dist/config/parse-config.d.ts.map +1 -1
  176. package/dist/config/parse-config.js +34 -14
  177. package/dist/config/parse-config.js.map +1 -1
  178. package/dist/config/resolve-settings.d.ts +3 -1
  179. package/dist/config/resolve-settings.d.ts.map +1 -1
  180. package/dist/config/resolve-settings.js +7 -4
  181. package/dist/config/resolve-settings.js.map +1 -1
  182. package/dist/config/types.d.ts +14 -5
  183. package/dist/config/types.d.ts.map +1 -1
  184. package/dist/dispatch/attach-worker.js +1 -1
  185. package/dist/dispatch/attach-worker.js.map +1 -1
  186. package/dist/dispatch/worker-acp-session.d.ts +21 -3
  187. package/dist/dispatch/worker-acp-session.d.ts.map +1 -1
  188. package/dist/dispatch/worker-acp-session.js +50 -13
  189. package/dist/dispatch/worker-acp-session.js.map +1 -1
  190. package/dist/ensemble-paths.d.ts +19 -19
  191. package/dist/ensemble-paths.d.ts.map +1 -1
  192. package/dist/ensemble-paths.js +40 -22
  193. package/dist/ensemble-paths.js.map +1 -1
  194. package/dist/issue/resolve-issue-url.d.ts +4 -0
  195. package/dist/issue/resolve-issue-url.d.ts.map +1 -1
  196. package/dist/issue/resolve-issue-url.js +2 -2
  197. package/dist/issue/resolve-issue-url.js.map +1 -1
  198. package/dist/operator-runtime.d.ts +19 -4
  199. package/dist/operator-runtime.d.ts.map +1 -1
  200. package/dist/operator-runtime.js +13 -2
  201. package/dist/operator-runtime.js.map +1 -1
  202. package/dist/profile/types.d.ts +4 -0
  203. package/dist/profile/types.d.ts.map +1 -1
  204. package/dist/profile/types.js +29 -12
  205. package/dist/profile/types.js.map +1 -1
  206. package/dist/representation/escape-terminal-control-characters.d.ts +9 -0
  207. package/dist/representation/escape-terminal-control-characters.d.ts.map +1 -0
  208. package/dist/representation/escape-terminal-control-characters.js +34 -0
  209. package/dist/representation/escape-terminal-control-characters.js.map +1 -0
  210. package/dist/representation/index.d.ts +1 -0
  211. package/dist/representation/index.d.ts.map +1 -1
  212. package/dist/representation/index.js +1 -0
  213. package/dist/representation/index.js.map +1 -1
  214. package/dist/runtime/types.d.ts +9 -0
  215. package/dist/runtime/types.d.ts.map +1 -1
  216. package/dist/runtime/worker-runtime.d.ts +14 -1
  217. package/dist/runtime/worker-runtime.d.ts.map +1 -1
  218. package/dist/runtime/worker-runtime.js +197 -50
  219. package/dist/runtime/worker-runtime.js.map +1 -1
  220. package/dist/runtime/worker-session.d.ts +2 -1
  221. package/dist/runtime/worker-session.d.ts.map +1 -1
  222. package/dist/runtime/worker-session.js +1 -0
  223. package/dist/runtime/worker-session.js.map +1 -1
  224. package/dist/session/export-session-bundle.d.ts +77 -0
  225. package/dist/session/export-session-bundle.d.ts.map +1 -0
  226. package/dist/session/export-session-bundle.js +640 -0
  227. package/dist/session/export-session-bundle.js.map +1 -0
  228. package/dist/session/index.d.ts +6 -3
  229. package/dist/session/index.d.ts.map +1 -1
  230. package/dist/session/index.js +4 -1
  231. package/dist/session/index.js.map +1 -1
  232. package/dist/session/open-question-control.d.ts +9 -0
  233. package/dist/session/open-question-control.d.ts.map +1 -1
  234. package/dist/session/open-question-control.js +26 -1
  235. package/dist/session/open-question-control.js.map +1 -1
  236. package/dist/session/open-questions-store.d.ts +1 -1
  237. package/dist/session/open-questions-store.d.ts.map +1 -1
  238. package/dist/session/open-questions-store.js +3 -6
  239. package/dist/session/open-questions-store.js.map +1 -1
  240. package/dist/session/persisted-file.d.ts +5 -0
  241. package/dist/session/persisted-file.d.ts.map +1 -0
  242. package/dist/session/persisted-file.js +56 -0
  243. package/dist/session/persisted-file.js.map +1 -0
  244. package/dist/session/session-resume-store.d.ts +79 -10
  245. package/dist/session/session-resume-store.d.ts.map +1 -1
  246. package/dist/session/session-resume-store.js +353 -83
  247. package/dist/session/session-resume-store.js.map +1 -1
  248. package/dist/session/session-snapshot-paths.d.ts +11 -0
  249. package/dist/session/session-snapshot-paths.d.ts.map +1 -0
  250. package/dist/session/session-snapshot-paths.js +41 -0
  251. package/dist/session/session-snapshot-paths.js.map +1 -0
  252. package/dist/session/session-state-store.d.ts +2 -2
  253. package/dist/session/session-state-store.d.ts.map +1 -1
  254. package/dist/session/session-state-store.js +5 -16
  255. package/dist/session/session-state-store.js.map +1 -1
  256. package/dist/session/tool-call-snapshot-history.d.ts +12 -1
  257. package/dist/session/tool-call-snapshot-history.d.ts.map +1 -1
  258. package/dist/session/tool-call-snapshot-history.js +75 -70
  259. package/dist/session/tool-call-snapshot-history.js.map +1 -1
  260. package/dist/session/worker-session-history.d.ts +51 -0
  261. package/dist/session/worker-session-history.d.ts.map +1 -0
  262. package/dist/session/worker-session-history.js +654 -0
  263. package/dist/session/worker-session-history.js.map +1 -0
  264. package/dist/workspace/index.d.ts +1 -1
  265. package/dist/workspace/index.d.ts.map +1 -1
  266. package/dist/worktree/worktree.d.ts +6 -1
  267. package/dist/worktree/worktree.d.ts.map +1 -1
  268. package/dist/worktree/worktree.js +123 -2
  269. package/dist/worktree/worktree.js.map +1 -1
  270. 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, writeFile } from 'node:fs/promises';
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 { loadPiResources, resolvePiResourceRoots, } from './pi-resource-loader.js';
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 createPiConductorAuthContext(options, resources);
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
- const roots = resolvePiResourceRoots(options);
40
- await context.modelRuntime.login(provider, 'oauth', toPiAuthInteraction(options.oauthCallbacks), {
41
- getDeviceId: () => SettingsManager.create(options.cwd, roots.agentDir).getOrCreateDeviceId(),
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. Pass a secret API-key prompt value or set ${formatPiProviderEnvHint(provider)}.`);
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 createPiConductorAuthContext(options, resources);
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 (error) {
165
- runtimeError = error;
302
+ catch {
166
303
  // Non-command references can still use the ensemble fallback chain. A
167
- // command reference is reported below instead of returning the raw command
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(options.provider, commandKey, runtimeError);
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 { provider, configured: true, source: 'project_stored' };
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.configured || registryStatus.source === 'stored') {
373
- return normalizePiAuthStatus(provider, registryStatus, true);
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
- return { provider, configured: true, source: 'settings_json' };
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
- return { provider, configured: true, source: 'models_json_key' };
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 normalizePiAuthStatus(provider, registryStatus, false);
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(provider, command, cause) {
466
- const causeMessage = cause instanceof Error ? ` (${cause.message})` : '';
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 (error) {
482
- throw formatPiCommandResolutionError(provider, command, error);
938
+ catch {
939
+ throw formatPiCommandResolutionError();
483
940
  }
484
- throw formatPiCommandResolutionError(provider, command, undefined);
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: join(roots.agentDir, 'auth.json'),
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
- runtime.registerProvider(provider, toPiRuntimeProviderConfig(config, env));
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(authPath, 'utf8'));
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 (!isRecord(error) || error.code !== 'ENOENT')
1035
+ if (!isMissingFile(error))
568
1036
  throw error;
569
1037
  }
570
1038
  auth[provider] = { type: 'api_key', key: apiKey };
571
- await writeFile(authPath, `${JSON.stringify(auth, null, 2)}\n`, 'utf8');
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) => {