@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.
- package/dist/channels/agentcore-protocol.d.ts +2 -1
- package/dist/channels/agentcore-service.js +10 -18
- package/dist/channels/agentcore.js +6 -3
- package/dist/channels/control.js +23 -0
- package/dist/channels/slack/config-api.d.ts +2 -2
- package/dist/channels/slack/config-api.js +2 -2
- package/dist/channels/slack/onboard.js +2 -2
- package/dist/channels/slack/register-webhook.js +2 -2
- package/dist/channels/slack/slack-api.js +4 -4
- package/dist/cli/add-feishu.js +4 -2
- package/dist/cli/auth-view.d.ts +15 -6
- package/dist/cli/auth-view.js +24 -8
- package/dist/cli/box-login.d.ts +33 -0
- package/dist/cli/box-login.js +162 -0
- package/dist/cli/commands/deploy/agentcore.js +48 -11
- package/dist/cli/commands/deploy/docker.js +24 -13
- package/dist/cli/commands/deploy/fly.js +30 -7
- package/dist/cli/commands/deploy/railway.js +21 -7
- package/dist/cli/commands/deploy/shared.d.ts +18 -17
- package/dist/cli/commands/deploy/shared.js +20 -21
- package/dist/cli/commands/destroy.js +2 -2
- package/dist/cli/commands/info.js +9 -6
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/login.d.ts +7 -1
- package/dist/cli/commands/login.js +123 -10
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/models.d.ts +4 -1
- package/dist/cli/commands/models.js +37 -4
- package/dist/cli/commands/routine-run.js +2 -2
- package/dist/cli/commands/start.d.ts +0 -5
- package/dist/cli/commands/start.js +3 -13
- package/dist/cli/kernel.d.ts +2 -0
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/login-relay.d.ts +38 -0
- package/dist/cli/login-relay.js +121 -0
- package/dist/cli/program.js +46 -8
- package/dist/cli/shared.d.ts +4 -4
- package/dist/cli/shared.js +24 -30
- package/dist/deploy/agentcore/plan.d.ts +10 -11
- package/dist/deploy/agentcore/plan.js +8 -14
- package/dist/deploy/agentcore/run.d.ts +8 -1
- package/dist/deploy/agentcore/run.js +20 -14
- package/dist/deploy/agentcore/shell.d.ts +25 -0
- package/dist/deploy/agentcore/shell.js +202 -0
- package/dist/deploy/box-shell.d.ts +43 -0
- package/dist/deploy/box-shell.js +28 -0
- package/dist/deploy/build-context.js +16 -7
- package/dist/deploy/channel-ingress.d.ts +20 -1
- package/dist/deploy/channel-ingress.js +34 -7
- package/dist/deploy/container.d.ts +14 -0
- package/dist/deploy/container.js +45 -1
- package/dist/deploy/docker/plan.d.ts +7 -3
- package/dist/deploy/docker/plan.js +9 -14
- package/dist/deploy/docker/run.d.ts +14 -7
- package/dist/deploy/docker/run.js +58 -35
- package/dist/deploy/fly/plan.d.ts +6 -3
- package/dist/deploy/fly/plan.js +6 -8
- package/dist/deploy/fly/run.d.ts +8 -2
- package/dist/deploy/fly/run.js +15 -25
- package/dist/deploy/preflight.d.ts +9 -7
- package/dist/deploy/preflight.js +74 -20
- package/dist/deploy/railway/plan.d.ts +6 -3
- package/dist/deploy/railway/plan.js +7 -9
- package/dist/deploy/railway/run.d.ts +10 -6
- package/dist/deploy/railway/run.js +29 -13
- package/dist/deploy/runner.d.ts +16 -0
- package/dist/deploy/runner.js +31 -0
- package/dist/deploy/secrets.d.ts +4 -13
- package/dist/deploy/secrets.js +8 -27
- package/dist/dev-supervisor.js +5 -2
- package/dist/engines/pi/agent-models.d.ts +70 -0
- package/dist/engines/pi/agent-models.js +93 -0
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +63 -16
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +22 -16
- package/dist/engines/pi/create.js +28 -28
- package/dist/engines/pi/entry-images.d.ts +14 -0
- package/dist/engines/pi/entry-images.js +23 -0
- package/dist/engines/pi/invoke-session.js +6 -4
- package/dist/engines/pi/login.d.ts +20 -13
- package/dist/engines/pi/login.js +19 -12
- package/dist/engines/pi/models.d.ts +115 -36
- package/dist/engines/pi/models.js +218 -48
- package/dist/engines/pi/open.d.ts +42 -11
- package/dist/engines/pi/open.js +53 -21
- package/dist/engines/pi/service.d.ts +2 -2
- package/dist/engines/pi/service.js +1 -0
- package/dist/engines/pi/session-builder.d.ts +3 -2
- package/dist/engines/pi/session-builder.js +28 -58
- package/dist/engines/pi/session-control.js +14 -3
- package/dist/engines/pi/turn-kit.js +11 -5
- package/dist/open-url.d.ts +15 -1
- package/dist/open-url.js +35 -3
- package/dist/paths.d.ts +4 -2
- package/dist/paths.js +6 -2
- package/dist/pi.d.ts +5 -3
- package/dist/pi.js +3 -2
- package/dist/service.d.ts +5 -8
- package/dist/session-remote.js +21 -0
- package/dist/session.d.ts +28 -7
- package/package.json +2 -1
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent models [search]`: print every "provider/modelId" the agent here (or, outside one or with `-g`, this
|
|
3
|
+
* machine) can name; `[search]` filters by substring. `--refresh` first fetches the model catalog into that scope's
|
|
4
|
+
* `models-store.json`.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
2
8
|
import { formatModelsCommand } from "../models-view.js";
|
|
3
9
|
import { listModels } from "../../engines/pi/config.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
import { createPiModelRuntime, globalCatalogPath, machineModelRuntime, refreshGlobalModelCatalog, } from "../../engines/pi/models.js";
|
|
11
|
+
import { refreshModelCatalog } from "../../engines/pi/open.js";
|
|
12
|
+
import { AGENT_MODEL_CATALOG_FILE, findAgentDir, globalHome, placementDeadEnd } from "../../paths.js";
|
|
13
|
+
import { enterAgentEnv } from "../../env.js";
|
|
14
|
+
import { failStartup, placementOrExit } from "../fail.js";
|
|
15
|
+
export async function runModels(search, opts = {}) {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const agentDir = opts.global ? undefined : findAgentDir(cwd);
|
|
18
|
+
// "Outside an agent" must mean exactly that, as for `login`: inside an agent's subdirectory, or among several agents
|
|
19
|
+
// with no default, falling back to the machine would list without the agent's own catalog and models.json, and a
|
|
20
|
+
// refresh would record the models where no deploy carries them. Refused either way; `-g` asks for the machine.
|
|
21
|
+
if (!opts.global && !agentDir && placementDeadEnd(cwd))
|
|
22
|
+
placementOrExit(cwd);
|
|
23
|
+
if (opts.refresh) {
|
|
24
|
+
// pi fetches a provider's catalog only with a usable credential for it: the scope's own credentials and `.env`
|
|
25
|
+
// keys, as `login` reads them.
|
|
26
|
+
enterAgentEnv(agentDir ?? globalHome());
|
|
27
|
+
if (agentDir) {
|
|
28
|
+
await refreshModelCatalog(agentDir).catch(failStartup);
|
|
29
|
+
console.error(`[fastagent] refreshed ${join(agentDir, AGENT_MODEL_CATALOG_FILE)} — commit it: it ships with a deploy`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await refreshGlobalModelCatalog().catch(failStartup);
|
|
33
|
+
console.error(`[fastagent] refreshed ${globalCatalogPath()} — every agent on this machine reads it`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const models = await (agentDir
|
|
37
|
+
? createPiModelRuntime({ agentDir, credentials: new InMemoryCredentialStore() })
|
|
38
|
+
: machineModelRuntime()).catch(failStartup);
|
|
39
|
+
const { lines, error } = formatModelsCommand(listModels(models), search);
|
|
7
40
|
for (const spec of lines)
|
|
8
41
|
console.log(spec);
|
|
9
42
|
if (error)
|
|
@@ -36,11 +36,11 @@ export async function runRoutine(name, dirArg, opts) {
|
|
|
36
36
|
// were printed above: its guarantee must not depend on this call site remembering (a repeated line
|
|
37
37
|
// on the refusal path is the cheaper failure).
|
|
38
38
|
gateSecretsOrExit({ declared: secrets, failures, owner: name });
|
|
39
|
-
const { agent, modelSpec,
|
|
39
|
+
const { agent, modelSpec, models } = await createPiAgentFromDir(placement.agentDir, {
|
|
40
40
|
model: opts.model,
|
|
41
41
|
}).catch(failStartup);
|
|
42
42
|
console.error(`[fastagent] routine run: ${name} (${modelSpec})`);
|
|
43
|
-
await reportAuth(
|
|
43
|
+
await reportAuth(models, modelSpec);
|
|
44
44
|
const exitCode = await runInvokeStream(agent.invoke({ session: routineSession(name) }, { text: routine.prompt }), (text) => process.stdout.write(text), (line) => console.error(line));
|
|
45
45
|
process.stdout.write("\n");
|
|
46
46
|
process.exit(exitCode);
|
|
@@ -28,11 +28,6 @@ export interface PreparedWorkspace {
|
|
|
28
28
|
* volume.
|
|
29
29
|
*/
|
|
30
30
|
export declare function prepareStartWorkspace(dirArg: string): Promise<PreparedWorkspace>;
|
|
31
|
-
/**
|
|
32
|
-
* Stage two — RUN in the prepared workspace: install the agent's dependencies, then open through the workspace's own
|
|
33
|
-
* FastAgent install.
|
|
34
|
-
*/
|
|
35
|
-
export declare function openPreparedWorkspace(prepared: PreparedWorkspace, opts: StartOptions): Promise<StartedService>;
|
|
36
31
|
export declare function openStartService(dirArg: string, opts: StartOptions): Promise<StartedService>;
|
|
37
32
|
/** Internal entry loaded from the active workspace's installed package after storage initialization. */
|
|
38
33
|
export declare function openPreparedStartService(dirArg: string, opts: StartOptions): Promise<StartedService>;
|
|
@@ -12,11 +12,9 @@ import { pathToFileURL } from "node:url";
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { readFile } from "node:fs/promises";
|
|
14
14
|
import * as Effect from "effect/Effect";
|
|
15
|
-
import {
|
|
16
|
-
import { applyCarriedEnv, authSeedBytes, collectAuthSeed } from "../../deploy/secrets.js";
|
|
15
|
+
import { applyCarriedEnv } from "../../deploy/secrets.js";
|
|
17
16
|
import { applyReleaseEnv, installAgentDependencies, parseDeploymentRelease, prepareDeployment, } from "../../deploy/workspace.js";
|
|
18
|
-
import {
|
|
19
|
-
import { SECRET_FILE_MODE, resolveSecretsDir, isAgentcoreRuntime, isUnderDir, exists } from "../../paths.js";
|
|
17
|
+
import { resolveSecretsDir, isAgentcoreRuntime, isUnderDir, exists } from "../../paths.js";
|
|
20
18
|
import { log, setLogLevel } from "../../log.js";
|
|
21
19
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
22
20
|
import { DEFAULT_HTTP_PORT, mountAgentService } from "../../service.js";
|
|
@@ -100,7 +98,7 @@ export async function prepareStartWorkspace(dirArg) {
|
|
|
100
98
|
* Stage two — RUN in the prepared workspace: install the agent's dependencies, then open through the workspace's own
|
|
101
99
|
* FastAgent install.
|
|
102
100
|
*/
|
|
103
|
-
|
|
101
|
+
async function openPreparedWorkspace(prepared, opts) {
|
|
104
102
|
let open = openPreparedStartService;
|
|
105
103
|
if (prepared.deployed) {
|
|
106
104
|
const { root, agent } = prepared.deployed;
|
|
@@ -126,7 +124,6 @@ export async function openStartService(dirArg, opts) {
|
|
|
126
124
|
/** Internal entry loaded from the active workspace's installed package after storage initialization. */
|
|
127
125
|
export async function openPreparedStartService(dirArg, opts) {
|
|
128
126
|
const placement = await enterAgentCommand(dirArg, opts);
|
|
129
|
-
await maybeSeedAuth(resolveAuthPath(placement.agentDir));
|
|
130
127
|
const opened = await createPiAgentFromDir(placement.agentDir, {
|
|
131
128
|
model: opts.model,
|
|
132
129
|
serving: true,
|
|
@@ -152,13 +149,6 @@ export async function openPreparedStartService(dirArg, opts) {
|
|
|
152
149
|
: mountAgentService(mountable, cliMountOptions(() => traced)));
|
|
153
150
|
return { ...service, stateRoot, port: config.http?.port ?? DEFAULT_HTTP_PORT };
|
|
154
151
|
}
|
|
155
|
-
async function maybeSeedAuth(authPath) {
|
|
156
|
-
const bytes = authSeedBytes(collectAuthSeed(process.env), await exists(authPath));
|
|
157
|
-
if (!bytes)
|
|
158
|
-
return;
|
|
159
|
-
writeFileAtomic(authPath, bytes, SECRET_FILE_MODE); // creates the directory it needs
|
|
160
|
-
log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
|
|
161
|
-
}
|
|
162
152
|
/**
|
|
163
153
|
* Install the wake-ALARM sink before the scheduler starts: the first wake poll may advance a recurring entry, and that
|
|
164
154
|
* save must already re-arm its alarm.
|
package/dist/cli/kernel.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface ArgSpec {
|
|
|
12
12
|
export interface FlagSpec {
|
|
13
13
|
flags: string;
|
|
14
14
|
description: string;
|
|
15
|
+
/** Parsed but left out of help: an internal seam between two fastagent processes, not a user-facing flag. */
|
|
16
|
+
hidden?: boolean;
|
|
15
17
|
}
|
|
16
18
|
interface ExampleSpec {
|
|
17
19
|
cmd: string;
|
package/dist/cli/kernel.js
CHANGED
|
@@ -62,7 +62,7 @@ function register(parent, spec) {
|
|
|
62
62
|
cmd.addArgument(arg);
|
|
63
63
|
}
|
|
64
64
|
for (const f of spec.flags ?? [])
|
|
65
|
-
cmd.addOption(new Option(f.flags, f.description));
|
|
65
|
+
cmd.addOption(new Option(f.flags, f.description).hideHelp(f.hidden === true));
|
|
66
66
|
for (const sub of spec.subcommands ?? [])
|
|
67
67
|
register(cmd, sub);
|
|
68
68
|
const run = spec.run;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { LoginIO, LoginMethod } from "../engines/pi/login.ts";
|
|
3
|
+
/** How a login on the box ended, as the box reports it. */
|
|
4
|
+
export type RelayResult =
|
|
5
|
+
/** Logged in now: `method` is how, `path` where the box saved it. */
|
|
6
|
+
{
|
|
7
|
+
ok: true;
|
|
8
|
+
provider: string;
|
|
9
|
+
method: LoginMethod;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `--if-missing`: the box already authenticates the provider and was left alone. `source` is what does it, as the
|
|
14
|
+
* box's startup report names it (`OAuth`, `stored credential`, or an environment variable such as `OPENAI_API_KEY`).
|
|
15
|
+
*/
|
|
16
|
+
| {
|
|
17
|
+
ok: true;
|
|
18
|
+
provider: string;
|
|
19
|
+
kept: string;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: "cancelled" | "missing" | "failed";
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The box half: a {@link LoginIO} over `input`/`output`. Once `input` ends (the session dropped), every pending and
|
|
27
|
+
* later question reads as backed out, so the flow ends in `LoginCancelled` instead of waiting forever.
|
|
28
|
+
*/
|
|
29
|
+
export declare function stdioLoginIO(input: Readable, output: Writable): {
|
|
30
|
+
io: LoginIO;
|
|
31
|
+
result(result: RelayResult): void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The terminal half: drive `io` from what the box sends on `from`, answering on `to`. Resolves with the box's
|
|
35
|
+
* `result` line when `from` ends, or `undefined` when it ended without one. A line that is not the wire (a host
|
|
36
|
+
* CLI's banner, a shell's error) goes to `passthrough`, so nothing the box printed is lost.
|
|
37
|
+
*/
|
|
38
|
+
export declare function relayLogin(from: Readable, to: Writable, io: LoginIO, passthrough: (line: string) => void): Promise<RelayResult | undefined>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent login` split across a process boundary: the flow runs on a deployed box, and the owner's terminal renders
|
|
3
|
+
* it. The wire is {@link LoginIO} itself, one JSON object per line: the box sends what the flow asks and says
|
|
4
|
+
* (`select`, `prompt`, `abort`, `note`, `open`) and ends with ONE `result` line; the terminal answers each question by
|
|
5
|
+
* its id. The box holds the PKCE verifier and writes the credential; the terminal only shows and answers.
|
|
6
|
+
*
|
|
7
|
+
* The `result` line is the only success signal. A host's shell can drop the session and still exit 0 (Railway was
|
|
8
|
+
* seen doing so), so an ended stream without that line is a failure, whatever the exit code says.
|
|
9
|
+
*/
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
const send = (out, message) => void out.write(`${JSON.stringify(message)}\n`);
|
|
12
|
+
/**
|
|
13
|
+
* The box half: a {@link LoginIO} over `input`/`output`. Once `input` ends (the session dropped), every pending and
|
|
14
|
+
* later question reads as backed out, so the flow ends in `LoginCancelled` instead of waiting forever.
|
|
15
|
+
*/
|
|
16
|
+
export function stdioLoginIO(input, output) {
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
let closed = false;
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
const lines = createInterface({ input });
|
|
21
|
+
lines.on("line", (line) => {
|
|
22
|
+
const answer = JSON.parse(line);
|
|
23
|
+
pending.get(answer.id)?.(answer.value ?? undefined);
|
|
24
|
+
pending.delete(answer.id);
|
|
25
|
+
});
|
|
26
|
+
lines.on("close", () => {
|
|
27
|
+
closed = true;
|
|
28
|
+
for (const settle of pending.values())
|
|
29
|
+
settle(undefined);
|
|
30
|
+
pending.clear();
|
|
31
|
+
});
|
|
32
|
+
const ask = (message, signal) => {
|
|
33
|
+
if (closed)
|
|
34
|
+
return Promise.resolve(undefined);
|
|
35
|
+
const id = nextId++;
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
pending.set(id, resolve);
|
|
38
|
+
send(output, message(id));
|
|
39
|
+
signal?.addEventListener("abort", () => {
|
|
40
|
+
if (!pending.delete(id))
|
|
41
|
+
return;
|
|
42
|
+
send(output, { type: "abort", id });
|
|
43
|
+
resolve(undefined);
|
|
44
|
+
}, { once: true });
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
io: {
|
|
49
|
+
select: (message, options) => ask((id) => ({ type: "select", id, message, options })),
|
|
50
|
+
prompt: (message, opts) => ask((id) => ({ type: "prompt", id, message, hidden: opts?.hidden === true }), opts?.signal),
|
|
51
|
+
note: (message) => send(output, { type: "note", message }),
|
|
52
|
+
openUrl: (url) => send(output, { type: "open", url }),
|
|
53
|
+
},
|
|
54
|
+
result: (result) => {
|
|
55
|
+
send(output, { type: "result", ...result });
|
|
56
|
+
lines.close(); // nothing more is read; lets the process exit once stdout drains
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The terminal half: drive `io` from what the box sends on `from`, answering on `to`. Resolves with the box's
|
|
62
|
+
* `result` line when `from` ends, or `undefined` when it ended without one. A line that is not the wire (a host
|
|
63
|
+
* CLI's banner, a shell's error) goes to `passthrough`, so nothing the box printed is lost.
|
|
64
|
+
*/
|
|
65
|
+
export function relayLogin(from, to, io, passthrough) {
|
|
66
|
+
// The box may exit before reading an answer; its result (or its absence) is what gets reported, not the EPIPE.
|
|
67
|
+
to.on("error", () => { });
|
|
68
|
+
const answer = (id, value) => send(to, { id, value: value ?? null });
|
|
69
|
+
const prompts = new Map();
|
|
70
|
+
let result;
|
|
71
|
+
const lines = createInterface({ input: from });
|
|
72
|
+
lines.on("line", (line) => {
|
|
73
|
+
const message = parse(line);
|
|
74
|
+
if (message === undefined)
|
|
75
|
+
return passthrough(line);
|
|
76
|
+
switch (message.type) {
|
|
77
|
+
case "select":
|
|
78
|
+
void io.select(message.message, message.options).then((value) => answer(message.id, value));
|
|
79
|
+
return;
|
|
80
|
+
case "prompt": {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
prompts.set(message.id, controller);
|
|
83
|
+
void io.prompt(message.message, { hidden: message.hidden, signal: controller.signal }).then((value) => {
|
|
84
|
+
prompts.delete(message.id);
|
|
85
|
+
if (!controller.signal.aborted)
|
|
86
|
+
answer(message.id, value);
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case "abort":
|
|
91
|
+
prompts.get(message.id)?.abort();
|
|
92
|
+
return;
|
|
93
|
+
case "note":
|
|
94
|
+
return io.note(message.message);
|
|
95
|
+
case "open":
|
|
96
|
+
return io.openUrl(message.url);
|
|
97
|
+
case "result": {
|
|
98
|
+
const { type: _, ...rest } = message;
|
|
99
|
+
result = rest;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return new Promise((resolve) => lines.on("close", () => {
|
|
105
|
+
for (const controller of prompts.values())
|
|
106
|
+
controller.abort();
|
|
107
|
+
resolve(result);
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
const WIRE_TYPES = new Set(["select", "prompt", "abort", "note", "open", "result"]);
|
|
111
|
+
function parse(line) {
|
|
112
|
+
if (!line.startsWith("{"))
|
|
113
|
+
return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const value = JSON.parse(line);
|
|
116
|
+
return typeof value.type === "string" && WIRE_TYPES.has(value.type) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined; // not ours: a host CLI printed a line that happens to start with "{"
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/cli/program.js
CHANGED
|
@@ -156,13 +156,30 @@ const invoke = {
|
|
|
156
156
|
const models = {
|
|
157
157
|
name: "models",
|
|
158
158
|
summary: 'list the available "provider/modelId" model specs',
|
|
159
|
-
description: 'List every
|
|
159
|
+
description: 'List every "provider/modelId" spec the agent in this directory can name (pi\'s built-ins, the model ' +
|
|
160
|
+
"catalogs and models.json files, its own over the machine's) — use one with --model or as `model` in " +
|
|
161
|
+
"fastagent.config.ts. Outside an agent, or with -g, list the machine's.",
|
|
160
162
|
args: [{ name: "[search]", description: "case-insensitive substring filter" }],
|
|
163
|
+
flags: [
|
|
164
|
+
{
|
|
165
|
+
flags: "--refresh",
|
|
166
|
+
description: "fetch the model catalog first, into the agent's models-store.json (with -g, the machine's)",
|
|
167
|
+
},
|
|
168
|
+
{ flags: "-g, --global", description: "the machine's catalog, ~/.fastagent/models-store.json" },
|
|
169
|
+
],
|
|
161
170
|
examples: [
|
|
162
171
|
{ cmd: "fastagent models", note: "all specs" },
|
|
163
172
|
{ cmd: "fastagent models claude", note: "filter; provider-name matches rank first" },
|
|
173
|
+
{ cmd: "fastagent models --refresh", note: "models newer than the installed pi" },
|
|
164
174
|
],
|
|
165
|
-
|
|
175
|
+
notes: "A refresh asks pi.dev for the providers the scope's credentials authenticate (the agent's, or with -g the " +
|
|
176
|
+
"global auth.json and environment) and writes the answer to models-store.json. The agent's is part of the " +
|
|
177
|
+
"definition: commit it, and it ships with a deploy. The machine's (-g) is read by every agent here and does " +
|
|
178
|
+
"not ship. Nothing refreshes on its own.",
|
|
179
|
+
run: async (args, f) => (await import("./commands/models.js")).runModels(args[0], {
|
|
180
|
+
refresh: f.refresh === true,
|
|
181
|
+
global: f.global === true,
|
|
182
|
+
}),
|
|
166
183
|
};
|
|
167
184
|
const start = {
|
|
168
185
|
name: "start",
|
|
@@ -300,7 +317,8 @@ const deploy = {
|
|
|
300
317
|
description: "drive the target CLI to completion. Docker runs `docker compose up -d --build`; with a tunnel " +
|
|
301
318
|
"service, reads its URL and registers webhooks. Fly/Railway provision app/service + volume + " +
|
|
302
319
|
"secrets + deploy + webhook setup. AgentCore builds/pushes the arm64 image and deploys the " +
|
|
303
|
-
"stack (aws + docker CLIs).
|
|
320
|
+
"stack (aws + docker CLIs). A model key in the value file travels; any other credential is a login on " +
|
|
321
|
+
"the deployment itself, which --run starts once the box is up (fastagent login --deployment). " +
|
|
304
322
|
"Stops at a gate (missing CLI/daemon/login/secret) with one actionable line. Without it: prints " +
|
|
305
323
|
"the runbook",
|
|
306
324
|
},
|
|
@@ -438,21 +456,41 @@ const login = {
|
|
|
438
456
|
"agent it writes the global ~/.fastagent/.secrets/auth.json, and says so): pick a method " +
|
|
439
457
|
"(subscription/OAuth or API " +
|
|
440
458
|
"key), then a provider that offers it (configured status shown). [provider] takes the method from " +
|
|
441
|
-
"what that provider supports, asked only when both. An agent READS the global store for
|
|
442
|
-
"its own file
|
|
459
|
+
"what that provider supports, asked only when both. An agent READS the global store for a provider it has " +
|
|
460
|
+
"no other credential for (its own file, a models.json key, an env variable), so `-g` logs in once for every " +
|
|
461
|
+
"agent on this machine; it warns when an env variable already authenticates the provider.",
|
|
443
462
|
args: [{ name: "[provider]", description: "provider id (skip the provider menu)" }],
|
|
444
463
|
flags: [
|
|
445
464
|
{
|
|
446
465
|
flags: "-g, --global",
|
|
447
|
-
description: `store in ~/.fastagent/.secrets/auth.json — every agent here reads it for
|
|
466
|
+
description: `store in ~/.fastagent/.secrets/auth.json — every agent here reads it for a provider it has no other credential for`,
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
flags: "--deployment [host]",
|
|
470
|
+
description: "log in this agent's deployment instead, on the box itself (docker, fly, railway, agentcore); the host may be left out " +
|
|
471
|
+
"when the agent dir holds one host's deploy artifacts",
|
|
448
472
|
},
|
|
473
|
+
{ flags: "--stdio", description: "the box's half of --deployment", hidden: true },
|
|
474
|
+
{ flags: "--if-missing", description: "with --stdio: keep a stored credential for the provider", hidden: true },
|
|
449
475
|
NO_INPUT,
|
|
450
476
|
],
|
|
451
|
-
examples: [
|
|
452
|
-
|
|
477
|
+
examples: [
|
|
478
|
+
{ cmd: "fastagent login" },
|
|
479
|
+
{ cmd: "fastagent login openai" },
|
|
480
|
+
{ cmd: "fastagent login openai -g" },
|
|
481
|
+
{ cmd: "fastagent login openai-codex --deployment fly", note: "on the box" },
|
|
482
|
+
],
|
|
483
|
+
notes: "The positional is the PROVIDER (not a dir) — `cd` into your agent before logging in. " +
|
|
484
|
+
"--deployment runs the login on the deployed box through the host's own shell (docker compose exec, fly ssh, " +
|
|
485
|
+
"railway ssh, AgentCore's command shell); this terminal shows it, opens the browser, and catches the browser's " +
|
|
486
|
+
"return to localhost (or asks you to paste that address when its port is taken). The box keeps the credential, " +
|
|
487
|
+
"so it is the only holder of that grant, and logging in again replaces it there.",
|
|
453
488
|
run: async (args, f) => (await import("./commands/login.js")).runLogin(args[0], {
|
|
454
489
|
global: f.global === true,
|
|
455
490
|
input: f.input !== false,
|
|
491
|
+
...(f.deployment !== undefined ? { deployment: f.deployment } : {}),
|
|
492
|
+
stdio: f.stdio === true,
|
|
493
|
+
ifMissing: f.ifMissing === true,
|
|
456
494
|
}),
|
|
457
495
|
};
|
|
458
496
|
/**
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type AuthLayers } from "../engines/pi/config.ts";
|
|
2
1
|
import { type LoginIO } from "../engines/pi/login.ts";
|
|
2
|
+
import { type AgentModels } from "../engines/pi/agent-models.ts";
|
|
3
3
|
import type { LoadedDefinition } from "../engines/pi/definition.ts";
|
|
4
4
|
import type { ToolCollision } from "../engines/pi/tool.ts";
|
|
5
|
-
import type
|
|
5
|
+
import { type ResolvedPlacement } from "../paths.ts";
|
|
6
6
|
/** How every command that runs the model enters its agent directory, in the one order that works. */
|
|
7
7
|
export declare function enterAgentCommand(dirArg: string, opts: {
|
|
8
8
|
model?: string;
|
|
@@ -13,7 +13,7 @@ export interface ReportableAssembly {
|
|
|
13
13
|
agentDir: string;
|
|
14
14
|
workspace: string;
|
|
15
15
|
modelSpec: string;
|
|
16
|
-
|
|
16
|
+
models: AgentModels;
|
|
17
17
|
config: {
|
|
18
18
|
thinkingLevel?: string;
|
|
19
19
|
};
|
|
@@ -39,6 +39,6 @@ export declare function parsePort(value: string | undefined, source: string, fro
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function parseBind(value: string | undefined): string | undefined;
|
|
41
41
|
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. */
|
|
42
|
-
export declare function reportAuth(
|
|
42
|
+
export declare function reportAuth(models: AgentModels, modelSpec: string): Promise<void>;
|
|
43
43
|
/** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. */
|
|
44
44
|
export declare function terminalLoginIO(): LoginIO;
|
package/dist/cli/shared.js
CHANGED
|
@@ -6,14 +6,15 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
6
6
|
import { relative, resolve } from "node:path";
|
|
7
7
|
import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
|
|
8
8
|
import { buildModelPickerOptions } from "./models-view.js";
|
|
9
|
-
import {
|
|
10
|
-
import { isValidPort, listModels, loadConfig, providerOf, resolveAuthLayers, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
9
|
+
import { isValidPort, listModels, loadConfig, providerOf, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
11
10
|
import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
|
|
12
11
|
import { readMachine, withMachine } from "../engines/pi/machine.js";
|
|
13
|
-
import {
|
|
12
|
+
import { providerAuthStatuses } from "../engines/pi/models.js";
|
|
13
|
+
import { agentModels } from "../engines/pi/agent-models.js";
|
|
14
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
15
15
|
import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
|
|
16
16
|
import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
17
|
+
import { isDeployedWorkspace } from "../paths.js";
|
|
17
18
|
import { log } from "../log.js";
|
|
18
19
|
import { enterAgentEnv } from "../env.js";
|
|
19
20
|
import { openExternalUrl } from "../open-url.js";
|
|
@@ -37,7 +38,7 @@ export async function reportAssembly(a, extras = {}) {
|
|
|
37
38
|
for (const [label, value] of extras.beforeModel ?? [])
|
|
38
39
|
reportLine(label, value);
|
|
39
40
|
reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
40
|
-
await reportAuth(a.
|
|
41
|
+
await reportAuth(a.models, a.modelSpec);
|
|
41
42
|
reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
42
43
|
if (a.definition.persona)
|
|
43
44
|
reportLine("persona", "persona.md");
|
|
@@ -87,26 +88,18 @@ export function parseBind(value) {
|
|
|
87
88
|
return bindAddress(trimmed); // a name never travels past this point — see bind.ts
|
|
88
89
|
}
|
|
89
90
|
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. */
|
|
90
|
-
export async function reportAuth(
|
|
91
|
+
export async function reportAuth(models, modelSpec) {
|
|
91
92
|
const provider = providerOf(modelSpec);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
// Name the layer the credential actually came from: "which file do I edit" is the question this line answers, and
|
|
103
|
-
// a global credential lives in a file the agent dir does not contain. With NEITHER layer holding it, the answer is
|
|
104
|
-
// the primary — that is the file the `fastagent login` this report recommends writes.
|
|
105
|
-
const found = fallbackAuthPath !== undefined && inFallback ? fallbackAuthPath : authPath;
|
|
106
|
-
// Only when nothing satisfies auth does the stored credential matter: it tells "nothing stored" from "stored but
|
|
107
|
-
// unusable".
|
|
108
|
-
const stored = source === undefined ? (inPrimary ?? inFallback) : undefined;
|
|
109
|
-
const report = formatAuthReport(provider, found, source, stored);
|
|
93
|
+
// No files: the opener was handed a credential store, which is its caller's to describe.
|
|
94
|
+
if (models.auth === undefined) {
|
|
95
|
+
log.info(`[fastagent] auth: the caller's credential store (${provider})`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// The agent's own model environment, the one its turns run on and a deployed box's `login --if-missing` asks: the
|
|
99
|
+
// line cannot name a file, or a source, other than the ones the runtime uses.
|
|
100
|
+
const modelId = modelSpec.slice(provider.length + 1);
|
|
101
|
+
const status = await models.authStatus(provider, modelId).catch(failStartup);
|
|
102
|
+
const report = formatAuthReport({ provider, path: models.auth.path, ...status, deployed: isDeployedWorkspace() });
|
|
110
103
|
log.info(`[fastagent] ${report.line}`);
|
|
111
104
|
if (report.warn)
|
|
112
105
|
log.warn(`[fastagent] ${report.warn}`);
|
|
@@ -122,11 +115,11 @@ async function resolveFirstRunModel(agentDir, options = {}) {
|
|
|
122
115
|
return; // --no-input: never prompt (clig) — the opener raises the clear error
|
|
123
116
|
if (!isInteractive())
|
|
124
117
|
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
125
|
-
const
|
|
118
|
+
const environment = agentModels(agentDir);
|
|
126
119
|
// The picker lists the AGENT's surface: built-ins plus whatever its models.json declares, so a self-hosted endpoint
|
|
127
120
|
// is pickable on first run instead of being invisible until hand-set.
|
|
128
|
-
const models = await
|
|
129
|
-
const chosen = await pickWithCredentials(models, auth.path, agentDir);
|
|
121
|
+
const models = await environment.runtime().catch(failStartup);
|
|
122
|
+
const chosen = await pickWithCredentials(models, environment.auth.path, agentDir);
|
|
130
123
|
if (chosen === undefined)
|
|
131
124
|
return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
|
|
132
125
|
process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
|
|
@@ -197,10 +190,10 @@ export function terminalLoginIO() {
|
|
|
197
190
|
}
|
|
198
191
|
/** Best-effort persist the picked model so the next run does not prompt. */
|
|
199
192
|
async function persistModelChoice(agentDir, configPath, spec) {
|
|
200
|
-
const hint = () => console.error(
|
|
193
|
+
const hint = (why = "") => console.error(
|
|
201
194
|
// The pick lives in THIS process's environment only, so it serves this run and nothing that outlives it: a
|
|
202
195
|
// deployment resolves in the environment being deployed, where this machine's variables do not exist.
|
|
203
|
-
`[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
|
|
196
|
+
`[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist${why}`);
|
|
204
197
|
if (!configPath)
|
|
205
198
|
return hint();
|
|
206
199
|
try {
|
|
@@ -210,7 +203,8 @@ async function persistModelChoice(agentDir, configPath, spec) {
|
|
|
210
203
|
await writeFile(configPath, replaced);
|
|
211
204
|
console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(agentDir, configPath)}`);
|
|
212
205
|
}
|
|
213
|
-
catch {
|
|
214
|
-
|
|
206
|
+
catch (error) {
|
|
207
|
+
// The run goes on with the pick either way; the reason the file was not written is what the author needs to fix.
|
|
208
|
+
hint(` (could not write ${configPath}: ${error.message})`);
|
|
215
209
|
}
|
|
216
210
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
2
2
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
3
|
-
import {
|
|
3
|
+
import type { DeploymentSecret } from "../secrets.ts";
|
|
4
4
|
/** The one schedule fact the plan needs (from loadRoutines) — name + cron + tz. */
|
|
5
5
|
export interface ScheduleFact {
|
|
6
6
|
name: string;
|
|
@@ -10,8 +10,6 @@ export interface ScheduleFact {
|
|
|
10
10
|
export interface AgentcorePlanInput extends ContainerInput {
|
|
11
11
|
/** Base name (dir basename) — shapes the runtime name, stack name, ECR repo, session id. */
|
|
12
12
|
name: string;
|
|
13
|
-
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
14
|
-
modelAuth: string | undefined;
|
|
15
13
|
/**
|
|
16
14
|
* Every declared channel and its ingress — the source of the webhook steps, and whether the
|
|
17
15
|
* forwarder is needed at all (ANY webhook channel requires it, customs included).
|
|
@@ -30,6 +28,12 @@ export interface AgentcorePlanInput extends ContainerInput {
|
|
|
30
28
|
* {@link DEFAULT_IDLE_TIMEOUT_SECONDS}.
|
|
31
29
|
*/
|
|
32
30
|
idleTimeoutSeconds?: number;
|
|
31
|
+
/**
|
|
32
|
+
* The provider the runtime logs in to itself (`fastagent login --deployment`) after every deploy and every 14-day
|
|
33
|
+
* idle reset: its storage, and
|
|
34
|
+
* the login with it, resets each time.
|
|
35
|
+
*/
|
|
36
|
+
boxLogin?: string;
|
|
33
37
|
}
|
|
34
38
|
export interface AgentcorePlan {
|
|
35
39
|
/** template + forwarder + Dockerfile/.dockerignore — written by the CLI (kept unless --force). */
|
|
@@ -69,18 +73,13 @@ export declare function wakeAlarmPrefix(name: string): string;
|
|
|
69
73
|
/** Account-suffixed artifact bucket for S3's global namespace. */
|
|
70
74
|
export declare function deploymentBucketName(name: string, account: string): string;
|
|
71
75
|
/**
|
|
72
|
-
* AgentCore env values max 2048 chars — a
|
|
73
|
-
* `<NAME>` + `_2`… (collectChunked reassembles at boot)
|
|
74
|
-
* value file's variables.
|
|
76
|
+
* AgentCore env values max 2048 chars — a value file's variables easily exceed it, so the carrier is CHUNKED across
|
|
77
|
+
* `<NAME>` + `_2`… (collectChunked reassembles at boot).
|
|
75
78
|
*/
|
|
76
79
|
export declare const CARRIER_CHUNK_SIZE = 2000;
|
|
77
80
|
export declare const CARRIER_MAX_CHUNKS = 4;
|
|
78
|
-
/** The template's
|
|
81
|
+
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
79
82
|
export declare const CARRIERS: readonly [{
|
|
80
|
-
readonly param: "FastagentAuthSeed";
|
|
81
|
-
readonly env: "FASTAGENT_AUTH_SEED";
|
|
82
|
-
readonly what: "base64 auth.json";
|
|
83
|
-
}, {
|
|
84
83
|
readonly param: "FastagentEnv";
|
|
85
84
|
readonly env: "FASTAGENT_ENV";
|
|
86
85
|
readonly what: "base64 JSON of the value file's variables";
|