@fastagent-sh/fastagent 0.22.1 → 0.24.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 (128) hide show
  1. package/dist/agent.d.ts +10 -0
  2. package/dist/channels/agentcore-protocol.d.ts +2 -1
  3. package/dist/channels/agentcore-service.js +10 -18
  4. package/dist/channels/agentcore.js +6 -3
  5. package/dist/channels/control.js +23 -0
  6. package/dist/channels/kit/preview-kit.d.ts +6 -1
  7. package/dist/channels/kit/preview-kit.js +21 -4
  8. package/dist/channels/slack/config-api.d.ts +2 -2
  9. package/dist/channels/slack/config-api.js +2 -2
  10. package/dist/channels/slack/onboard.js +2 -2
  11. package/dist/channels/slack/preview.js +2 -1
  12. package/dist/channels/slack/register-webhook.js +2 -2
  13. package/dist/channels/slack/slack-api.js +4 -4
  14. package/dist/cli/add-feishu.js +4 -2
  15. package/dist/cli/auth-view.d.ts +15 -6
  16. package/dist/cli/auth-view.js +24 -8
  17. package/dist/cli/box-login.d.ts +33 -0
  18. package/dist/cli/box-login.js +162 -0
  19. package/dist/cli/commands/deploy/agentcore.js +48 -11
  20. package/dist/cli/commands/deploy/docker.js +24 -13
  21. package/dist/cli/commands/deploy/fly.js +30 -7
  22. package/dist/cli/commands/deploy/railway.js +21 -7
  23. package/dist/cli/commands/deploy/shared.d.ts +18 -17
  24. package/dist/cli/commands/deploy/shared.js +20 -21
  25. package/dist/cli/commands/destroy.js +2 -2
  26. package/dist/cli/commands/info.js +15 -12
  27. package/dist/cli/commands/invoke.js +2 -2
  28. package/dist/cli/commands/login.d.ts +7 -1
  29. package/dist/cli/commands/login.js +123 -10
  30. package/dist/cli/commands/logs.js +2 -2
  31. package/dist/cli/commands/models.d.ts +4 -1
  32. package/dist/cli/commands/models.js +37 -4
  33. package/dist/cli/commands/routine-run.js +2 -2
  34. package/dist/cli/commands/start.d.ts +0 -5
  35. package/dist/cli/commands/start.js +3 -13
  36. package/dist/cli/invoke-stream.js +14 -0
  37. package/dist/cli/kernel.d.ts +2 -0
  38. package/dist/cli/kernel.js +1 -1
  39. package/dist/cli/login-relay.d.ts +38 -0
  40. package/dist/cli/login-relay.js +121 -0
  41. package/dist/cli/program.js +46 -8
  42. package/dist/cli/shared.d.ts +6 -5
  43. package/dist/cli/shared.js +29 -36
  44. package/dist/deploy/agentcore/plan.d.ts +10 -11
  45. package/dist/deploy/agentcore/plan.js +8 -14
  46. package/dist/deploy/agentcore/run.d.ts +8 -1
  47. package/dist/deploy/agentcore/run.js +20 -14
  48. package/dist/deploy/agentcore/shell.d.ts +25 -0
  49. package/dist/deploy/agentcore/shell.js +202 -0
  50. package/dist/deploy/box-shell.d.ts +43 -0
  51. package/dist/deploy/box-shell.js +28 -0
  52. package/dist/deploy/build-context.js +16 -7
  53. package/dist/deploy/channel-ingress.d.ts +20 -1
  54. package/dist/deploy/channel-ingress.js +34 -7
  55. package/dist/deploy/container.d.ts +14 -0
  56. package/dist/deploy/container.js +45 -1
  57. package/dist/deploy/docker/plan.d.ts +7 -3
  58. package/dist/deploy/docker/plan.js +9 -14
  59. package/dist/deploy/docker/run.d.ts +14 -7
  60. package/dist/deploy/docker/run.js +58 -35
  61. package/dist/deploy/fly/plan.d.ts +6 -3
  62. package/dist/deploy/fly/plan.js +6 -8
  63. package/dist/deploy/fly/run.d.ts +8 -2
  64. package/dist/deploy/fly/run.js +15 -25
  65. package/dist/deploy/preflight.d.ts +9 -7
  66. package/dist/deploy/preflight.js +92 -20
  67. package/dist/deploy/railway/plan.d.ts +6 -3
  68. package/dist/deploy/railway/plan.js +7 -9
  69. package/dist/deploy/railway/run.d.ts +10 -6
  70. package/dist/deploy/railway/run.js +29 -13
  71. package/dist/deploy/runner.d.ts +16 -0
  72. package/dist/deploy/runner.js +31 -0
  73. package/dist/deploy/secrets.d.ts +4 -13
  74. package/dist/deploy/secrets.js +8 -27
  75. package/dist/dev-supervisor.js +7 -4
  76. package/dist/engines/pi/agent-models.d.ts +83 -0
  77. package/dist/engines/pi/agent-models.js +120 -0
  78. package/dist/engines/pi/agent-session-factory.d.ts +13 -13
  79. package/dist/engines/pi/agent-session-factory.js +132 -159
  80. package/dist/engines/pi/auth.d.ts +56 -3
  81. package/dist/engines/pi/auth.js +68 -109
  82. package/dist/engines/pi/config.d.ts +0 -25
  83. package/dist/engines/pi/config.js +3 -37
  84. package/dist/engines/pi/create.d.ts +34 -21
  85. package/dist/engines/pi/create.js +90 -57
  86. package/dist/engines/pi/entry-images.d.ts +14 -0
  87. package/dist/engines/pi/entry-images.js +23 -0
  88. package/dist/engines/pi/invoke-session.js +88 -20
  89. package/dist/engines/pi/locked-file.d.ts +9 -0
  90. package/dist/engines/pi/locked-file.js +102 -0
  91. package/dist/engines/pi/login.d.ts +27 -14
  92. package/dist/engines/pi/login.js +41 -18
  93. package/dist/engines/pi/machine.d.ts +23 -0
  94. package/dist/engines/pi/machine.js +24 -2
  95. package/dist/engines/pi/models.d.ts +120 -36
  96. package/dist/engines/pi/models.js +316 -66
  97. package/dist/engines/pi/open.d.ts +65 -20
  98. package/dist/engines/pi/open.js +88 -34
  99. package/dist/engines/pi/report.d.ts +3 -0
  100. package/dist/engines/pi/report.js +11 -0
  101. package/dist/engines/pi/service.d.ts +2 -2
  102. package/dist/engines/pi/service.js +1 -0
  103. package/dist/engines/pi/session-builder.d.ts +3 -2
  104. package/dist/engines/pi/session-builder.js +30 -76
  105. package/dist/engines/pi/session-control.d.ts +4 -4
  106. package/dist/engines/pi/session-control.js +86 -43
  107. package/dist/engines/pi/session-settings.d.ts +9 -0
  108. package/dist/engines/pi/session-settings.js +17 -0
  109. package/dist/engines/pi/session-store.d.ts +2 -2
  110. package/dist/engines/pi/tool-context.d.ts +3 -5
  111. package/dist/engines/pi/tool-context.js +4 -16
  112. package/dist/engines/pi/tool.d.ts +32 -21
  113. package/dist/engines/pi/tool.js +21 -25
  114. package/dist/engines/pi/turn-kit.d.ts +18 -9
  115. package/dist/engines/pi/turn-kit.js +115 -32
  116. package/dist/open-url.d.ts +15 -1
  117. package/dist/open-url.js +35 -3
  118. package/dist/paths.d.ts +4 -2
  119. package/dist/paths.js +6 -2
  120. package/dist/pi.d.ts +5 -3
  121. package/dist/pi.js +3 -2
  122. package/dist/service.d.ts +5 -8
  123. package/dist/session-remote.js +21 -0
  124. package/dist/session.d.ts +88 -15
  125. package/dist/session.js +5 -1
  126. package/package.json +6 -5
  127. package/dist/engines/pi/search-tools.d.ts +0 -4
  128. package/dist/engines/pi/search-tools.js +0 -109
@@ -1,17 +1,27 @@
1
1
  /**
2
2
  * `fastagent login [provider]`: authenticate a model provider into the project-level auth file
3
- * (`<agentDir>/.secrets/auth.json`) by default, or `FASTAGENT_AUTH_PATH`.
3
+ * (`<agentDir>/.secrets/auth.json`) by default, or `FASTAGENT_AUTH_PATH`. `--deployment` runs the same login on this
4
+ * workspace's deployed box instead (box-login.ts); `--stdio` is the box's half of that.
4
5
  */
5
- import { homedir } from "node:os";
6
+ import { existsSync } from "node:fs";
6
7
  import { join } from "node:path";
7
8
  import { enterAgentEnv } from "../../env.js";
8
- import { resolveAuthPath } from "../../engines/pi/config.js";
9
- import { GLOBAL_AUTH_PATH } from "../../engines/pi/auth.js";
10
- import { GLOBAL_HOME_DIR, findAgentDir, placementDeadEnd } from "../../paths.js";
9
+ import { GLOBAL_AUTH_PATH, resolveAuthPath } from "../../engines/pi/auth.js";
10
+ import { agentModels } from "../../engines/pi/agent-models.js";
11
+ import { DEPLOY_HOSTS } from "../../deploy/hosts.js";
12
+ import { findAgentDir, globalHome, placementDeadEnd, resolvePlacement } from "../../paths.js";
11
13
  import { LoginCancelled, loginFlow } from "../../engines/pi/login.js";
12
- import { failStartup, placementOrExit } from "../fail.js";
14
+ import { environmentAuthSource } from "../../engines/pi/models.js";
15
+ import { loginOnBox } from "../box-login.js";
16
+ import { failStartup, failUsage, placementOrExit } from "../fail.js";
17
+ import { stdioLoginIO } from "../login-relay.js";
13
18
  import { isInteractive, terminalLoginIO } from "../shared.js";
19
+ import { HOSTS } from "./deploy.js";
14
20
  export async function runLogin(provider, opts) {
21
+ if (opts.stdio)
22
+ return runStdioLogin(provider, opts);
23
+ if (opts.deployment !== undefined)
24
+ return runDeploymentLogin(provider, opts);
15
25
  const cwd = process.cwd();
16
26
  const agentDir = findAgentDir(cwd);
17
27
  // "Outside an agent" must mean exactly that.
@@ -19,9 +29,12 @@ export async function runLogin(provider, opts) {
19
29
  placementOrExit(cwd);
20
30
  // Outside any agent the target is the user-global machinery home — handed over explicitly, so the path resolvers
21
31
  // need no "is this $HOME?" special case to infer it.
22
- const loginDir = agentDir ?? join(homedir(), GLOBAL_HOME_DIR);
32
+ const loginDir = agentDir ?? globalHome();
23
33
  // FASTAGENT_AUTH_PATH and a proxy may both be configured in the project .env, and the OAuth token exchange must go
24
34
  // through that proxy (region-locked providers).
35
+ // Before the agent's `.env` joins it: the shadowing check below is about what EVERY agent sees, and that is the
36
+ // environment the shell hands down, not one agent's value file.
37
+ const shellEnv = { ...process.env };
25
38
  enterAgentEnv(loginDir);
26
39
  // `-g` names the global file outright. Otherwise: FASTAGENT_AUTH_PATH > default — the one owner. The
27
40
  // store built here is deliberately UNLAYERED: reading falls back to the global file, but writing must land
@@ -32,9 +45,9 @@ export async function runLogin(provider, opts) {
32
45
  // lands somewhere no agent will read.
33
46
  if (!agentDir && !opts.global && !process.env.FASTAGENT_AUTH_PATH) {
34
47
  console.error(`[fastagent] no agent here (no fastagent.config.ts, here or one level inside) — ` +
35
- `logging in GLOBALLY (${authPath}). Every agent on this machine reads this file for providers its own ` +
36
- `.secrets/auth.json does not have, so this is usually what you want; \`cd\` into an agent to give that ` +
37
- `one its own account instead.`);
48
+ `logging in GLOBALLY (${authPath}). An agent on this machine uses this file for a provider it has no ` +
49
+ `credential of its own for (no entry in its .secrets/auth.json, no apiKey in its models.json, no env ` +
50
+ `variable), so this is usually what you want; \`cd\` into an agent to give that one its own account instead.`);
38
51
  }
39
52
  // login is inherently interactive — loginFlow renders provider/method menus and opens a browser (or prompts for a
40
53
  // key).
@@ -57,5 +70,105 @@ export async function runLogin(provider, opts) {
57
70
  failStartup(error);
58
71
  });
59
72
  console.error(`[fastagent] logged in to ${result.provider} (${result.method}) — saved to ${authPath}`);
73
+ // The environment outranks the global file for every agent, so a global login it shadows would sit unused while
74
+ // agents run on, say, an API key billed per request, with only a startup line to show it.
75
+ const shadowedBy = authPath === GLOBAL_AUTH_PATH ? await environmentAuthSource(result.provider, shellEnv) : undefined;
76
+ if (shadowedBy !== undefined) {
77
+ console.error(`[fastagent] warning: ${result.provider} is also authenticated by ${shadowedBy} in your shell environment, which ` +
78
+ `agents use before the global file, so this login is not used while it is set. Unset it to use this login.`);
79
+ }
60
80
  process.exit(0); // the undici proxy agent's keep-alive sockets would otherwise hold the event loop open
61
81
  }
82
+ /**
83
+ * `--deployment [host]`: the host is named, or the one whose generated artifact is in the agent dir. Several is a
84
+ * question only the operator can answer, so it is asked back rather than guessed.
85
+ */
86
+ async function runDeploymentLogin(provider, opts) {
87
+ if (opts.global)
88
+ failUsage("--deployment logs the deployment in; -g names this machine's global file — pick one");
89
+ const placement = placementOrExit(process.cwd());
90
+ // The host's CLI must reach the account/region/proxy the deploy used, and those may be definition-local.
91
+ enterAgentEnv(placement.agentDir);
92
+ let host;
93
+ if (typeof opts.deployment === "string") {
94
+ if (!DEPLOY_HOSTS.includes(opts.deployment)) {
95
+ failUsage(`--deployment takes a host (${DEPLOY_HOSTS.join(", ")}), not "${opts.deployment}" — to name a provider too: ` +
96
+ `fastagent login <provider> --deployment <host>`);
97
+ }
98
+ host = opts.deployment;
99
+ }
100
+ else {
101
+ const found = DEPLOY_HOSTS.filter((h) => existsSync(join(placement.agentDir, HOSTS[h].artifact)));
102
+ if (found.length !== 1) {
103
+ failUsage(found.length === 0
104
+ ? `no deployment artifacts in ${placement.agentDir} — name the host: --deployment <${DEPLOY_HOSTS.join("|")}>`
105
+ : `this agent deploys to ${found.join(" and ")} — name one: --deployment <${found.join("|")}>`);
106
+ }
107
+ host = found[0];
108
+ }
109
+ const input = opts.input !== false && isInteractive();
110
+ if (!input) {
111
+ failStartup(new Error(`login is interactive (it shows a menu and opens a browser) — run it in a terminal`));
112
+ }
113
+ const failed = await loginOnBox({
114
+ host,
115
+ shell: await HOSTS[host].shell(placement).catch(failStartup),
116
+ placement,
117
+ ...(provider ? { provider } : {}),
118
+ input,
119
+ });
120
+ if (failed)
121
+ failStartup(new Error(failed));
122
+ process.exit(0);
123
+ }
124
+ /**
125
+ * `--stdio`: the box's half of `--deployment`. Runs where the server runs; everything it would show or ask travels as
126
+ * the relay wire, and it ends with exactly one result line. Never started by hand: the credential path is the one the
127
+ * caller exported (container.ts `boxLoginCommand`).
128
+ */
129
+ async function runStdioLogin(provider, opts) {
130
+ const wire = stdioLoginIO(process.stdin, process.stdout);
131
+ const result = await stdioLogin(wire.io, provider, opts).catch((error) => error instanceof LoginCancelled
132
+ ? { ok: false, reason: "cancelled", message: "cancelled" }
133
+ : { ok: false, reason: "failed", message: error.message });
134
+ wire.result(result);
135
+ process.exitCode = result.ok ? 0 : 1;
136
+ // Exit once the result line is flushed: an undici keep-alive socket would otherwise hold the process open.
137
+ process.stdout.write("", () => process.exit());
138
+ }
139
+ async function stdioLogin(io, provider, opts) {
140
+ // `boxLoginCommand` starts this in the deployed agent directory; anywhere else there is no box to log in.
141
+ const agentDir = findAgentDir(process.cwd());
142
+ if (!agentDir)
143
+ throw new Error(`login --stdio: ${process.cwd()} is not an agent directory`);
144
+ // A deployed box has no value file to read: its variables reach this process only as the host's shell hands them
145
+ // down (the container's for `docker compose exec`, the platform's over fly/railway ssh), and this installs the egress
146
+ // proxy they name. AgentCore's command shell hands down none of the runtime's, so a proxy carried there in
147
+ // FASTAGENT_ENV does not reach the login (docs/deploy.md).
148
+ enterAgentEnv(agentDir);
149
+ // The model environment the serving runtime reads (createPiAgentFromDir), so "logged in" means what the server will
150
+ // use.
151
+ const models = agentModels(agentDir, {}, { cwd: resolvePlacement(agentDir).workspace });
152
+ const { auth } = models;
153
+ if (opts.ifMissing && provider) {
154
+ // The server's own answer (`authStatus`), the one its startup report prints. Known ceiling: nothing is asked of
155
+ // the provider beyond a due refresh, so a revoked grant whose access token has not expired yet, or a revoked API
156
+ // key, reads as held and the first turn fails with the provider's error. Asking would spend a real model call on
157
+ // every redeploy, which the readiness checks deliberately never do.
158
+ const held = await models.authStatus(provider);
159
+ if (held.source !== undefined)
160
+ return { ok: true, provider, kept: held.source };
161
+ if (opts.input === false) {
162
+ const what = held.stored
163
+ ? `the stored ${provider} ${held.stored} credential is expired or unusable`
164
+ : `no ${provider} credential`;
165
+ const because = held.error === undefined ? "" : ` (${held.error})`;
166
+ return { ok: false, reason: "missing", message: `${what}${because} in ${held.path ?? auth.path}` };
167
+ }
168
+ }
169
+ if (opts.input === false) {
170
+ return { ok: false, reason: "missing", message: `no ${provider ?? "model"} credential in ${auth.path}` };
171
+ }
172
+ const result = await loginFlow(io, { authPath: auth.path, ...(provider ? { provider } : {}), agentDir });
173
+ return { ok: true, provider: result.provider, method: result.method, path: auth.path };
174
+ }
@@ -2,7 +2,7 @@
2
2
  import { basename, resolve } from "node:path";
3
3
  import { agentcoreName } from "../../deploy/agentcore/plan.js";
4
4
  import { tailAgentcoreLogs } from "../../deploy/agentcore/logs.js";
5
- import { spawnRunner } from "../../deploy/runner.js";
5
+ import { awsRunner } from "../../deploy/runner.js";
6
6
  import { enterAgentEnv } from "../../env.js";
7
7
  import { failStartup, failUsage, placementOrExit } from "../fail.js";
8
8
  export async function runLogs(host, dirArg, opts) {
@@ -20,7 +20,7 @@ export async function runLogs(host, dirArg, opts) {
20
20
  source: source,
21
21
  since: opts.since,
22
22
  follow: opts.follow === true,
23
- }, spawnRunner("aws", placement.workspace), (message) => console.error(`[fastagent] logs: ${message}`));
23
+ }, awsRunner(placement.workspace), (message) => console.error(`[fastagent] logs: ${message}`));
24
24
  if (!outcome.ok)
25
25
  failStartup(new Error(`logs stopped: ${outcome.gate}`));
26
26
  }
@@ -1 +1,4 @@
1
- export declare function runModels(search: string | undefined): void;
1
+ export declare function runModels(search: string | undefined, opts?: {
2
+ refresh?: boolean;
3
+ global?: boolean;
4
+ }): Promise<void>;
@@ -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 } from "../../engines/pi/models.js";
11
+ import { refreshMachineModelCatalog, 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 refreshMachineModelCatalog().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.
@@ -10,6 +10,9 @@ export async function runInvokeStream(events, out, err) {
10
10
  toolName.set(event.id, event.name);
11
11
  err(`[tool] ${event.name}`);
12
12
  break;
13
+ case "tool_progress":
14
+ err(`[tool] ${toolName.get(event.id) ?? event.id} › ${clipStatus(event.text)}`);
15
+ break;
13
16
  case "tool_ended":
14
17
  if (event.isError)
15
18
  err(`[tool] ${toolName.get(event.id) ?? event.id} failed`);
@@ -28,3 +31,14 @@ export async function runInvokeStream(events, out, err) {
28
31
  }
29
32
  return exitCode;
30
33
  }
34
+ /** How much of a status line a terminal gets, in code points. */
35
+ const STATUS_MAX = 120;
36
+ /**
37
+ * A `tool_progress` line clipped to {@link STATUS_MAX} code points. The event is untruncated by contract, and a shell
38
+ * printing one long line (`curl -s` of a JSON body) re-sends that growing line every 100 ms.
39
+ */
40
+ function clipStatus(text) {
41
+ // Only the first STATUS_MAX + 1 code points decide the result, and they fit in twice as many UTF-16 units.
42
+ const head = [...text.slice(0, (STATUS_MAX + 1) * 2)];
43
+ return head.length > STATUS_MAX ? `${head.slice(0, STATUS_MAX).join("")}…` : head.join("");
44
+ }
@@ -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,9 @@
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
+ import { type IndirectTool } from "../engines/pi/create.ts";
3
4
  import type { LoadedDefinition } from "../engines/pi/definition.ts";
4
5
  import type { ToolCollision } from "../engines/pi/tool.ts";
5
- import type { ResolvedPlacement } from "../paths.ts";
6
+ import { type ResolvedPlacement } from "../paths.ts";
6
7
  /** How every command that runs the model enters its agent directory, in the one order that works. */
7
8
  export declare function enterAgentCommand(dirArg: string, opts: {
8
9
  model?: string;
@@ -13,13 +14,13 @@ export interface ReportableAssembly {
13
14
  agentDir: string;
14
15
  workspace: string;
15
16
  modelSpec: string;
16
- auth: AuthLayers;
17
+ models: AgentModels;
17
18
  config: {
18
19
  thinkingLevel?: string;
19
20
  };
20
21
  definition: LoadedDefinition;
21
22
  toolNames: string[];
22
- deferredToolNames: string[];
23
+ indirectTools: IndirectTool[];
23
24
  toolCollisions: ToolCollision[];
24
25
  }
25
26
  /** What `dev` and `start` say about the directory they just opened, in the order they say it. */
@@ -39,6 +40,6 @@ export declare function parsePort(value: string | undefined, source: string, fro
39
40
  */
40
41
  export declare function parseBind(value: string | undefined): string | undefined;
41
42
  /** 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>;
43
+ export declare function reportAuth(models: AgentModels, modelSpec: string): Promise<void>;
43
44
  /** Login terminal IO via @clack/prompts: a searchable list once long, a hidden prompt for keys. */
44
45
  export declare function terminalLoginIO(): LoginIO;