@fastagent-sh/fastagent 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/channels/agentcore-protocol.d.ts +2 -1
  2. package/dist/channels/agentcore-service.js +10 -18
  3. package/dist/channels/agentcore.js +6 -3
  4. package/dist/channels/control.js +23 -0
  5. package/dist/channels/slack/config-api.d.ts +2 -2
  6. package/dist/channels/slack/config-api.js +2 -2
  7. package/dist/channels/slack/onboard.js +2 -2
  8. package/dist/channels/slack/register-webhook.js +2 -2
  9. package/dist/channels/slack/slack-api.js +4 -4
  10. package/dist/cli/add-feishu.js +4 -2
  11. package/dist/cli/auth-view.d.ts +15 -6
  12. package/dist/cli/auth-view.js +24 -8
  13. package/dist/cli/box-login.d.ts +33 -0
  14. package/dist/cli/box-login.js +162 -0
  15. package/dist/cli/commands/deploy/agentcore.js +48 -11
  16. package/dist/cli/commands/deploy/docker.js +24 -13
  17. package/dist/cli/commands/deploy/fly.js +30 -7
  18. package/dist/cli/commands/deploy/railway.js +21 -7
  19. package/dist/cli/commands/deploy/shared.d.ts +18 -17
  20. package/dist/cli/commands/deploy/shared.js +20 -21
  21. package/dist/cli/commands/destroy.js +2 -2
  22. package/dist/cli/commands/info.js +9 -6
  23. package/dist/cli/commands/invoke.js +2 -2
  24. package/dist/cli/commands/login.d.ts +7 -1
  25. package/dist/cli/commands/login.js +123 -10
  26. package/dist/cli/commands/logs.js +2 -2
  27. package/dist/cli/commands/models.d.ts +4 -1
  28. package/dist/cli/commands/models.js +37 -4
  29. package/dist/cli/commands/routine-run.js +2 -2
  30. package/dist/cli/commands/start.d.ts +0 -5
  31. package/dist/cli/commands/start.js +3 -13
  32. package/dist/cli/kernel.d.ts +2 -0
  33. package/dist/cli/kernel.js +1 -1
  34. package/dist/cli/login-relay.d.ts +38 -0
  35. package/dist/cli/login-relay.js +121 -0
  36. package/dist/cli/program.js +46 -8
  37. package/dist/cli/shared.d.ts +4 -4
  38. package/dist/cli/shared.js +24 -30
  39. package/dist/deploy/agentcore/plan.d.ts +10 -11
  40. package/dist/deploy/agentcore/plan.js +8 -14
  41. package/dist/deploy/agentcore/run.d.ts +8 -1
  42. package/dist/deploy/agentcore/run.js +20 -14
  43. package/dist/deploy/agentcore/shell.d.ts +25 -0
  44. package/dist/deploy/agentcore/shell.js +202 -0
  45. package/dist/deploy/box-shell.d.ts +43 -0
  46. package/dist/deploy/box-shell.js +28 -0
  47. package/dist/deploy/build-context.js +16 -7
  48. package/dist/deploy/channel-ingress.d.ts +20 -1
  49. package/dist/deploy/channel-ingress.js +34 -7
  50. package/dist/deploy/container.d.ts +14 -0
  51. package/dist/deploy/container.js +45 -1
  52. package/dist/deploy/docker/plan.d.ts +7 -3
  53. package/dist/deploy/docker/plan.js +9 -14
  54. package/dist/deploy/docker/run.d.ts +14 -7
  55. package/dist/deploy/docker/run.js +58 -35
  56. package/dist/deploy/fly/plan.d.ts +6 -3
  57. package/dist/deploy/fly/plan.js +6 -8
  58. package/dist/deploy/fly/run.d.ts +8 -2
  59. package/dist/deploy/fly/run.js +15 -25
  60. package/dist/deploy/preflight.d.ts +9 -7
  61. package/dist/deploy/preflight.js +74 -20
  62. package/dist/deploy/railway/plan.d.ts +6 -3
  63. package/dist/deploy/railway/plan.js +7 -9
  64. package/dist/deploy/railway/run.d.ts +10 -6
  65. package/dist/deploy/railway/run.js +29 -13
  66. package/dist/deploy/runner.d.ts +16 -0
  67. package/dist/deploy/runner.js +31 -0
  68. package/dist/deploy/secrets.d.ts +4 -13
  69. package/dist/deploy/secrets.js +8 -27
  70. package/dist/dev-supervisor.js +5 -2
  71. package/dist/engines/pi/agent-models.d.ts +70 -0
  72. package/dist/engines/pi/agent-models.js +93 -0
  73. package/dist/engines/pi/auth.d.ts +56 -3
  74. package/dist/engines/pi/auth.js +63 -16
  75. package/dist/engines/pi/config.d.ts +0 -25
  76. package/dist/engines/pi/config.js +3 -37
  77. package/dist/engines/pi/create.d.ts +22 -16
  78. package/dist/engines/pi/create.js +28 -28
  79. package/dist/engines/pi/entry-images.d.ts +14 -0
  80. package/dist/engines/pi/entry-images.js +23 -0
  81. package/dist/engines/pi/invoke-session.js +26 -0
  82. package/dist/engines/pi/login.d.ts +20 -13
  83. package/dist/engines/pi/login.js +19 -12
  84. package/dist/engines/pi/models.d.ts +115 -36
  85. package/dist/engines/pi/models.js +218 -48
  86. package/dist/engines/pi/open.d.ts +42 -11
  87. package/dist/engines/pi/open.js +53 -21
  88. package/dist/engines/pi/service.d.ts +2 -2
  89. package/dist/engines/pi/service.js +1 -0
  90. package/dist/engines/pi/session-builder.d.ts +3 -2
  91. package/dist/engines/pi/session-builder.js +28 -58
  92. package/dist/engines/pi/session-control.js +14 -3
  93. package/dist/engines/pi/turn-kit.js +11 -5
  94. package/dist/open-url.d.ts +15 -1
  95. package/dist/open-url.js +35 -3
  96. package/dist/paths.d.ts +4 -2
  97. package/dist/paths.js +6 -2
  98. package/dist/pi.d.ts +5 -3
  99. package/dist/pi.js +3 -2
  100. package/dist/service.d.ts +5 -8
  101. package/dist/session-remote.js +21 -0
  102. package/dist/session.d.ts +39 -6
  103. package/package.json +2 -1
@@ -1,9 +1,42 @@
1
- /** `fastagent models [search]`: print every registered "provider/modelId"; `[search]` filters by substring. */
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 { createPiModels } from "../../engines/pi/models.js";
5
- export function runModels(search) {
6
- const { lines, error } = formatModelsCommand(listModels(createPiModels()), search);
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, auth } = await createPiAgentFromDir(placement.agentDir, {
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(placement.agentDir, modelSpec, auth);
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 { writeFileAtomic } from "../../atomic-write.js";
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 { resolveAuthPath } from "../../engines/pi/config.js";
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
- export async function openPreparedWorkspace(prepared, opts) {
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.
@@ -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;
@@ -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
+ }
@@ -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 registered "provider/modelId" spec — use one with --model or as `model` in fastagent.config.ts.',
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
- run: async (args) => (await import("./commands/models.js")).runModels(args[0]),
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). Carries your local credential (env key or OAuth auth.json). " +
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 any provider " +
442
- "its own file does not have, so `-g` logs in once for every agent on this machine.",
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 providers its own file lacks`,
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: [{ cmd: "fastagent login" }, { cmd: "fastagent login openai" }, { cmd: "fastagent login openai -g" }],
452
- notes: "The positional is the PROVIDER (not a dir) — `cd` into your agent before logging in.",
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
  /**
@@ -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 { ResolvedPlacement } from "../paths.ts";
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
- auth: AuthLayers;
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(agentDir: string, modelSpec: string, auth: AuthLayers): Promise<void>;
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;
@@ -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 { fastagentCredentialStore } from "../engines/pi/auth.js";
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 { createPiModelRuntime, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
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.agentDir, a.modelSpec, a.auth);
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(agentDir, modelSpec, auth) {
91
+ export async function reportAuth(models, modelSpec) {
91
92
  const provider = providerOf(modelSpec);
92
- const { path: authPath, fallback: fallbackAuthPath } = auth;
93
- const models = await createPiModelRuntime({ agentDir, auth }).catch(failStartup);
94
- const source = await probeAuthSource(models, modelSpec);
95
- // One refresh-FREE read per layer, serving both questions below. A read failure is already warned about by the
96
- // store itself; this line degrades to "nothing stored" rather than taking down the startup report.
97
- const readFrom = (path) => fastagentCredentialStore(path)
98
- .read(provider)
99
- .catch(() => undefined);
100
- const inPrimary = await readFrom(authPath);
101
- const inFallback = inPrimary || fallbackAuthPath === undefined ? undefined : await readFrom(fallbackAuthPath);
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 auth = resolveAuthLayers(agentDir);
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 createPiModelRuntime({ agentDir, auth }).catch(failStartup);
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
- hint();
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 { type DeploymentSecret } from "../secrets.ts";
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 real OAuth auth.json's base64 exceeds it, so each carrier is CHUNKED across
73
- * `<NAME>` + `_2`… (collectChunked reassembles at boot): FASTAGENT_AUTH_SEED for the credential, FASTAGENT_ENV for the
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 two chunked carriers: CloudFormation parameter prefix → container env-var name. */
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";