@fastagent-sh/fastagent 0.25.0 → 0.26.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 (53) hide show
  1. package/README.md +3 -2
  2. package/dist/agent-api.d.ts +10 -2
  3. package/dist/agent-api.js +10 -2
  4. package/dist/authoring.d.ts +12 -8
  5. package/dist/authoring.js +14 -7
  6. package/dist/cli/add-feishu.js +1 -1
  7. package/dist/cli/add-slack.js +1 -1
  8. package/dist/cli/box-login.d.ts +3 -2
  9. package/dist/cli/commands/add.js +2 -1
  10. package/dist/cli/commands/deploy.js +1 -1
  11. package/dist/cli/commands/env.js +5 -4
  12. package/dist/cli/commands/init.js +3 -3
  13. package/dist/cli/commands/invoke.js +2 -1
  14. package/dist/cli/commands/login.js +6 -4
  15. package/dist/cli/login-relay.d.ts +4 -3
  16. package/dist/cli/login-relay.js +1 -1
  17. package/dist/cli/shared.js +5 -3
  18. package/dist/content/git.d.ts +1 -1
  19. package/dist/content/git.js +7 -4
  20. package/dist/content/resolve.d.ts +1 -1
  21. package/dist/content/resolve.js +2 -2
  22. package/dist/deploy/preflight.js +1 -1
  23. package/dist/dev-supervisor.js +1 -1
  24. package/dist/env.d.ts +0 -13
  25. package/dist/env.js +1 -44
  26. package/dist/environment/mise.d.ts +8 -7
  27. package/dist/environment/mise.js +24 -20
  28. package/dist/git.d.ts +2 -2
  29. package/dist/git.js +5 -5
  30. package/dist/harnesses/models/environment.d.ts +7 -2
  31. package/dist/harnesses/models/environment.js +16 -10
  32. package/dist/harnesses/models/host.d.ts +30 -0
  33. package/dist/harnesses/models/host.js +97 -0
  34. package/dist/harnesses/models/login.d.ts +38 -73
  35. package/dist/harnesses/models/login.js +50 -50
  36. package/dist/harnesses/models/models.d.ts +4 -3
  37. package/dist/harnesses/models/models.js +51 -12
  38. package/dist/harnesses/pi-durable/agent.js +2 -0
  39. package/dist/harnesses/pi-durable/open.d.ts +7 -0
  40. package/dist/harnesses/pi-durable/open.js +4 -3
  41. package/dist/harnesses/pi-durable/serve.d.ts +11 -1
  42. package/dist/harnesses/pi-durable/serve.js +42 -12
  43. package/dist/harnesses/pi-durable/wake.d.ts +8 -1
  44. package/dist/harnesses/pi-durable/wake.js +10 -1
  45. package/dist/index.d.ts +5 -3
  46. package/dist/index.js +3 -0
  47. package/dist/scaffold/add-channel.js +1 -1
  48. package/dist/sign-in-api.d.ts +147 -0
  49. package/dist/sign-in-api.js +9 -0
  50. package/dist/tunnel.js +2 -1
  51. package/dist/value-file.d.ts +19 -0
  52. package/dist/value-file.js +61 -5
  53. package/package.json +1 -1
@@ -15,7 +15,7 @@ import { loadEnvironment } from "../content/file.js";
15
15
  import { log } from "../log.js";
16
16
  import { detectRuntime, listsFastagent, readPackageJson } from "../runtime.js";
17
17
  import { aptAdvice, missingAptPackages } from "./apt.js";
18
- import { missingPythonRuntime, resolvedBackendRefusal, writtenBackend, } from "./declare.js";
18
+ import { missingPythonRuntime, resolvedBackendRefusal, writtenBackend } from "./declare.js";
19
19
  /** mise's npm package for each platform it publishes one for, by Node's `${platform}-${arch}`. */
20
20
  const MISE_PACKAGES = {
21
21
  "darwin-arm64": "@jdxcode/mise-darwin-arm64",
@@ -77,14 +77,15 @@ function specOf(name, packages) {
77
77
  return `${name}@${packages[name]}`;
78
78
  }
79
79
  /**
80
- * Run the agent's mise with `args` in `cwd`. Its errors go to this process's stderr unless `quiet` keeps them for
81
- * the error, which then says what mise said; `capture` returns its stdout instead of passing it through.
80
+ * Run the agent's mise with `args` in `cwd`, in `env` (this process's by default). Its errors go to this process's
81
+ * stderr unless `quiet` keeps them for the error, which then says what mise said; `capture` returns its stdout instead
82
+ * of passing it through.
82
83
  */
83
84
  function runMise(cwd, bin, args, options = {}) {
84
85
  return new Promise((done, fail) => {
85
86
  const child = spawn(bin, args, {
86
87
  cwd,
87
- env: { ...process.env, ...MISE_ENVIRONMENT },
88
+ env: { ...(options.env ?? process.env), ...MISE_ENVIRONMENT },
88
89
  stdio: ["inherit", options.capture ? "pipe" : process.stderr, options.quiet ? "pipe" : "inherit"],
89
90
  });
90
91
  let stdout = "";
@@ -175,14 +176,14 @@ export async function checkPackages(agentDir, bin, packages) {
175
176
  * so a start whose packages are installed needs no network and keeps the versions it has. An install that matches may
176
177
  * be another project's: mise's data directory is the machine's.
177
178
  */
178
- export async function installPackages(agentDir, bin, packages, newest = []) {
179
+ export async function installPackages(agentDir, bin, packages, newest = [], env = process.env) {
179
180
  const names = Object.keys(packages);
180
181
  const asked = names.filter((name) => newest === "all" || newest.includes(name));
181
182
  const others = names.filter((name) => !asked.includes(name));
182
183
  const installed = await Promise.all(
183
184
  // A failed `where` is what mise says of a package with no install here; the install that follows reports any other
184
185
  // reason it cannot be had.
185
- others.map((name) => runMise(agentDir, bin, ["where", specOf(name, packages)], { capture: true, quiet: true }).then(() => true, () => false)));
186
+ others.map((name) => runMise(agentDir, bin, ["where", specOf(name, packages)], { capture: true, quiet: true, env }).then(() => true, () => false)));
186
187
  const present = new Set(others.filter((_, i) => installed[i]));
187
188
  const wanted = names.filter((name) => !present.has(name)).map((name) => specOf(name, packages));
188
189
  if (wanted.length === 0)
@@ -190,11 +191,14 @@ export async function installPackages(agentDir, bin, packages, newest = []) {
190
191
  const installedSpecs = [...present].map((name) => specOf(name, packages));
191
192
  // A package installs with the ones it needs (a PyPI one runs uv), which mise finds in the packages it is given or on
192
193
  // the PATH. The installed ones go on the PATH: given as packages, mise would install a newer release of them.
193
- await runInstall(agentDir, bin, wanted, installedSpecs.length > 0 ? await packagesEnvironment(agentDir, bin, installedSpecs) : {});
194
+ await runInstall(agentDir, bin, wanted, {
195
+ ...env,
196
+ ...(installedSpecs.length > 0 ? await packagesEnvironment(agentDir, bin, installedSpecs, env) : {}),
197
+ });
194
198
  }
195
199
  /** What `mise env` puts in a process's environment for `specs`: the PATH their commands are on. */
196
- async function packagesEnvironment(agentDir, bin, specs) {
197
- return JSON.parse(await runMise(agentDir, bin, ["env", "--json", ...specs], { capture: true }));
200
+ async function packagesEnvironment(agentDir, bin, specs, env) {
201
+ return JSON.parse(await runMise(agentDir, bin, ["env", "--json", ...specs], { capture: true, env }));
198
202
  }
199
203
  /**
200
204
  * The notice mise prints after installing a package no configuration file names, with the `mise use` it suggests: not
@@ -202,7 +206,7 @@ async function packagesEnvironment(agentDir, bin, specs) {
202
206
  */
203
207
  const NOT_ACTIVATED = /installed but not activated|^To install and activate, run:|^\s+mise use /;
204
208
  /**
205
- * `mise install` of `specs`, with `env` added to its environment. Its progress is passed on to this process's stderr as
209
+ * `mise install` of `specs`, in `env`. Its progress is passed on to this process's stderr as
206
210
  * it comes, so a runtime that takes minutes is seen to; a failure carries what mise said, every line of it, since
207
211
  * mise names the packages on its first line and the reason (a 404, a proxy, a rate limit) on the lines after.
208
212
  */
@@ -211,7 +215,7 @@ function runInstall(agentDir, bin, specs, env) {
211
215
  return new Promise((done, fail) => {
212
216
  const child = spawn(bin, ["install", ...specs], {
213
217
  cwd: agentDir,
214
- env: { ...process.env, ...env, ...MISE_ENVIRONMENT },
218
+ env: { ...env, ...MISE_ENVIRONMENT },
215
219
  stdio: ["inherit", process.stderr, "pipe"],
216
220
  });
217
221
  const said = [];
@@ -246,23 +250,23 @@ function missingMise(agentDir) {
246
250
  return new Error(`context.json declares packages, and the agent has no mise installed — run \`fastagent env install\` in ${agentDir}`);
247
251
  }
248
252
  /**
249
- * Enter the agent's environment, for a process that runs it: install the declared packages that have no install here
250
- * yet, and put them first on this process's PATH, which every command the agent runs inherits. mise itself is not put
251
- * on the PATH. An agent without packages borrows the machine's commands. Apt packages are the image's to install; one
252
- * missing here is said, with how.
253
+ * The agent's declared environment, for a process that runs it: install the declared packages that have no install
254
+ * here yet, and answer the variables that put them first on the PATH of the commands the agent runs. mise runs in
255
+ * `env`, the agent's environment so far, and nothing is written into this process's: a process may run several agents.
256
+ * mise itself is not put on the PATH. An agent without packages borrows the machine's commands (no variables). Apt
257
+ * packages are the image's to install; one missing here is said, with how.
253
258
  */
254
- export async function enterEnvironment(agentDir) {
259
+ export async function prepareEnvironment(agentDir, env) {
255
260
  const declared = loadEnvironment(agentDir);
256
261
  const missingApt = missingAptPackages(declared?.aptPackages ?? []);
257
262
  if (missingApt && missingApt.length > 0)
258
263
  log.warn(`[fastagent] ${aptAdvice(missingApt)}`);
259
264
  const packages = declared?.packages ?? {};
260
265
  if (Object.keys(packages).length === 0)
261
- return declared;
266
+ return {};
262
267
  const bin = miseBinary(agentDir);
263
268
  if (!bin)
264
269
  throw missingMise(agentDir);
265
- await installPackages(agentDir, bin, packages);
266
- Object.assign(process.env, await packagesEnvironment(agentDir, bin, packageSpecs(packages)));
267
- return declared;
270
+ await installPackages(agentDir, bin, packages, [], env);
271
+ return packagesEnvironment(agentDir, bin, packageSpecs(packages), env);
268
272
  }
package/dist/git.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /** git is not on the PATH. Its message names what needed it: `git is not installed: <why>`. */
2
2
  export declare class GitNotInstalled extends Error {
3
3
  }
4
- /** git for one purpose, `why`: what a missing git is reported as needed for. */
5
- export declare function gitFor(why: string): {
4
+ /** git for one purpose, `why`: what a missing git is reported as needed for. It runs in `env`. */
5
+ export declare function gitFor(why: string, env?: NodeJS.ProcessEnv): {
6
6
  /**
7
7
  * git's answer in `cwd` (which must exist), or undefined when it answers with a non-zero exit. A git that cannot
8
8
  * run is an error.
package/dist/git.js CHANGED
@@ -12,10 +12,10 @@ export class GitNotInstalled extends Error {
12
12
  * Never wait on a terminal prompt for a credential: a serving process has nobody to answer it, so git fails and says
13
13
  * why instead. Not covered by a test: it takes a remote that asks for a credential.
14
14
  */
15
- const gitEnv = () => ({ ...process.env, GIT_TERMINAL_PROMPT: "0" });
15
+ const gitEnv = (env) => ({ ...env, GIT_TERMINAL_PROMPT: "0" });
16
16
  const execGit = promisify(execFile);
17
- /** git for one purpose, `why`: what a missing git is reported as needed for. */
18
- export function gitFor(why) {
17
+ /** git for one purpose, `why`: what a missing git is reported as needed for. It runs in `env`. */
18
+ export function gitFor(why, env = process.env) {
19
19
  const missing = () => new GitNotInstalled(`git is not installed: ${why}`);
20
20
  return {
21
21
  /**
@@ -26,7 +26,7 @@ export function gitFor(why) {
26
26
  try {
27
27
  return execFileSync("git", args, {
28
28
  cwd,
29
- env: gitEnv(),
29
+ env: gitEnv(env),
30
30
  encoding: "utf8",
31
31
  stdio: ["ignore", "pipe", "pipe"],
32
32
  }).trim();
@@ -42,7 +42,7 @@ export function gitFor(why) {
42
42
  /** Run git in `cwd`: its output, or its stderr as the error. */
43
43
  async run(args, cwd) {
44
44
  try {
45
- return (await execGit("git", args, { cwd, env: gitEnv() })).stdout;
45
+ return (await execGit("git", args, { cwd, env: gitEnv(env) })).stdout;
46
46
  }
47
47
  catch (error) {
48
48
  if (error.code === "ENOENT")
@@ -34,10 +34,15 @@ export interface ModelEnvironment {
34
34
  /**
35
35
  * The model environment of the agent in `agentDir` (or, without one, of a caller with no directory: the named or global
36
36
  * credentials file). A supplied `credentialStore` is used as given; otherwise the files: the directory's own layers
37
- * ({@link resolveAuthLayers}), or the named file, or the global one. `providers` are the definition's. The model lists
38
- * are refreshed before it resolves, kept in the agent's state root; `force` asks the network even when they are fresh.
37
+ * ({@link resolveAuthLayers}), or the named file, or the global one. `providers` are the definition's. `environment` is
38
+ * what the agent's environment adds to this process's: its keys, the requests' settings, and where its files are
39
+ * (`FASTAGENT_AUTH_PATH`, `FASTAGENT_STATE_DIR`) are all read there first. The model lists are refreshed before it
40
+ * resolves, kept in the instance's state root (`stateRoot`, else the directory's); `force` asks the network even when
41
+ * they are fresh.
39
42
  */
40
43
  export declare function modelEnvironment(agentDir: string | undefined, source?: CredentialSourceOptions & FastagentAuthOptions, providers?: readonly Provider[], options?: {
41
44
  force?: boolean;
45
+ stateRoot?: string;
46
+ environment?: Readonly<Record<string, string>>;
42
47
  }): Promise<ModelEnvironment>;
43
48
  export {};
@@ -14,21 +14,27 @@ import { environmentAuth, modelsOver, probeAuthSource } from "./models.js";
14
14
  /**
15
15
  * The model environment of the agent in `agentDir` (or, without one, of a caller with no directory: the named or global
16
16
  * credentials file). A supplied `credentialStore` is used as given; otherwise the files: the directory's own layers
17
- * ({@link resolveAuthLayers}), or the named file, or the global one. `providers` are the definition's. The model lists
18
- * are refreshed before it resolves, kept in the agent's state root; `force` asks the network even when they are fresh.
17
+ * ({@link resolveAuthLayers}), or the named file, or the global one. `providers` are the definition's. `environment` is
18
+ * what the agent's environment adds to this process's: its keys, the requests' settings, and where its files are
19
+ * (`FASTAGENT_AUTH_PATH`, `FASTAGENT_STATE_DIR`) are all read there first. The model lists are refreshed before it
20
+ * resolves, kept in the instance's state root (`stateRoot`, else the directory's); `force` asks the network even when
21
+ * they are fresh.
19
22
  */
20
23
  export async function modelEnvironment(agentDir, source = {}, providers = [], options = {}) {
24
+ const environment = options.environment ?? {};
25
+ const env = { ...process.env, ...environment };
21
26
  assertOneCredentialSource(source);
22
27
  const files = source.credentialStore
23
28
  ? undefined
24
29
  : agentDir
25
- ? resolveAuthLayers(agentDir, source.authPath)
30
+ ? resolveAuthLayers(agentDir, source.authPath, env)
26
31
  : { path: source.authPath ?? GLOBAL_AUTH_PATH };
27
- const store = files === undefined ? undefined : layeredStore(files, providers, source.warn);
32
+ const store = files === undefined ? undefined : layeredStore(files, providers, source.warn, environment);
28
33
  const credentials = source.credentialStore ?? store;
29
- const catalogs = catalogsIn(agentDir === undefined ? undefined : join(resolveStateRoot(agentDir), "models"));
30
- const models = modelsOver(credentials, providers, catalogs);
31
- const catalogProblem = await catalogs.refresh(models, options);
34
+ const stateRoot = options.stateRoot ?? (agentDir === undefined ? undefined : resolveStateRoot(agentDir, env));
35
+ const catalogs = catalogsIn(stateRoot === undefined ? undefined : join(stateRoot, "models"));
36
+ const models = modelsOver(credentials, providers, catalogs, environment);
37
+ const catalogProblem = await catalogs.refresh(models, { force: options.force === true });
32
38
  if (catalogProblem !== undefined)
33
39
  log.warn(`[fastagent] ${catalogProblem}`);
34
40
  return {
@@ -52,7 +58,7 @@ export async function modelEnvironment(agentDir, source = {}, providers = [], op
52
58
  }
53
59
  // `read` never refreshes: this is the kind on file, whatever became of it.
54
60
  const stored = (await credentials.read(provider))?.type;
55
- const fromEnvironment = stored === undefined ? undefined : (await environmentAuth(provider, process.env, undefined, providers))?.source;
61
+ const fromEnvironment = stored === undefined ? undefined : (await environmentAuth(provider, env, undefined, providers))?.source;
56
62
  const shadowed = resolved.source !== undefined && fromEnvironment !== resolved.source ? fromEnvironment : undefined;
57
63
  return {
58
64
  ...(store ? { path: await store.layerOf(provider) } : {}),
@@ -69,11 +75,11 @@ export async function modelEnvironment(agentDir, source = {}, providers = [], op
69
75
  * never has the global file, so a global login that outranked, say, an `ANTHROPIC_API_KEY` in `.secrets/.env` would
70
76
  * run one credential here and a different one deployed.
71
77
  */
72
- function layeredStore(auth, providers, warn) {
78
+ function layeredStore(auth, providers, warn, environment) {
73
79
  const own = () => fastagentCredentialStore(auth.path, { ...(warn ? { warn } : {}) });
74
80
  if (auth.fallback === undefined)
75
81
  return own();
76
- const project = modelsOver(own(), providers);
82
+ const project = modelsOver(own(), providers, undefined, environment);
77
83
  return fastagentCredentialStore(auth.path, {
78
84
  ...(warn ? { warn } : {}),
79
85
  fallbackPath: auth.fallback,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * What an embedding host calls for model credentials (sign-in-api.ts has the types): a store kept where the host
3
+ * chooses, a sign-in in its own UI, and what authenticates a provider. Thin over what `fastagent login`, `logout` and
4
+ * `auth check` run, so each rule keeps one implementation. The one translation between FastAgent's types and pi-ai's
5
+ * is here, written without casts and with every function checked as a function, so it stops compiling when the two
6
+ * drift apart.
7
+ */
8
+ import type { CredentialStore as PiCredentialStore } from "@earendil-works/pi-ai";
9
+ import type { CredentialStore, ProviderAuth, SignInOption, SignInRequest, SignInResult } from "../../sign-in-api.ts";
10
+ /**
11
+ * FastAgent's locked JSON credentials file at `path` (`~` is the home directory, a relative one resolves against the
12
+ * working directory now). A corrupt or unreadable file rejects every call.
13
+ */
14
+ export declare function credentialFile(path: string): CredentialStore;
15
+ /** A host's store as pi-ai reads it. */
16
+ export declare function piCredentials(store: CredentialStore): PiCredentialStore;
17
+ /** Every sign-in of the built-in providers, by name, with what authenticates each one now. */
18
+ export declare function signInOptions(credentials: CredentialStore): Promise<SignInOption[]>;
19
+ /**
20
+ * Sign in to a built-in provider and store the credential. The store is written to first, so a sign-in never runs
21
+ * toward a store that refuses it; an API key is checked with one request before it is stored, and one the provider
22
+ * rejects is asked for again. Aborting the interaction's signal rejects with `SignInCancelled` and stores nothing.
23
+ */
24
+ export declare function signIn(request: SignInRequest): Promise<SignInResult>;
25
+ /**
26
+ * What a request to a built-in provider authenticates with, resolved as a message resolves it: a stored credential
27
+ * (refreshing an OAuth token that is due, through the store's `modify`), else the environment. Undefined when nothing
28
+ * does; rejects when resolving fails, such as a refresh the provider refused.
29
+ */
30
+ export declare function providerAuth(credentials: CredentialStore, provider: string): Promise<ProviderAuth | undefined>;
@@ -0,0 +1,97 @@
1
+ import { resolveOverridePath } from "../../paths.js";
2
+ import { fastagentCredentialStore } from "./credentials.js";
3
+ import { loginOptionsOver, loginOver } from "./login.js";
4
+ import { modelsOver } from "./models.js";
5
+ /** The file behind each store {@link credentialFile} made: a sign-in keeps the installation's id beside it. */
6
+ const files = new WeakMap();
7
+ /**
8
+ * FastAgent's locked JSON credentials file at `path` (`~` is the home directory, a relative one resolves against the
9
+ * working directory now). A corrupt or unreadable file rejects every call.
10
+ */
11
+ export function credentialFile(path) {
12
+ // Resolved once, so the credentials and the installation's id beside them stay in one directory.
13
+ const resolved = resolveOverridePath(path);
14
+ if (resolved === undefined)
15
+ throw new Error("credentialFile needs the path of a credentials file");
16
+ const file = fastagentCredentialStore(resolved, {
17
+ warn: (message) => {
18
+ throw new Error(message);
19
+ },
20
+ });
21
+ const store = {
22
+ read: (provider) => file.read(provider),
23
+ list: async () => (await file.list()).map(({ providerId, type }) => ({ provider: providerId, type })),
24
+ modify: (provider, change) => file.modify(provider, change),
25
+ delete: (provider) => file.delete(provider),
26
+ };
27
+ files.set(store, resolved);
28
+ return store;
29
+ }
30
+ /** A host's store as pi-ai reads it. */
31
+ export function piCredentials(store) {
32
+ return {
33
+ read: (provider) => store.read(provider),
34
+ list: async () => (await store.list()).map(({ provider, type }) => ({ providerId: provider, type })),
35
+ modify: (provider, change) => store.modify(provider, change),
36
+ delete: (provider) => store.delete(provider),
37
+ };
38
+ }
39
+ /**
40
+ * A host's interaction as pi-ai drives it. Arrow functions, not the object as it is: both declare `prompt` and
41
+ * `notify` as methods, whose parameters TypeScript checks in either direction, so a kind of prompt or event pi-ai adds
42
+ * would pass unseen; a call checks it.
43
+ */
44
+ function piInteraction(interaction) {
45
+ return {
46
+ ...(interaction.signal === undefined ? {} : { signal: interaction.signal }),
47
+ prompt: (prompt) => interaction.prompt(prompt),
48
+ notify: (event) => interaction.notify(event),
49
+ };
50
+ }
51
+ /** Every sign-in of the built-in providers, by name, with what authenticates each one now. */
52
+ export function signInOptions(credentials) {
53
+ return loginOptionsOver(piCredentials(credentials));
54
+ }
55
+ /**
56
+ * Sign in to a built-in provider and store the credential. The store is written to first, so a sign-in never runs
57
+ * toward a store that refuses it; an API key is checked with one request before it is stored, and one the provider
58
+ * rejects is asked for again. Aborting the interaction's signal rejects with `SignInCancelled` and stores nothing.
59
+ */
60
+ export function signIn(request) {
61
+ const { credentials, provider, method, interaction, deviceId } = request;
62
+ const file = files.get(credentials);
63
+ return loginOver({
64
+ provider,
65
+ method,
66
+ interaction: piInteraction(interaction),
67
+ ...(file === undefined ? { credentialStore: piCredentials(credentials) } : { authPath: file }),
68
+ ...(deviceId === undefined ? {} : { getDeviceId: () => deviceId }),
69
+ });
70
+ }
71
+ /**
72
+ * What a request to a built-in provider authenticates with, resolved as a message resolves it: a stored credential
73
+ * (refreshing an OAuth token that is due, through the store's `modify`), else the environment. Undefined when nothing
74
+ * does; rejects when resolving fails, such as a refresh the provider refused.
75
+ */
76
+ export async function providerAuth(credentials, provider) {
77
+ const models = modelsOver(piCredentials(credentials));
78
+ if (models.getProvider(provider) === undefined)
79
+ throw new Error(`unknown provider "${provider}"`);
80
+ const checked = await models.checkAuth(provider);
81
+ if (checked === undefined)
82
+ return undefined;
83
+ const resolved = await models.getAuth(provider);
84
+ if (resolved === undefined)
85
+ return undefined;
86
+ const source = resolved.source ?? checked.source;
87
+ const { apiKey, baseUrl } = resolved.auth;
88
+ // pi's null header is one a request leaves out, which is what a header not given does.
89
+ const headers = Object.entries(resolved.auth.headers ?? {}).flatMap(([name, value]) => value === null ? [] : [[name, value]]);
90
+ return {
91
+ type: checked.type,
92
+ ...(source === undefined ? {} : { source }),
93
+ ...(apiKey === undefined ? {} : { apiKey }),
94
+ ...(headers.length === 0 ? {} : { headers: Object.fromEntries(headers) }),
95
+ ...(baseUrl === undefined ? {} : { baseUrl }),
96
+ };
97
+ }
@@ -1,55 +1,27 @@
1
1
  /**
2
- * Signing in to a MODEL PROVIDER, for any front end: {@link login} runs one provider's flow over pi-ai's own
2
+ * Signing in to a MODEL PROVIDER, for any front end: {@link loginOver} runs one provider's flow over pi-ai's own
3
3
  * `AuthInteraction` and persists the credential through the same {@link fastagentCredentialStore} the runtime reads
4
- * (one writer, one lock/corruption semantics); {@link loginOptions} is the list a picker offers. `fastagent login` is
5
- * one front end ({@link loginFlow}: terminal menus, then {@link login}); `login --deployment` drives the same flow on a
6
- * deployed box over a relay (cli/login-relay.ts).
4
+ * (one writer, one lock/corruption semantics); {@link loginOptionsOver} is the list a picker offers. An embedding host
5
+ * reaches both through host.ts (`signIn`, `signInOptions`); `fastagent login` is a front end of its own
6
+ * ({@link loginFlow}: terminal menus, then {@link loginOver}); `login --deployment` drives the same flow on a deployed
7
+ * box over a relay (cli/login-relay.ts).
7
8
  */
8
9
  import { type AuthInteraction, type CredentialStore, type Provider } from "@earendil-works/pi-ai";
9
- export type LoginMethod = "oauth" | "api_key";
10
- /** The user backed out of a prompt/menu, or the flow was aborted — a decision, not a failure. */
11
- export declare class LoginCancelled extends Error {
12
- }
10
+ import { type SignInMethod, type SignInOption, type SignInResult } from "../../sign-in-api.ts";
13
11
  /** A sign-in that cannot start as asked: an unknown provider, or a choice a caller without menus did not make. */
14
12
  export declare class LoginRefused extends Error {
15
13
  /** The sign-ins it could be instead, for a caller to choose from. */
16
- readonly choices: readonly LoginOption[];
17
- constructor(message: string, choices: readonly LoginOption[]);
18
- }
19
- /** One provider sign-in a picker can offer, as pi's `/login` offers it. */
20
- export interface LoginOption {
21
- provider: string;
22
- /** The provider's name, as pi lists it: "OpenAI", "OpenAI Codex (legacy)". */
23
- name: string;
24
- method: LoginMethod;
25
- /** What signing in this way is called: the provider's own label ("Sign in with ChatGPT"), else pi's. */
26
- label: string;
27
- /** An OAuth login backed by a provider subscription. */
28
- subscription: boolean;
29
- /** Its key comes from the environment, never from a sign-in: picking it says so. */
30
- external: boolean;
31
- /**
32
- * What authenticates the provider now: the kind of credential, and `OAuth`, `stored credential` or an env variable
33
- * such as `OPENAI_API_KEY`. The store keeps ONE credential per provider, so signing in with the other method
34
- * replaces a stored one, which a picker shows.
35
- */
36
- status?: {
37
- type: LoginMethod;
38
- source: string;
39
- };
40
- /** Why checking what authenticates it failed, when it did. */
41
- problem?: string;
14
+ readonly choices: readonly SignInOption[];
15
+ constructor(message: string, choices: readonly SignInOption[]);
42
16
  }
43
17
  /**
44
- * Every sign-in of pi's built-in providers, sorted by name as pi's `/login` lists them, with what authenticates each
45
- * one here: a credential `credentials` (a credentials file, or the caller's own store) holds, else this process's
46
- * environment, which a stored credential outranks.
18
+ * Every sign-in of pi's built-in providers plus `providers` (a same id replaces one), sorted by name as pi's `/login`
19
+ * lists them, with what authenticates each one here: a credential `credentials` (a credentials file, or a store) holds,
20
+ * else this process's environment, which a stored credential outranks.
47
21
  */
48
- export declare function loginOptions(credentials: string | CredentialStore): Promise<LoginOption[]>;
49
- /** {@link loginOptions} over pi's built-ins plus `providers` (a same id replaces one). Not public: a test seam. */
50
- export declare function loginOptionsOver(credentials: string | CredentialStore, providers?: readonly Provider[]): Promise<LoginOption[]>;
22
+ export declare function loginOptionsOver(credentials: string | CredentialStore, providers?: readonly Provider[]): Promise<SignInOption[]>;
51
23
  /** An option's status as pi's picker writes it after the provider's name. */
52
- export declare function loginStatusText(option: LoginOption): string;
24
+ export declare function loginStatusText(option: SignInOption): string;
53
25
  export type LoginRequest = {
54
26
  /**
55
27
  * Stable installation identity (Sign in with ChatGPT requires one). With `authPath` it defaults to `device.json`
@@ -58,7 +30,7 @@ export type LoginRequest = {
58
30
  */
59
31
  getDeviceId?: () => string;
60
32
  provider: string;
61
- method: LoginMethod;
33
+ method: SignInMethod;
62
34
  /** pi-ai's own interaction: prompts (`text`, `secret`, `select`, `manual_code`) and events (`auth_url`, …). */
63
35
  interaction: AuthInteraction;
64
36
  } & ({
@@ -70,29 +42,7 @@ export type LoginRequest = {
70
42
  credentialStore: CredentialStore;
71
43
  authPath?: never;
72
44
  });
73
- export interface LoginResult {
74
- provider: string;
75
- method: LoginMethod;
76
- /** An API key checked with one minimal request: accepted, or not decidable (kept). OAuth needs no check. */
77
- verified: "ok" | "unknown" | "n/a";
78
- }
79
- /**
80
- * Sign in to one of pi's built-in providers and persist the credential to `authPath`, or to the caller's
81
- * `credentialStore`.
82
- *
83
- * The store is checked FIRST (a no-op write runs the file store's refuse-corrupt and writability checks, or whatever
84
- * the caller's store does on a write), so a flow never runs toward a credential that could not be saved. An entered API key is verified with one minimal request to pi's
85
- * default model for the provider (its first model when pi names none), at pi's built-in endpoint, BEFORE it is
86
- * written. Credentials are provider-scoped, so any of the provider's models answers the question the check asks (does
87
- * the provider refuse the key, HTTP 401); a key the provider rejects is never stored, and the provider's key flow runs
88
- * again (the provider and method were not the mistake). Progress and outcome go out through `interaction.notify`.
89
- *
90
- * Aborting `interaction.signal` at any point, the verification included, rejects with {@link LoginCancelled} and
91
- * writes no credential (an OAuth device ID, the installation's identity, is kept once created). Every prompt carries a signal that aborts on it, on the prompt's own signal (a callback server
92
- * beating a `manual_code` prompt), and when the flow settles with a prompt still pending.
93
- */
94
- export declare function login(request: LoginRequest): Promise<LoginResult>;
95
- /** Not public: what the CLI and the tests add to {@link login}. */
45
+ /** What the CLI adds to a sign-in. */
96
46
  export interface LoginInternals {
97
47
  /**
98
48
  * The definition's providers, added to pi's built-ins (a same id replaces one): a key is verified against the
@@ -106,8 +56,23 @@ export interface LoginInternals {
106
56
  */
107
57
  verifyWith?: string;
108
58
  }
109
- /** {@link login}, plus {@link LoginInternals}. */
110
- export declare function loginOver(request: LoginRequest, internals?: LoginInternals): Promise<LoginResult>;
59
+ /**
60
+ * Sign in to one of pi's built-in providers (plus the definition's) and persist the credential to `authPath`, or to
61
+ * the caller's `credentialStore`.
62
+ *
63
+ * The store is checked FIRST (a no-op write runs the file store's refuse-corrupt and writability checks, or whatever
64
+ * the caller's store does on a write), so a flow never runs toward a credential that could not be saved. An entered API key is verified with one minimal request to pi's
65
+ * default model for the provider (its first model when pi names none), at pi's built-in endpoint, BEFORE it is
66
+ * written. Credentials are provider-scoped, so any of the provider's models answers the question the check asks (does
67
+ * the provider refuse the key, HTTP 401); a key the provider rejects is never stored, and the provider's key flow runs
68
+ * again (the provider and method were not the mistake). Progress and outcome go out through `interaction.notify`.
69
+ *
70
+ * Aborting `interaction.signal` at any point, the verification included, rejects with {@link SignInCancelled} and
71
+ * writes no credential (an OAuth device ID, the installation's identity, is kept once created). Every prompt carries a
72
+ * signal that aborts on it, on the prompt's own signal (a callback server beating a `manual_code` prompt), and when the
73
+ * flow settles with a prompt still pending.
74
+ */
75
+ export declare function loginOver(request: LoginRequest, internals?: LoginInternals): Promise<SignInResult>;
111
76
  /**
112
77
  * pi's default model per provider (`defaultModelPerProvider` in pi-coding-agent's model-resolver): the general-purpose
113
78
  * model pi itself starts on, so the likeliest to answer a minimal request. COPIED, because pi does not export it;
@@ -140,12 +105,12 @@ export interface LoginIO {
140
105
  }
141
106
  /**
142
107
  * `fastagent login` in a terminal: ask only what was not given, as pi's `/login` asks it (how to sign in, then the
143
- * provider; a provider named by its id or its name, then how, when it offers both), then {@link login}.
108
+ * provider; a provider named by its id or its name, then how, when it offers both), then {@link loginOver}.
144
109
  */
145
110
  export declare function loginFlow(io: LoginIO, options: {
146
111
  authPath: string;
147
112
  provider?: string;
148
- method?: LoginMethod;
113
+ method?: SignInMethod;
149
114
  providers?: readonly Provider[];
150
115
  signal?: AbortSignal;
151
116
  /**
@@ -155,12 +120,12 @@ export declare function loginFlow(io: LoginIO, options: {
155
120
  statusFrom?: CredentialStore;
156
121
  /** {@link LoginInternals.verifyWith}. */
157
122
  verifyWith?: string;
158
- }): Promise<LoginResult>;
123
+ }): Promise<SignInResult>;
159
124
  /**
160
125
  * The sign-in `given` names, or, with `menus`, the one a person picks; without them a choice not given refuses
161
126
  * ({@link LoginRefused}) naming how to give it.
162
127
  */
163
- export declare function chooseLogin(offered: readonly LoginOption[], given: {
128
+ export declare function chooseLogin(offered: readonly SignInOption[], given: {
164
129
  provider?: string;
165
- method?: LoginMethod;
166
- }, menus: LoginIO | undefined): Promise<LoginOption>;
130
+ method?: SignInMethod;
131
+ }, menus: LoginIO | undefined): Promise<SignInOption>;