@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.
- package/README.md +3 -2
- package/dist/agent-api.d.ts +10 -2
- package/dist/agent-api.js +10 -2
- package/dist/authoring.d.ts +12 -8
- package/dist/authoring.js +14 -7
- package/dist/cli/add-feishu.js +1 -1
- package/dist/cli/add-slack.js +1 -1
- package/dist/cli/box-login.d.ts +3 -2
- package/dist/cli/commands/add.js +2 -1
- package/dist/cli/commands/deploy.js +1 -1
- package/dist/cli/commands/env.js +5 -4
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/commands/invoke.js +2 -1
- package/dist/cli/commands/login.js +6 -4
- package/dist/cli/login-relay.d.ts +4 -3
- package/dist/cli/login-relay.js +1 -1
- package/dist/cli/shared.js +5 -3
- package/dist/content/git.d.ts +1 -1
- package/dist/content/git.js +7 -4
- package/dist/content/resolve.d.ts +1 -1
- package/dist/content/resolve.js +2 -2
- package/dist/deploy/preflight.js +1 -1
- package/dist/dev-supervisor.js +1 -1
- package/dist/env.d.ts +0 -13
- package/dist/env.js +1 -44
- package/dist/environment/mise.d.ts +8 -7
- package/dist/environment/mise.js +24 -20
- package/dist/git.d.ts +2 -2
- package/dist/git.js +5 -5
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +16 -10
- package/dist/harnesses/models/host.d.ts +30 -0
- package/dist/harnesses/models/host.js +97 -0
- package/dist/harnesses/models/login.d.ts +38 -73
- package/dist/harnesses/models/login.js +50 -50
- package/dist/harnesses/models/models.d.ts +4 -3
- package/dist/harnesses/models/models.js +51 -12
- package/dist/harnesses/pi-durable/agent.js +2 -0
- package/dist/harnesses/pi-durable/open.d.ts +7 -0
- package/dist/harnesses/pi-durable/open.js +4 -3
- package/dist/harnesses/pi-durable/serve.d.ts +11 -1
- package/dist/harnesses/pi-durable/serve.js +42 -12
- package/dist/harnesses/pi-durable/wake.d.ts +8 -1
- package/dist/harnesses/pi-durable/wake.js +10 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -0
- package/dist/scaffold/add-channel.js +1 -1
- package/dist/sign-in-api.d.ts +147 -0
- package/dist/sign-in-api.js +9 -0
- package/dist/tunnel.js +2 -1
- package/dist/value-file.d.ts +19 -0
- package/dist/value-file.js +61 -5
- package/package.json +1 -1
package/dist/environment/mise.js
CHANGED
|
@@ -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
|
|
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
|
|
81
|
-
* the error, which then says what mise said; `capture` returns its stdout instead
|
|
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,
|
|
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`,
|
|
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: { ...
|
|
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
|
-
*
|
|
250
|
-
* yet, and put them first on
|
|
251
|
-
*
|
|
252
|
-
*
|
|
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
|
|
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
|
|
266
|
+
return {};
|
|
262
267
|
const bin = miseBinary(agentDir);
|
|
263
268
|
if (!bin)
|
|
264
269
|
throw missingMise(agentDir);
|
|
265
|
-
await installPackages(agentDir, bin, packages);
|
|
266
|
-
|
|
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 = () => ({ ...
|
|
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.
|
|
38
|
-
*
|
|
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.
|
|
18
|
-
*
|
|
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
|
|
30
|
-
const
|
|
31
|
-
const
|
|
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,
|
|
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
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
17
|
-
constructor(message: string, choices: readonly
|
|
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`
|
|
45
|
-
* one here: a credential `credentials` (a credentials file, or
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
110
|
-
|
|
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
|
|
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?:
|
|
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<
|
|
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
|
|
128
|
+
export declare function chooseLogin(offered: readonly SignInOption[], given: {
|
|
164
129
|
provider?: string;
|
|
165
|
-
method?:
|
|
166
|
-
}, menus: LoginIO | undefined): Promise<
|
|
130
|
+
method?: SignInMethod;
|
|
131
|
+
}, menus: LoginIO | undefined): Promise<SignInOption>;
|