@fastagent-sh/fastagent 0.22.1 → 0.23.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 (103) hide show
  1. package/dist/channels/agentcore-protocol.d.ts +2 -1
  2. package/dist/channels/agentcore-service.js +10 -18
  3. package/dist/channels/agentcore.js +6 -3
  4. package/dist/channels/control.js +23 -0
  5. package/dist/channels/slack/config-api.d.ts +2 -2
  6. package/dist/channels/slack/config-api.js +2 -2
  7. package/dist/channels/slack/onboard.js +2 -2
  8. package/dist/channels/slack/register-webhook.js +2 -2
  9. package/dist/channels/slack/slack-api.js +4 -4
  10. package/dist/cli/add-feishu.js +4 -2
  11. package/dist/cli/auth-view.d.ts +15 -6
  12. package/dist/cli/auth-view.js +24 -8
  13. package/dist/cli/box-login.d.ts +33 -0
  14. package/dist/cli/box-login.js +162 -0
  15. package/dist/cli/commands/deploy/agentcore.js +48 -11
  16. package/dist/cli/commands/deploy/docker.js +24 -13
  17. package/dist/cli/commands/deploy/fly.js +30 -7
  18. package/dist/cli/commands/deploy/railway.js +21 -7
  19. package/dist/cli/commands/deploy/shared.d.ts +18 -17
  20. package/dist/cli/commands/deploy/shared.js +20 -21
  21. package/dist/cli/commands/destroy.js +2 -2
  22. package/dist/cli/commands/info.js +9 -6
  23. package/dist/cli/commands/invoke.js +2 -2
  24. package/dist/cli/commands/login.d.ts +7 -1
  25. package/dist/cli/commands/login.js +123 -10
  26. package/dist/cli/commands/logs.js +2 -2
  27. package/dist/cli/commands/models.d.ts +4 -1
  28. package/dist/cli/commands/models.js +37 -4
  29. package/dist/cli/commands/routine-run.js +2 -2
  30. package/dist/cli/commands/start.d.ts +0 -5
  31. package/dist/cli/commands/start.js +3 -13
  32. package/dist/cli/kernel.d.ts +2 -0
  33. package/dist/cli/kernel.js +1 -1
  34. package/dist/cli/login-relay.d.ts +38 -0
  35. package/dist/cli/login-relay.js +121 -0
  36. package/dist/cli/program.js +46 -8
  37. package/dist/cli/shared.d.ts +4 -4
  38. package/dist/cli/shared.js +24 -30
  39. package/dist/deploy/agentcore/plan.d.ts +10 -11
  40. package/dist/deploy/agentcore/plan.js +8 -14
  41. package/dist/deploy/agentcore/run.d.ts +8 -1
  42. package/dist/deploy/agentcore/run.js +20 -14
  43. package/dist/deploy/agentcore/shell.d.ts +25 -0
  44. package/dist/deploy/agentcore/shell.js +202 -0
  45. package/dist/deploy/box-shell.d.ts +43 -0
  46. package/dist/deploy/box-shell.js +28 -0
  47. package/dist/deploy/build-context.js +16 -7
  48. package/dist/deploy/channel-ingress.d.ts +20 -1
  49. package/dist/deploy/channel-ingress.js +34 -7
  50. package/dist/deploy/container.d.ts +14 -0
  51. package/dist/deploy/container.js +45 -1
  52. package/dist/deploy/docker/plan.d.ts +7 -3
  53. package/dist/deploy/docker/plan.js +9 -14
  54. package/dist/deploy/docker/run.d.ts +14 -7
  55. package/dist/deploy/docker/run.js +58 -35
  56. package/dist/deploy/fly/plan.d.ts +6 -3
  57. package/dist/deploy/fly/plan.js +6 -8
  58. package/dist/deploy/fly/run.d.ts +8 -2
  59. package/dist/deploy/fly/run.js +15 -25
  60. package/dist/deploy/preflight.d.ts +9 -7
  61. package/dist/deploy/preflight.js +74 -20
  62. package/dist/deploy/railway/plan.d.ts +6 -3
  63. package/dist/deploy/railway/plan.js +7 -9
  64. package/dist/deploy/railway/run.d.ts +10 -6
  65. package/dist/deploy/railway/run.js +29 -13
  66. package/dist/deploy/runner.d.ts +16 -0
  67. package/dist/deploy/runner.js +31 -0
  68. package/dist/deploy/secrets.d.ts +4 -13
  69. package/dist/deploy/secrets.js +8 -27
  70. package/dist/dev-supervisor.js +5 -2
  71. package/dist/engines/pi/agent-models.d.ts +70 -0
  72. package/dist/engines/pi/agent-models.js +93 -0
  73. package/dist/engines/pi/auth.d.ts +56 -3
  74. package/dist/engines/pi/auth.js +63 -16
  75. package/dist/engines/pi/config.d.ts +0 -25
  76. package/dist/engines/pi/config.js +3 -37
  77. package/dist/engines/pi/create.d.ts +22 -16
  78. package/dist/engines/pi/create.js +28 -28
  79. package/dist/engines/pi/entry-images.d.ts +14 -0
  80. package/dist/engines/pi/entry-images.js +23 -0
  81. package/dist/engines/pi/invoke-session.js +6 -4
  82. package/dist/engines/pi/login.d.ts +20 -13
  83. package/dist/engines/pi/login.js +19 -12
  84. package/dist/engines/pi/models.d.ts +115 -36
  85. package/dist/engines/pi/models.js +218 -48
  86. package/dist/engines/pi/open.d.ts +42 -11
  87. package/dist/engines/pi/open.js +53 -21
  88. package/dist/engines/pi/service.d.ts +2 -2
  89. package/dist/engines/pi/service.js +1 -0
  90. package/dist/engines/pi/session-builder.d.ts +3 -2
  91. package/dist/engines/pi/session-builder.js +28 -58
  92. package/dist/engines/pi/session-control.js +14 -3
  93. package/dist/engines/pi/turn-kit.js +11 -5
  94. package/dist/open-url.d.ts +15 -1
  95. package/dist/open-url.js +35 -3
  96. package/dist/paths.d.ts +4 -2
  97. package/dist/paths.js +6 -2
  98. package/dist/pi.d.ts +5 -3
  99. package/dist/pi.js +3 -2
  100. package/dist/service.d.ts +5 -8
  101. package/dist/session-remote.js +21 -0
  102. package/dist/session.d.ts +28 -7
  103. package/package.json +2 -1
@@ -0,0 +1,93 @@
1
+ import { ModelRuntime } from "@earendil-works/pi-coding-agent";
2
+ import { GLOBAL_AUTH_PATH, assertOneCredentialSource, fastagentCredentialStore, resolveAuthLayers, } from "./auth.js";
3
+ import { createPiModelRuntime, environmentAuthSource, modelRuntimeFiles, piModelsOver, probeAuthSource, } from "./models.js";
4
+ export function agentModels(agentDir, source = {}, options = {}) {
5
+ assertOneCredentialSource(source);
6
+ const { providers, machineLayer } = options;
7
+ const files = source.credentialStore
8
+ ? undefined
9
+ : agentDir
10
+ ? resolveAuthLayers(agentDir, source.authPath)
11
+ : { path: source.authPath ?? GLOBAL_AUTH_PATH };
12
+ const store = files
13
+ ? agentCredentialStore(files, { agentDir, providers, machineLayer, warn: source.warn })
14
+ : undefined;
15
+ const credentials = source.credentialStore ?? store;
16
+ let registry;
17
+ const runtime = () => {
18
+ registry ??= createPiModelRuntime({
19
+ credentials,
20
+ ...(agentDir ? { agentDir } : {}),
21
+ ...(providers ? { providers } : {}),
22
+ ...(machineLayer !== undefined ? { machineLayer } : {}),
23
+ });
24
+ return registry;
25
+ };
26
+ return {
27
+ ...(files ? { auth: files } : {}),
28
+ credentials,
29
+ runtime,
30
+ async authStatus(provider, modelId) {
31
+ const models = await runtime();
32
+ const id = modelId ?? models.getProvider(provider)?.getModels()[0]?.id;
33
+ let resolved = {};
34
+ if (id !== undefined) {
35
+ try {
36
+ const found = await probeAuthSource(models, `${provider}/${id}`);
37
+ if (found !== undefined)
38
+ resolved = { source: found };
39
+ }
40
+ catch (error) {
41
+ // The first line: pi folds the request's stack into the message of a failed refresh.
42
+ resolved = { error: String(error.message).split("\n")[0] };
43
+ }
44
+ }
45
+ // `read` never refreshes: this is the kind on file, whatever became of it.
46
+ const stored = (await credentials.read(provider))?.type;
47
+ const fromEnvironment = stored === undefined ? undefined : await environmentAuthSource(provider, process.env);
48
+ const shadowed = resolved.source !== undefined && fromEnvironment !== resolved.source ? fromEnvironment : undefined;
49
+ return {
50
+ ...(store ? { path: await store.layerOf(provider) } : {}),
51
+ ...resolved,
52
+ ...(stored !== undefined ? { stored } : {}),
53
+ ...(shadowed !== undefined ? { shadowed } : {}),
54
+ };
55
+ },
56
+ };
57
+ }
58
+ /** A `Models` with every built-in pi provider, wired to fastagent's auth. */
59
+ export function createPiModels(options = {}) {
60
+ return piModelsOver(agentModels(undefined, options).credentials, options.providers);
61
+ }
62
+ /**
63
+ * The credential store an agent reads through: its own file, then the user-global one for a provider the PROJECT
64
+ * authenticates no other way (its own auth file, a models.json key, or the environment). "No other way" is answered
65
+ * by pi's own resolution over a registry that has no global layer, built the first time a provider is looked up
66
+ * there. A deployment never has the global file, so a global login that outranked, say, an `ANTHROPIC_API_KEY` in
67
+ * `.secrets/.env` would run one credential here and a different one deployed.
68
+ */
69
+ function agentCredentialStore(auth, options) {
70
+ const { warn, providers = [] } = options;
71
+ const fallback = auth.fallback;
72
+ if (fallback === undefined)
73
+ return fastagentCredentialStore(auth.path, { warn });
74
+ let project;
75
+ const projectRuntime = () => {
76
+ project ??= (async () => {
77
+ const runtime = await ModelRuntime.create({
78
+ credentials: fastagentCredentialStore(auth.path, { warn }),
79
+ ...(await modelRuntimeFiles(options)).create,
80
+ refreshOnCreate: false,
81
+ });
82
+ for (const provider of providers)
83
+ runtime.registerNativeProvider(provider);
84
+ return runtime;
85
+ })();
86
+ return project;
87
+ };
88
+ return fastagentCredentialStore(auth.path, {
89
+ warn,
90
+ fallbackPath: fallback,
91
+ projectAuthenticates: async (providerId) => (await (await projectRuntime()).checkAuth(providerId)) !== undefined,
92
+ });
93
+ }
@@ -8,15 +8,68 @@ export interface FastagentAuthOptions {
8
8
  /** Sink for non-fatal auth anomalies (unreadable/corrupt file). */
9
9
  warn?: (message: string) => void;
10
10
  }
11
+ /**
12
+ * Where a caller's model credentials live: fastagent's JSON file (`authPath`, or the default layers when neither is
13
+ * given), or a {@link CredentialStore} the caller owns (an OS keychain, Electron `safeStorage`). A supplied store
14
+ * replaces the FILES: none is read or written, and no global layer applies. Env variables and `models.json` keys still
15
+ * apply, below a stored credential, exactly as with `authPath`. The store owns what the file store does
16
+ * for its file: `modify` for one provider must run one at a time, because an OAuth refresh happens inside it and a
17
+ * rotated refresh token must never be used twice; and a store it cannot parse must not be answered as empty, or the
18
+ * next write replaces every provider's credential.
19
+ */
20
+ export interface CredentialSourceOptions {
21
+ /** fastagent's credentials file. Mutually exclusive with {@link credentialStore}. */
22
+ authPath?: string;
23
+ /** The caller's own store, read and written instead of any file. Mutually exclusive with {@link authPath}. */
24
+ credentialStore?: CredentialStore;
25
+ }
26
+ /** The one refusal for a caller that names both sources: silently preferring one would hide the other's mistake. */
27
+ export declare function assertOneCredentialSource(options: CredentialSourceOptions): void;
28
+ /** A credential store over fastagent's files, which can also say which file a provider belongs to. */
29
+ export type FastagentCredentialStore = CredentialStore & {
30
+ /**
31
+ * The file this store reads a provider from and writes its refresh to: where it already is, else the primary. The
32
+ * one answer to "which file do I edit", so a report naming a file cannot disagree with the store that reads it.
33
+ */
34
+ layerOf(providerId: string): Promise<string>;
35
+ };
11
36
  /**
12
37
  * A read-write `CredentialStore` over the given credentials file (default {@link GLOBAL_AUTH_PATH}; the directory
13
- * opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file.
38
+ * opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file. A path is
39
+ * read like `FASTAGENT_AUTH_PATH` (`~` is the home directory, a relative one resolves against the cwd), so every
40
+ * caller that names a file names the same one.
14
41
  *
15
42
  * The fallback is PER PROVIDER, not per file: logging one provider into a project must not hide the others a person
16
43
  * already has globally. And the layer a credential was READ from is the layer its refresh is written back to —
17
44
  * anything else would conjure a second holder of the same OAuth grant, which is the failure this whole area exists
18
45
  * to avoid. A provider present in neither layer is new, and new credentials belong to the primary.
46
+ *
47
+ * The fallback also yields to `projectAuthenticates`: a provider the project authenticates some other way (an env key,
48
+ * its own models.json) never reaches it — for reading, listing or writing back.
19
49
  */
20
- export declare function fastagentCredentialStore(authPath?: string, options?: FastagentAuthOptions & {
50
+ export declare function fastagentCredentialStore(path?: string, options?: FastagentAuthOptions & {
21
51
  fallbackPath?: string;
22
- }): CredentialStore;
52
+ /** Whether the project authenticates this provider without the fallback; then the fallback is not read for it. */
53
+ projectAuthenticates?: (providerId: string) => Promise<boolean>;
54
+ }): FastagentCredentialStore;
55
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
56
+ export declare function resolveAuthPath(dir: string, flag?: string, env?: NodeJS.ProcessEnv): string;
57
+ /**
58
+ * Which credentials files an agent reads: `path` first, then, per provider, `fallback`. One value because it is one
59
+ * decision, read through `agentModels` (agent-models.ts) by everything that reads an agent's credentials.
60
+ */
61
+ export interface AuthLayers {
62
+ path: string;
63
+ fallback?: string;
64
+ }
65
+ /**
66
+ * The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file, and
67
+ * behind it the user-global store, because a login is a PERSON on a machine and not a project — one `login -g` then
68
+ * serves every agent here.
69
+ *
70
+ * No fallback when a path was named: "use this file" is an instruction, not a preference. BOTH knobs
71
+ * {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what the
72
+ * Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing else
73
+ * (the artifact is the truth).
74
+ */
75
+ export declare function resolveAuthLayers(agentDir: string, authPath?: string, env?: NodeJS.ProcessEnv): AuthLayers;
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Auth for the pi engine: a read-WRITE {@link CredentialStore} over a fastagent credentials file, consumed by the
3
- * `Models` collection (models.ts). The write path refuses to overwrite a corrupt file, so a torn read never clobbers
4
- * the other providers' credentials.
3
+ * `Models` collection (models.ts), and which of those files an agent reads. The write path refuses to overwrite a
4
+ * corrupt file, so a torn read never clobbers the other providers' credentials.
5
5
  */
6
6
  import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, writeFileSync } from "node:fs";
7
- import { homedir } from "node:os";
8
7
  import { dirname, join, resolve } from "node:path";
9
- import { GLOBAL_HOME_DIR, SECRETS_DIRNAME, SECRET_FILE_MODE } from "../../paths.js";
8
+ import { globalHome, SECRETS_DIRNAME, SECRET_FILE_MODE, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
10
9
  import { writeFileAtomic } from "../../atomic-write.js";
11
10
  import { log } from "../../log.js";
12
11
  import lockfile from "proper-lockfile";
@@ -14,7 +13,13 @@ import lockfile from "proper-lockfile";
14
13
  * The GLOBAL fastagent credentials file (distinct from pi's `~/.pi`), under the user-global machinery home
15
14
  * `~/.fastagent/`.
16
15
  */
17
- export const GLOBAL_AUTH_PATH = join(homedir(), GLOBAL_HOME_DIR, SECRETS_DIRNAME, "auth.json");
16
+ export const GLOBAL_AUTH_PATH = join(globalHome(), SECRETS_DIRNAME, "auth.json");
17
+ /** The one refusal for a caller that names both sources: silently preferring one would hide the other's mistake. */
18
+ export function assertOneCredentialSource(options) {
19
+ if (options.authPath !== undefined && options.credentialStore !== undefined) {
20
+ throw new Error("pass authPath or credentialStore, not both: a supplied store replaces the credentials file");
21
+ }
22
+ }
18
23
  /** A valid stored credential, or undefined — a foreign/old entry reads as not-configured, not a crash. */
19
24
  function pick(creds, providerId) {
20
25
  const cred = creds[providerId];
@@ -153,16 +158,25 @@ function parseForWrite(raw, where) {
153
158
  }
154
159
  /**
155
160
  * A read-write `CredentialStore` over the given credentials file (default {@link GLOBAL_AUTH_PATH}; the directory
156
- * opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file.
161
+ * opener passes the project-level `<root>/.secrets/auth.json`), optionally falling back to a second file. A path is
162
+ * read like `FASTAGENT_AUTH_PATH` (`~` is the home directory, a relative one resolves against the cwd), so every
163
+ * caller that names a file names the same one.
157
164
  *
158
165
  * The fallback is PER PROVIDER, not per file: logging one provider into a project must not hide the others a person
159
166
  * already has globally. And the layer a credential was READ from is the layer its refresh is written back to —
160
167
  * anything else would conjure a second holder of the same OAuth grant, which is the failure this whole area exists
161
168
  * to avoid. A provider present in neither layer is new, and new credentials belong to the primary.
169
+ *
170
+ * The fallback also yields to `projectAuthenticates`: a provider the project authenticates some other way (an env key,
171
+ * its own models.json) never reaches it — for reading, listing or writing back.
162
172
  */
163
- export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options = {}) {
173
+ export function fastagentCredentialStore(path, options = {}) {
164
174
  const warn = options.warn ?? ((message) => log.warn(message));
165
- const fallback = options.fallbackPath !== undefined && options.fallbackPath !== authPath ? options.fallbackPath : undefined;
175
+ const authPath = resolveOverridePath(path) ?? GLOBAL_AUTH_PATH;
176
+ const fallbackPath = resolveOverridePath(options.fallbackPath);
177
+ const fallback = fallbackPath !== undefined && fallbackPath !== authPath ? fallbackPath : undefined;
178
+ /** The fallback, when it may serve this provider at all. */
179
+ const fallbackFor = async (providerId) => fallback === undefined || (await options.projectAuthenticates?.(providerId)) ? undefined : fallback;
166
180
  /**
167
181
  * The file that owns this provider: where it already is, else the primary.
168
182
  *
@@ -171,18 +185,21 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
171
185
  * "one grant, one copy" rule can be lost. Re-checking under the lock means locking both files in a fixed order;
172
186
  * worth it only if concurrent logins stop being a rounding error.
173
187
  */
174
- const owner = (providerId) => {
175
- if (fallback === undefined)
188
+ const owner = async (providerId) => {
189
+ const second = await fallbackFor(providerId);
190
+ if (second === undefined)
176
191
  return authPath;
177
192
  const primary = readCreds(authPath, warn);
178
193
  if (primary && pick(primary, providerId))
179
194
  return authPath;
180
- const secondary = readCreds(fallback, warn);
181
- return secondary && pick(secondary, providerId) ? fallback : authPath;
195
+ const secondary = readCreds(second, warn);
196
+ return secondary && pick(secondary, providerId) ? second : authPath;
182
197
  };
183
198
  return {
199
+ layerOf: owner,
184
200
  async read(providerId) {
185
- for (const path of fallback === undefined ? [authPath] : [authPath, fallback]) {
201
+ const second = await fallbackFor(providerId);
202
+ for (const path of second === undefined ? [authPath] : [authPath, second]) {
186
203
  const creds = readCreds(path, warn);
187
204
  const found = creds && pick(creds, providerId);
188
205
  if (found)
@@ -196,14 +213,16 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
196
213
  const infos = new Map();
197
214
  for (const path of fallback === undefined ? [authPath] : [fallback, authPath]) {
198
215
  for (const [providerId, cred] of Object.entries(readCreds(path, warn) ?? {})) {
216
+ if (path === fallback && (await fallbackFor(providerId)) === undefined)
217
+ continue;
199
218
  if (cred && (cred.type === "oauth" || cred.type === "api_key"))
200
219
  infos.set(providerId, { providerId, type: cred.type });
201
220
  }
202
221
  }
203
222
  return [...infos.values()];
204
223
  },
205
- modify(providerId, fn) {
206
- const path = owner(providerId);
224
+ async modify(providerId, fn) {
225
+ const path = await owner(providerId);
207
226
  return withLockedAuthFile(path, async (current) => {
208
227
  const creds = parseForWrite(current, path); // corrupt → throw → no clobber
209
228
  const next = await fn(pick(creds, providerId));
@@ -214,7 +233,7 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
214
233
  });
215
234
  },
216
235
  async delete(providerId) {
217
- const path = owner(providerId);
236
+ const path = await owner(providerId);
218
237
  // No-op when nothing is stored: do NOT take the lock (which would create the file) on a machine that never
219
238
  // stored this provider.
220
239
  if (!existsSync(path))
@@ -229,3 +248,31 @@ export function fastagentCredentialStore(authPath = GLOBAL_AUTH_PATH, options =
229
248
  },
230
249
  };
231
250
  }
251
+ // ── Which files an agent reads ───────────────────────────────────────────────
252
+ /**
253
+ * The auth-file override: the SDK's `authPath` option > `FASTAGENT_AUTH_PATH` env > undefined (then
254
+ * `<secrets dir>/auth.json`). The CLI has no flag for it: a second spelling of one environment variable buys nothing,
255
+ * and the variable is what a deployed container reads anyway.
256
+ */
257
+ function resolveAuthPathOverride(flag, env) {
258
+ return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
259
+ }
260
+ /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
261
+ export function resolveAuthPath(dir, flag, env = process.env) {
262
+ return resolveAuthPathOverride(flag, env) ?? join(resolveSecretsDir(dir, env), "auth.json");
263
+ }
264
+ /**
265
+ * The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file, and
266
+ * behind it the user-global store, because a login is a PERSON on a machine and not a project — one `login -g` then
267
+ * serves every agent here.
268
+ *
269
+ * No fallback when a path was named: "use this file" is an instruction, not a preference. BOTH knobs
270
+ * {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what the
271
+ * Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing else
272
+ * (the artifact is the truth).
273
+ */
274
+ export function resolveAuthLayers(agentDir, authPath, env = process.env) {
275
+ const path = resolveAuthPath(agentDir, authPath, env);
276
+ const named = resolveAuthPathOverride(authPath, env) ?? resolveOverridePath(env.FASTAGENT_SECRETS_DIR);
277
+ return named === undefined ? { path, fallback: GLOBAL_AUTH_PATH } : { path };
278
+ }
@@ -63,28 +63,3 @@ export declare function rewriteConfigModel(src: string, spec: string): string |
63
63
  * would refuse to start an agent whose config names a model.
64
64
  */
65
65
  export declare function resolveModelSpec(flag: string | undefined, config: FastagentConfig, env?: NodeJS.ProcessEnv): string | undefined;
66
- /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
67
- export declare function defaultAuthPath(secretsDir: string): string;
68
- /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
69
- export declare function resolveAuthPath(dir: string, flag?: string, env?: NodeJS.ProcessEnv): string;
70
- /**
71
- * Which credentials files an agent reads: `path` first, then, per provider, `fallback` — present only when nothing
72
- * named the path ({@link resolveAuthFallback}). One value because it is one decision: the opener, the model list, the
73
- * startup report and the first-run picker all read through it.
74
- */
75
- export interface AuthLayers {
76
- path: string;
77
- fallback?: string;
78
- }
79
- /** The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file. */
80
- export declare function resolveAuthLayers(agentDir: string, authPath?: string, env?: NodeJS.ProcessEnv): AuthLayers;
81
- /**
82
- * Where a credential the project does not have is read from instead: the user-global store, because a login is a
83
- * PERSON on a machine and not a project — one `login -g` then serves every agent here. Undefined when an explicit
84
- * path was named: "use this file" is an instruction, not a preference, so it gets no second layer.
85
- *
86
- * BOTH knobs {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what
87
- * the Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing
88
- * else (the artifact is the truth).
89
- */
90
- export declare function resolveAuthFallback(flag?: string, env?: NodeJS.ProcessEnv): string | undefined;
@@ -14,11 +14,10 @@ import { existsSync, statSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { THINKING_LEVELS } from "./session-settings.js";
17
- import { GLOBAL_AUTH_PATH } from "./auth.js";
18
17
  import { readSecretDeclaration } from "../../declared-secrets.js";
19
18
  import { assertCorsOrigins } from "../../channels/serve.js";
20
19
  import { moduleLoadHint } from "../../loader.js";
21
- import { AGENT_CONFIG_FILE, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
20
+ import { AGENT_CONFIG_FILE } from "../../paths.js";
22
21
  /** Identity function for typing and IDE completion (vite/next-style). */
23
22
  export function defineConfig(config) {
24
23
  return config;
@@ -152,7 +151,8 @@ export function resolveModel(models, spec) {
152
151
  const modelId = spec.slice(slash + 1);
153
152
  const model = models.getModel(provider, modelId);
154
153
  if (!model) {
155
- throw new Error(`unknown model "${spec}" (provider "${provider}" / id "${modelId}" not in registry); run \`fastagent models\` to list available specs`);
154
+ throw new Error(`unknown model "${spec}" (provider "${provider}" / id "${modelId}" not in registry); run \`fastagent models\` to list ` +
155
+ "available specs, or `fastagent models --refresh` in the agent to fetch models newer than the installed pi");
156
156
  }
157
157
  return model;
158
158
  }
@@ -189,37 +189,3 @@ export function rewriteConfigModel(src, spec) {
189
189
  export function resolveModelSpec(flag, config, env = process.env) {
190
190
  return flag ?? (env.FASTAGENT_MODEL || config.model);
191
191
  }
192
- /**
193
- * The auth-file override: the SDK's `authPath` option > `FASTAGENT_AUTH_PATH` env > undefined (the opener then falls
194
- * back to {@link defaultAuthPath} under the {@link resolveSecretsDir} dir). The CLI has no flag for it: a second
195
- * spelling of one environment variable buys nothing, and the variable is what a deployed container reads anyway.
196
- */
197
- function resolveAuthPathOverride(flag, env = process.env) {
198
- return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
199
- }
200
- /** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
201
- export function defaultAuthPath(secretsDir) {
202
- return join(secretsDir, "auth.json");
203
- }
204
- /** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
205
- export function resolveAuthPath(dir, flag, env = process.env) {
206
- return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveSecretsDir(dir, env));
207
- }
208
- /** The {@link AuthLayers} an agent directory reads: `authPath` option > `FASTAGENT_AUTH_PATH` > its own file. */
209
- export function resolveAuthLayers(agentDir, authPath, env = process.env) {
210
- const fallback = resolveAuthFallback(authPath, env);
211
- return { path: resolveAuthPath(agentDir, authPath, env), ...(fallback !== undefined ? { fallback } : {}) };
212
- }
213
- /**
214
- * Where a credential the project does not have is read from instead: the user-global store, because a login is a
215
- * PERSON on a machine and not a project — one `login -g` then serves every agent here. Undefined when an explicit
216
- * path was named: "use this file" is an instruction, not a preference, so it gets no second layer.
217
- *
218
- * BOTH knobs {@link resolveAuthPath} reads count as that instruction, `FASTAGENT_SECRETS_DIR` included — it is what
219
- * the Fly/Railway/AgentCore artifacts set, and a deployed container must read its mounted credentials and nothing
220
- * else (the artifact is the truth).
221
- */
222
- export function resolveAuthFallback(flag, env = process.env) {
223
- const explicit = resolveAuthPathOverride(flag, env) ?? resolveOverridePath(env.FASTAGENT_SECRETS_DIR);
224
- return explicit === undefined ? GLOBAL_AUTH_PATH : undefined;
225
- }
@@ -1,15 +1,16 @@
1
1
  import type { ExecutionEnv, Skill, ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
3
- import type { Provider } from "@earendil-works/pi-ai";
3
+ import type { CredentialStore, Provider } from "@earendil-works/pi-ai";
4
4
  import type { Agent } from "../../agent.ts";
5
- import { type AuthLayers, type FastagentConfig } from "./config.ts";
6
- import type { FastagentAuthOptions } from "./auth.ts";
5
+ import { type FastagentConfig } from "./config.ts";
7
6
  import { type LoadedDefinition } from "./definition.ts";
8
7
  import type { ModuleLoadFailure } from "../../loader.ts";
9
8
  import { type ToolCollision, type MountedTool } from "./tool.ts";
10
9
  import { type DeclaredSecret } from "../../declared-secrets.ts";
11
10
  import { type PiAgentSessionFactory } from "./invoke-session.ts";
11
+ import { type PiAgentSessionFactoryOptions } from "./agent-session-factory.ts";
12
12
  import { type AnyModel } from "./models.ts";
13
+ import { type AgentModels } from "./agent-models.ts";
13
14
  import { type PiSessionRecordStore } from "./session-store.ts";
14
15
  import { type Lease, type SessionObserver } from "./turn-kit.ts";
15
16
  /**
@@ -65,6 +66,11 @@ export declare function assembleSystemPrompt(options: AssembleSystemPromptOption
65
66
  export interface PiAssembly {
66
67
  lease: Lease;
67
68
  sessionFactory: PiAgentSessionFactory;
69
+ /**
70
+ * The registry alone, with injected providers registered. `chat` resolves its model against it only after its
71
+ * extensions have loaded, because there an extension may register the provider (serving refuses that).
72
+ */
73
+ modelRuntime: () => Promise<ModelRuntime>;
68
74
  /** The registry and configured model, resolved on first use (a credential read is async). */
69
75
  engine: () => Promise<{
70
76
  modelRuntime: ModelRuntime;
@@ -72,6 +78,10 @@ export interface PiAssembly {
72
78
  }>;
73
79
  /** The configured reasoning effort — the other half of the pair a session without overrides runs on. */
74
80
  thinkingLevel: ThinkingLevel;
81
+ /** The tools every session mounts. */
82
+ tools: MountedTool[];
83
+ /** The prompt and skills a session runs on, read when a session is bound. */
84
+ readDefinition: PiAgentSessionFactoryOptions["readDefinition"];
75
85
  /** The extension entry points every session loads, discovered once with the assembly. */
76
86
  extensionPaths: readonly string[];
77
87
  }
@@ -98,6 +108,8 @@ export interface CreatePiAgentOptions {
98
108
  providers?: Provider[];
99
109
  /** Credentials file for stored OAuth/API-key auth (default `GLOBAL_AUTH_PATH`). */
100
110
  authPath?: string;
111
+ /** The caller's own credential store, in place of any file ({@link CredentialSourceOptions}). */
112
+ credentialStore?: CredentialStore;
101
113
  sessions?: PiSessionRecordStore;
102
114
  /** Supplies the working directory at L1 (default: process.cwd()), which loads no definition. */
103
115
  env?: ExecutionEnv;
@@ -129,6 +141,8 @@ export interface CreatePiAgentFromDefinitionOptions {
129
141
  * gets no second layer.
130
142
  */
131
143
  authPath?: string;
144
+ /** The caller's own credential store, in place of any file ({@link CredentialSourceOptions}). */
145
+ credentialStore?: CredentialStore;
132
146
  sessions?: PiSessionRecordStore;
133
147
  /** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. */
134
148
  env?: ExecutionEnv;
@@ -137,20 +151,12 @@ export interface CreatePiAgentFromDefinitionOptions {
137
151
  observer?: SessionObserver;
138
152
  }
139
153
  /**
140
- * THE model registry an agent directory runs on: pi's built-ins, the directory's own `models.json` (custom endpoints
141
- * are definition data and travel with the artifact) layered over the machine's `~/.fastagent/models.json` (environment,
142
- * which does not travel), and any injected Provider instance, over the given credentials.
143
- * The directory rung assembles from it and `availableModelsFromDir` lists from it, so a layer added here reaches both.
154
+ * L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. `models` is the directory's
155
+ * model environment when the caller already built it (the opener, whose report must describe what runs); otherwise it
156
+ * is built here from the credential options.
144
157
  */
145
- export declare function definitionModelRuntime(dir: string, options: FastagentAuthOptions & {
146
- auth: AuthLayers;
147
- providers?: Provider[];
148
- }): Promise<ModelRuntime>;
149
- /** L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. */
150
- export declare function assemblePiFromDefinition(dir: string,
151
- /** `auth` is the opener's already-resolved layers, which take the place of `authPath`. */
152
- options: Omit<CreatePiAgentFromDefinitionOptions, "observer"> & {
153
- auth?: AuthLayers;
158
+ export declare function assemblePiFromDefinition(dir: string, options: Omit<CreatePiAgentFromDefinitionOptions, "observer"> & {
159
+ models?: AgentModels;
154
160
  }): Promise<{
155
161
  assembly: PiAssembly;
156
162
  definition: LoadedDefinition;
@@ -5,7 +5,7 @@
5
5
  import { toUSVString } from "node:util";
6
6
  import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
7
7
  import { createCodingTools, createPowerShellTool, createReadOnlyTools, } from "@earendil-works/pi-coding-agent";
8
- import { resolveAuthLayers, resolveModel } from "./config.js";
8
+ import { resolveModel } from "./config.js";
9
9
  import { isAgentcoreRuntime, isDeployedWorkspace } from "../../paths.js";
10
10
  import { loadAgentDefinition, loadExtensionPaths } from "./definition.js";
11
11
  import { reportFindingsIfChanged } from "./report.js";
@@ -14,7 +14,8 @@ import { readSecretDeclaration } from "../../declared-secrets.js";
14
14
  import { withSearchTool } from "./search-tools.js";
15
15
  import { createPiAgentFromSession } from "./invoke-session.js";
16
16
  import { piAgentSessionFactory } from "./agent-session-factory.js";
17
- import { DEFAULT_THINKING_LEVEL, createPiModelRuntime } from "./models.js";
17
+ import { DEFAULT_THINKING_LEVEL } from "./models.js";
18
+ import { agentModels } from "./agent-models.js";
18
19
  import { piInMemorySessionRecordStore } from "./session-store.js";
19
20
  import { inProcessLease } from "./turn-kit.js";
20
21
  // ── §1 tools ─────────────────────────────────────────────────────────────────
@@ -188,18 +189,20 @@ function assemblePi(opts) {
188
189
  const lease = opts.lease ?? inProcessLease();
189
190
  const sessions = opts.sessions ?? piInMemorySessionRecordStore({ cwd });
190
191
  // The model and its runtime resolve on FIRST USE.
191
- let engine;
192
- const resolveEngine = () => {
193
- engine ??= (async () => {
194
- // The caller's registry when there is one — the directory rung builds it from the agent's own models.json, so a
195
- // custom endpoint declared there is the one a turn resolves against.
196
- const modelRuntime = opts.models ?? (await createPiModelRuntime({ auth: opts.auth }));
192
+ let registry;
193
+ const modelRuntime = () => {
194
+ registry ??= opts.models().then((runtime) => {
197
195
  // ModelRuntime registers providers by config record, so an injected Provider INSTANCE (a gateway, a self-hosted
198
196
  // endpoint, a test fake) goes in through its native seam.
199
197
  for (const provider of opts.providers ?? [])
200
- modelRuntime.registerNativeProvider(provider);
201
- return { modelRuntime, model: resolveModel(modelRuntime, opts.model) };
202
- })();
198
+ runtime.registerNativeProvider(provider);
199
+ return runtime;
200
+ });
201
+ return registry;
202
+ };
203
+ let engine;
204
+ const resolveEngine = () => {
205
+ engine ??= modelRuntime().then((runtime) => ({ modelRuntime: runtime, model: resolveModel(runtime, opts.model) }));
203
206
  return engine;
204
207
  };
205
208
  const sessionFactory = piAgentSessionFactory({
@@ -217,8 +220,11 @@ function assemblePi(opts) {
217
220
  return {
218
221
  lease,
219
222
  sessionFactory,
223
+ modelRuntime,
220
224
  engine: resolveEngine,
221
225
  thinkingLevel: opts.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
226
+ tools: opts.tools ?? [],
227
+ readDefinition: opts.readDefinition,
222
228
  extensionPaths: opts.extensionPaths ?? [],
223
229
  };
224
230
  }
@@ -233,7 +239,7 @@ export function createPiAgent(options) {
233
239
  model: options.model,
234
240
  thinkingLevel: options.thinkingLevel,
235
241
  providers: options.providers,
236
- ...(options.authPath !== undefined ? { auth: { path: options.authPath } } : {}),
242
+ models: agentModels(undefined, options).runtime,
237
243
  readDefinition: () => ({
238
244
  systemPrompt: typeof instructions === "function" ? instructions() : instructions,
239
245
  skills,
@@ -246,18 +252,11 @@ export function createPiAgent(options) {
246
252
  }), options.observer);
247
253
  }
248
254
  /**
249
- * THE model registry an agent directory runs on: pi's built-ins, the directory's own `models.json` (custom endpoints
250
- * are definition data and travel with the artifact) layered over the machine's `~/.fastagent/models.json` (environment,
251
- * which does not travel), and any injected Provider instance, over the given credentials.
252
- * The directory rung assembles from it and `availableModelsFromDir` lists from it, so a layer added here reaches both.
255
+ * L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. `models` is the directory's
256
+ * model environment when the caller already built it (the opener, whose report must describe what runs); otherwise it
257
+ * is built here from the credential options.
253
258
  */
254
- export function definitionModelRuntime(dir, options) {
255
- return createPiModelRuntime({ agentDir: dir, ...options });
256
- }
257
- /** L2, as the value: load the directory (base + AGENTS.md + skills + env) and assemble. */
258
- export async function assemblePiFromDefinition(dir,
259
- /** `auth` is the opener's already-resolved layers, which take the place of `authPath`. */
260
- options) {
259
+ export async function assemblePiFromDefinition(dir, options) {
261
260
  // `dir` = the agent-definition dir (persona.md/skills/); `cwd` (default = dir) is the run root where tools operate
262
261
  // and whose ancestors are walked for ② context.
263
262
  const cwd = options.cwd ?? dir;
@@ -271,14 +270,15 @@ options) {
271
270
  // Boot findings go through the SAME memoized reporter every later reader uses (report.ts, keyed by the resolved
272
271
  // dir).
273
272
  reportFindingsIfChanged(definition.dir, definition);
274
- const auth = options.auth ?? (options.authPath === undefined ? resolveAuthLayers(dir) : { path: options.authPath });
273
+ const { providers } = options;
274
+ const models = options.models ?? agentModels(dir, options, providers ? { providers } : {});
275
+ // Built at boot, so a malformed models.json fails the assembly rather than its first turn. The directory's own
276
+ // models.json is what a turn resolves against, layered over the machine's (models.ts).
277
+ await models.runtime();
275
278
  const assembly = assemblePi({
276
279
  model: options.model,
277
280
  thinkingLevel: options.thinkingLevel,
278
- models: await definitionModelRuntime(dir, {
279
- auth,
280
- ...(options.providers ? { providers: options.providers } : {}),
281
- }),
281
+ models: models.runtime,
282
282
  // The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills edits (the
283
283
  // author's, or the agent's own self-modification) take effect on the next turn with no process restart — restarts
284
284
  // are reserved for code (tools/channels/config, module cache).
@@ -0,0 +1,14 @@
1
+ import type { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import type { ImageRef } from "../../agent.ts";
3
+ import type { EntryImage } from "../../session.ts";
4
+ type Message = {
5
+ role: string;
6
+ content?: unknown;
7
+ };
8
+ /** `data.images` of the entry `entryId` holding `message`, absent when it has none. */
9
+ export declare function entryImages(entryId: string, message: Message): {
10
+ images?: EntryImage[];
11
+ };
12
+ /** The image `ref` names in `record`, or undefined when it names none there. The inverse of `entryImages`. */
13
+ export declare function imageAt(record: SessionManager, ref: string): ImageRef | undefined;
14
+ export {};
@@ -0,0 +1,23 @@
1
+ /** A user prompt's and a tool result's images; no other message publishes any, so no ref can reach them. */
2
+ function publishedImages(message) {
3
+ if (message.role !== "user" && message.role !== "toolResult")
4
+ return [];
5
+ const content = message.content;
6
+ return Array.isArray(content) ? content.filter((b) => b.type === "image") : [];
7
+ }
8
+ /** `data.images` of the entry `entryId` holding `message`, absent when it has none. */
9
+ export function entryImages(entryId, message) {
10
+ const images = publishedImages(message).map((b, i) => ({ ref: `${entryId}:${i}`, mimeType: b.mimeType }));
11
+ return images.length > 0 ? { images } : {};
12
+ }
13
+ /** The image `ref` names in `record`, or undefined when it names none there. The inverse of `entryImages`. */
14
+ export function imageAt(record, ref) {
15
+ const at = ref.lastIndexOf(":");
16
+ const index = ref.slice(at + 1);
17
+ // Canonical digits only, so one image has one ref (`:00` is not `:0`).
18
+ if (at <= 0 || !/^(0|[1-9]\d*)$/.test(index))
19
+ return undefined;
20
+ const entry = record.getEntry(ref.slice(0, at));
21
+ const image = entry?.type === "message" ? publishedImages(entry.message)[Number(index)] : undefined;
22
+ return image ? { data: image.data, mimeType: image.mimeType } : undefined;
23
+ }