@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
@@ -5,22 +5,47 @@
5
5
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { basename, join } from "node:path";
8
- import { FORWARDER_FILE, TEMPLATE_FILE, agentcoreName, forwarderLogGroup, ingressSessionId, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../../deploy/agentcore/plan.js";
9
- import { deployAgentcoreRun } from "../../../deploy/agentcore/run.js";
10
- import { spawnRunner } from "../../../deploy/runner.js";
8
+ import { FORWARDER_FILE, TEMPLATE_FILE, agentcoreName, agentcoreStackName, forwarderLogGroup, ingressSessionId, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../../deploy/agentcore/plan.js";
9
+ import { deployAgentcoreRun, pickStackOutputs } from "../../../deploy/agentcore/run.js";
10
+ import { awsCli, awsJson } from "../../../deploy/agentcore/aws-cli.js";
11
+ import { agentcoreShell } from "../../../deploy/agentcore/shell.js";
12
+ import { awsRunner, spawnRunner } from "../../../deploy/runner.js";
11
13
  import { SECRET_FILE_MODE, exists } from "../../../paths.js";
12
14
  import { loadRoutines } from "../../../schedule/discover.js";
15
+ import { assembleSecrets } from "../../../deploy/secrets.js";
13
16
  import { failStartup } from "../../fail.js";
14
- import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
17
+ import { isInteractive } from "../../shared.js";
18
+ import { boxLoginStep, registrarsFor } from "./shared.js";
15
19
  /** A copy/paste-safe POSIX shell argument for the command hints deploy prints. */
16
20
  function shellArg(value) {
17
21
  return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
18
22
  }
19
23
  export const agentcoreHost = {
20
24
  isOurs: (path, content) => path.endsWith(TEMPLATE_FILE) && isGeneratedAgentcoreTemplate(content),
25
+ artifact: TEMPLATE_FILE,
26
+ async shell({ workspace }) {
27
+ const name = agentcoreName(basename(workspace));
28
+ const stack = agentcoreStackName(name);
29
+ const aws = awsRunner(workspace);
30
+ const read = await awsCli(aws).read(["cloudformation", "describe-stacks", "--stack-name", stack, "--query", "Stacks[0].Outputs", "--output", "json"], awsJson(pickStackOutputs));
31
+ if ("absent" in read)
32
+ throw new Error(`no AgentCore stack ${stack} in this account/region — deploy it first`);
33
+ if ("unreadable" in read)
34
+ throw new Error(`could not read AgentCore stack ${stack}: ${read.unreadable}`);
35
+ const runtimeArn = read.ok.RuntimeArn;
36
+ if (!runtimeArn)
37
+ throw new Error(`stack ${stack} has no RuntimeArn output — redeploy it`);
38
+ return agentcoreShell(runtimeArn, ingressSessionId(name), aws);
39
+ },
21
40
  async deploy(ctx) {
22
41
  const { opts, agentDir, workspace, config, channels, longConnectionChannels, pre, write } = ctx;
23
- const { modelAuth, modelKeyInDefinition, authPath, container, declaredSecrets, values, valueFile } = pre;
42
+ const { modelAuth, boxLogin, container, declaredSecrets, values, valueFile } = pre;
43
+ if (boxLogin) {
44
+ console.error(`[fastagent] note: AgentCore resets the runtime's storage on every deploy AND after 14 idle days, and the ` +
45
+ `${boxLogin} login with it: every deploy of this agent ends with a login on the runtime, and after an idle ` +
46
+ `reset every turn fails until \`fastagent login ${boxLogin} --deployment agentcore\` (the runtime's log says ` +
47
+ `so). For an agent that must keep answering unattended, set ${boxLogin}'s API key in ${valueFile}.`);
48
+ }
24
49
  // Long-connection channels are STRUCTURALLY unsupported: the connection is the ingress, and a reclaimed session
25
50
  // has nothing to wake it.
26
51
  if (longConnectionChannels.length > 0) {
@@ -54,7 +79,7 @@ export const agentcoreHost = {
54
79
  }
55
80
  const plan = planAgentcoreDeploy({
56
81
  name: acName,
57
- modelAuth,
82
+ boxLogin,
58
83
  channels,
59
84
  secrets: pre.secrets,
60
85
  // ONLY THE ONES WITH A CRON become rules. A routine without one is reached by NAME, through this host's
@@ -98,8 +123,8 @@ export const agentcoreHost = {
98
123
  agentPrefix: container.agentPrefix,
99
124
  name: acName,
100
125
  modelAuth,
101
- modelKeyInDefinition,
102
- authPath,
126
+ boxLogin,
127
+ input: opts.input !== false && isInteractive(),
103
128
  channels,
104
129
  declaredSecrets,
105
130
  values,
@@ -114,12 +139,23 @@ export const agentcoreHost = {
114
139
  /** `deploy agentcore --run`: drive aws + docker to completion. */
115
140
  async function runDeployAgentcore(params) {
116
141
  const { agentDir, workspace, agentPrefix, name, channels, topology } = params;
117
- const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
142
+ // Decided before the first side effect: the deploy resets the runtime's storage and the login with it, so with nobody
143
+ // to log it in again it would end at "not logged in" AFTER replacing a runtime that was serving, with the webhooks
144
+ // a previous deploy registered still pointing at it.
145
+ if (params.boxLogin && !params.input) {
146
+ failStartup(new Error(`deploy stopped: every AgentCore deploy wipes the runtime's ${params.boxLogin} login, and without a terminal ` +
147
+ `nobody can log it in again — run this deploy in a terminal, or set ${params.boxLogin}'s API key in ` +
148
+ `${params.valueFile}`));
149
+ }
150
+ const { secrets, missingSecrets } = assembleSecrets({
151
+ modelAuth: params.modelAuth,
152
+ declared: params.declaredSecrets,
153
+ values: params.values,
154
+ });
118
155
  // The wake-alarm shared secret (container ↔ forwarder).
119
156
  secrets.FASTAGENT_WAKE_SECRET = crypto.randomUUID();
120
157
  // The forwarder→runtime ingress secret: what makes an envelope the forwarder's rather than any IAM principal's.
121
158
  secrets.FASTAGENT_INGRESS_SECRET = crypto.randomUUID();
122
- gateOnModelCredential(needsModelCredential);
123
159
  // The params temp dir holds the ONE file carrying secret values (file:// parameter-overrides — never argv);
124
160
  // 0700/0600 and removed after the run, success or gate.
125
161
  const paramsDir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-"));
@@ -138,7 +174,8 @@ async function runDeployAgentcore(params) {
138
174
  valueFile: params.valueFile,
139
175
  channels,
140
176
  topology,
141
- }, spawnRunner("aws", workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
177
+ ...boxLoginStep("agentcore", params, (runtimeArn) => agentcoreShell(runtimeArn, ingressSessionId(name), awsRunner(workspace))),
178
+ }, awsRunner(workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
142
179
  const path = join(paramsDir, "params.json");
143
180
  await writeFile(path, content, { mode: SECRET_FILE_MODE });
144
181
  return path;
@@ -2,20 +2,25 @@
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { webhookPaths } from "../../../deploy/channel-ingress.js";
5
- import { composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
5
+ import { DOCKER_COMPOSE_FILE, composeHasTunnelService, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../../deploy/docker/plan.js";
6
6
  import { deployDockerRun } from "../../../deploy/docker/run.js";
7
7
  import { spawnRunner } from "../../../deploy/runner.js";
8
8
  import { openExternalUrl } from "../../../open-url.js";
9
9
  import { SECRETS_DIRNAME, SECRET_FILE_MODE, exists, readTextIfExists, resolveStateRoot, } from "../../../paths.js";
10
10
  import { dotEnvPath } from "../../../env.js";
11
11
  import { announceWebhooks } from "../../../tunnel.js";
12
+ import { assembleSecrets } from "../../../deploy/secrets.js";
13
+ import { processShell } from "../../../deploy/box-shell.js";
12
14
  import { failStartup } from "../../fail.js";
13
- import { carryCredentials } from "./shared.js";
15
+ import { isInteractive } from "../../shared.js";
16
+ import { boxLoginStep } from "./shared.js";
14
17
  export const dockerHost = {
15
- isOurs: (path, content) => path.endsWith("fastagent.compose.yml") && isGeneratedCompose(content),
18
+ isOurs: (path, content) => path.endsWith(DOCKER_COMPOSE_FILE) && isGeneratedCompose(content),
19
+ artifact: DOCKER_COMPOSE_FILE,
20
+ shell: async ({ agentDir, workspace }) => composeShell(`${basename(agentDir)}/${DOCKER_COMPOSE_FILE}`, workspace),
16
21
  async deploy(ctx) {
17
22
  const { opts, agentDir, workspace, channels, webhookChannels, pre, write } = ctx;
18
- const { modelAuth, modelKeyInDefinition, authPath, container, port, declaredSecrets, values, valueFile } = pre;
23
+ const { modelAuth, boxLogin, container, port, declaredSecrets, values, valueFile } = pre;
19
24
  // The generated Compose names `<agent>/.secrets/.env` unconditionally, so it has to be there — Compose refuses
20
25
  // an `env_file` entry pointing at a missing path, and this floor predates `required: false` (Compose 2.24).
21
26
  // Creating it empty is honest: a deployment that declares nothing declares it in an empty file.
@@ -51,7 +56,7 @@ export const dockerHost = {
51
56
  const dockerPlan = (tunnel) => planDockerDeploy({
52
57
  projectName,
53
58
  port,
54
- modelAuth,
59
+ boxLogin,
55
60
  channels,
56
61
  tunnel,
57
62
  secrets: pre.secrets,
@@ -84,8 +89,8 @@ export const dockerHost = {
84
89
  port,
85
90
  requireTunnel: requestedTunnel,
86
91
  modelAuth,
87
- modelKeyInDefinition,
88
- authPath,
92
+ boxLogin,
93
+ input: opts.input !== false && isInteractive(),
89
94
  channels,
90
95
  declaredSecrets,
91
96
  values,
@@ -100,21 +105,23 @@ export const dockerHost = {
100
105
  return;
101
106
  },
102
107
  };
103
- /**
104
- * `deploy docker --run`: carry local credentials into Compose's child environment, then reconcile the user-owned local
105
- * topology.
106
- */
108
+ /** `deploy docker --run`: reconcile the user-owned local topology, then log the box in if it must. */
107
109
  async function runDeployDocker(params) {
108
110
  const { agentDir, workspace, composeFile, port, requireTunnel, channels } = params;
109
- const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
111
+ const { secrets, missingSecrets } = assembleSecrets({
112
+ modelAuth: params.modelAuth,
113
+ declared: params.declaredSecrets,
114
+ values: params.values,
115
+ });
110
116
  const outcome = await deployDockerRun({
111
117
  composeFile,
112
118
  port,
113
119
  secrets,
114
120
  missingSecrets,
115
121
  valueFile: params.valueFile,
116
- needsModelCredential,
117
122
  requireTunnel,
123
+ channels,
124
+ ...boxLoginStep("docker", params, () => composeShell(composeFile, workspace)),
118
125
  announce: (tunnelUrl) => announceWebhooks(agentDir, tunnelUrl, channels, {
119
126
  openUrl: openExternalUrl,
120
127
  stateRoot: resolveStateRoot(agentDir),
@@ -144,3 +151,7 @@ async function runDeployDocker(params) {
144
151
  `default webhook path(s): ${paths.join(", ")} (or your remapped channel routes)`);
145
152
  }
146
153
  }
154
+ /** `docker compose exec` into the running agent service. */
155
+ function composeShell(composeFile, workspace) {
156
+ return processShell("docker", (command) => ["compose", "-f", composeFile, "exec", "-T", "agent", "sh", "-c", command], workspace);
157
+ }
@@ -5,13 +5,21 @@ import { deployFlyRun } from "../../../deploy/fly/run.js";
5
5
  import { spawnRunner } from "../../../deploy/runner.js";
6
6
  import { readTextIfExists } from "../../../paths.js";
7
7
  import { residencyFor } from "../../../deploy/residency.js";
8
+ import { assembleSecrets } from "../../../deploy/secrets.js";
9
+ import { processShell } from "../../../deploy/box-shell.js";
8
10
  import { failStartup } from "../../fail.js";
9
- import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
11
+ import { isInteractive } from "../../shared.js";
12
+ import { boxLoginStep, registrarsFor } from "./shared.js";
10
13
  export const flyHost = {
11
14
  isOurs: (path, content) => path.endsWith("fly.toml") && isGeneratedFlyToml(content),
15
+ artifact: "fly.toml",
16
+ async shell({ agentDir, workspace }) {
17
+ const flyToml = await readTextIfExists(join(agentDir, "fly.toml"));
18
+ return flyShell((flyToml && parseFlyAppName(flyToml)) ?? toFlyAppName(basename(workspace)), workspace);
19
+ },
12
20
  async deploy(ctx) {
13
21
  const { opts, agentDir, workspace, channels, pre, write } = ctx;
14
- const { hasCron, modelAuth, modelKeyInDefinition, authPath, container, port, declaredSecrets, values, valueFile } = pre;
22
+ const { hasCron, modelAuth, boxLogin, container, port, declaredSecrets, values, valueFile } = pre;
15
23
  // Two consistent modes.
16
24
  const flyTomlPath = join(agentDir, "fly.toml");
17
25
  const flyToml = await readTextIfExists(flyTomlPath).catch(failStartup);
@@ -43,7 +51,7 @@ export const flyHost = {
43
51
  const plan = planFlyDeploy({
44
52
  appName,
45
53
  port,
46
- modelAuth,
54
+ boxLogin,
47
55
  channels,
48
56
  secrets: pre.secrets,
49
57
  hasCron,
@@ -57,8 +65,8 @@ export const flyHost = {
57
65
  agentPrefix: container.agentPrefix,
58
66
  appName,
59
67
  modelAuth,
60
- modelKeyInDefinition,
61
- authPath,
68
+ boxLogin,
69
+ input: opts.input !== false && isInteractive(),
62
70
  channels,
63
71
  declaredSecrets,
64
72
  values,
@@ -76,8 +84,11 @@ async function runDeployFly(params) {
76
84
  if ((await fly(["version"], { capture: true })).code === 127) {
77
85
  failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
78
86
  }
79
- const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
80
- gateOnModelCredential(needsModelCredential);
87
+ const { secrets, missingSecrets } = assembleSecrets({
88
+ modelAuth: params.modelAuth,
89
+ declared: params.declaredSecrets,
90
+ values: params.values,
91
+ });
81
92
  const outcome = await deployFlyRun({
82
93
  appName,
83
94
  secrets,
@@ -86,8 +97,20 @@ async function runDeployFly(params) {
86
97
  channels,
87
98
  flyConfig: `${agentPrefix}fly.toml`,
88
99
  dockerfile: `${agentPrefix}Dockerfile`,
100
+ ...boxLoginStep("fly", params, () => flyShell(appName, workspace)),
89
101
  }, fly, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
90
102
  if (!outcome.ok)
91
103
  failStartup(new Error(`deploy stopped: ${outcome.gate}`));
92
104
  console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
93
105
  }
106
+ /** `fly ssh console` into the app's machine, woken first: a suspended machine has no shell to open. */
107
+ function flyShell(app, workspace) {
108
+ return {
109
+ ...processShell("fly", (command) => ["ssh", "console", "--app", app, "--quiet", "--command", `sh -c '${command}'`], workspace),
110
+ // `fly ssh` fails with "no started VMs" while the machine is suspended, and a request is what resumes it. The
111
+ // answer is not the point: a machine that stays down makes the shell fail right after, with Fly's own reason.
112
+ wake: async () => {
113
+ await fetch(`https://${app}.fly.dev/health`, { signal: AbortSignal.timeout(30_000) }).catch(() => undefined);
114
+ },
115
+ };
116
+ }
@@ -6,17 +6,22 @@ import { basename } from "node:path";
6
6
  import { dockerfilePathVar, isGeneratedRailwayJson, planRailwayDeploy, toRailwayName, } from "../../../deploy/railway/plan.js";
7
7
  import { deployRailwayRun } from "../../../deploy/railway/run.js";
8
8
  import { spawnRunner } from "../../../deploy/runner.js";
9
+ import { assembleSecrets } from "../../../deploy/secrets.js";
10
+ import { processShell } from "../../../deploy/box-shell.js";
9
11
  import { failStartup } from "../../fail.js";
10
- import { carryCredentials, gateOnModelCredential, registrarsFor } from "./shared.js";
12
+ import { isInteractive } from "../../shared.js";
13
+ import { boxLoginStep, registrarsFor } from "./shared.js";
11
14
  export const railwayHost = {
12
15
  isOurs: (path, content) => path.endsWith("railway.json") && isGeneratedRailwayJson(content),
16
+ artifact: "railway.json",
17
+ shell: async ({ workspace }) => railwayShell(toRailwayName(basename(workspace)), workspace),
13
18
  async deploy(ctx) {
14
19
  const { opts, agentDir, workspace, pre, channels, write } = ctx;
15
- const { hasCron, modelAuth, modelKeyInDefinition, authPath, container, declaredSecrets, values, valueFile } = pre;
20
+ const { hasCron, modelAuth, boxLogin, container, declaredSecrets, values, valueFile } = pre;
16
21
  const serviceName = toRailwayName(basename(workspace));
17
22
  const plan = planRailwayDeploy({
18
23
  serviceName,
19
- modelAuth,
24
+ boxLogin,
20
25
  channels,
21
26
  secrets: pre.secrets,
22
27
  hasCron,
@@ -36,8 +41,8 @@ export const railwayHost = {
36
41
  workspace,
37
42
  name: serviceName,
38
43
  modelAuth,
39
- modelKeyInDefinition,
40
- authPath,
44
+ boxLogin,
45
+ input: opts.input !== false && isInteractive(),
41
46
  channels,
42
47
  declaredSecrets,
43
48
  values,
@@ -58,8 +63,11 @@ async function runDeployRailway(params) {
58
63
  if ((await railway(["--version"], { capture: true })).code === 127) {
59
64
  failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
60
65
  }
61
- const { secrets, missingSecrets, needsModelCredential } = await carryCredentials(params);
62
- gateOnModelCredential(needsModelCredential);
66
+ const { secrets, missingSecrets } = assembleSecrets({
67
+ modelAuth: params.modelAuth,
68
+ declared: params.declaredSecrets,
69
+ values: params.values,
70
+ });
63
71
  const outcome = await deployRailwayRun({
64
72
  name,
65
73
  mountPath: "/data",
@@ -69,8 +77,14 @@ async function runDeployRailway(params) {
69
77
  channels,
70
78
  intoLinked,
71
79
  dockerfilePath,
80
+ ...boxLoginStep("railway", params, () => railwayShell(name, workspace)),
72
81
  }, railway, (m) => console.error(`[fastagent] ${m}`), registrarsFor(agentDir));
73
82
  if (!outcome.ok)
74
83
  failStartup(new Error(`deploy stopped: ${outcome.gate}`));
75
84
  console.error(`[fastagent] deployed → ${outcome.url}`);
76
85
  }
86
+ /** `railway ssh` into the service, in the project and environment this directory is linked to. */
87
+ function railwayShell(service, workspace) {
88
+ // One quoted word: the command reaches the box as ONE line its shell parses, the way OpenSSH hands it over.
89
+ return processShell("railway", (command) => ["ssh", "--service", service, "--", `sh -c '${command}'`], workspace);
90
+ }
@@ -2,7 +2,9 @@ import type { DeclaredChannel } from "../../../channels/discover.ts";
2
2
  import type { Registrars } from "../../../deploy/channel-ingress.ts";
3
3
  import type { DeployPreflight } from "../../../deploy/preflight.ts";
4
4
  import type { FastagentConfig } from "../../../engines/pi/config.ts";
5
- import type { DeclaredSecret } from "../../../declared-secrets.ts";
5
+ import { type ResolvedPlacement } from "../../../paths.ts";
6
+ import { type BoxLoginStep, type BoxShell } from "../../../deploy/box-shell.ts";
7
+ import type { DeployHost } from "../../../deploy/hosts.ts";
6
8
  export interface DeployOptions {
7
9
  run?: boolean;
8
10
  tunnel?: boolean;
@@ -39,6 +41,10 @@ interface DeployContext {
39
41
  export interface HostDeploy {
40
42
  /** Did this host generate the file at `path`? */
41
43
  isOurs(path: string, content: string): boolean;
44
+ /** The file in the agent dir whose presence says this workspace deploys to the host. */
45
+ artifact: string;
46
+ /** The shell into this workspace's running box (`fastagent login --deployment`). */
47
+ shell(placement: ResolvedPlacement): Promise<BoxShell>;
42
48
  /**
43
49
  * Plan the artifacts from the pre-flight facts and what is on disk, write them, then either drive the host CLI
44
50
  * (`--run`) or print the runbook.
@@ -48,23 +54,18 @@ export interface HostDeploy {
48
54
  /** The registrars every host driver gets. */
49
55
  export declare function registrarsFor(agentDir: string): Registrars;
50
56
  /**
51
- * The `--run` carry, for every host: the value file's variables plus the local model credential (an env key, or the
52
- * whole auth.json as a `FASTAGENT_AUTH_SEED`).
57
+ * The login `--run` starts on the box it just deployed, before any webhook is pointed at it (a host driver's `boxLogin`): for
58
+ * the model's provider, keeping a credential the box already holds (a redeploy), and asking only when a person can
59
+ * answer. None when the model's credential travels as a variable.
53
60
  */
54
- export declare function carryCredentials(params: {
55
- modelAuth: string | undefined;
56
- modelKeyInDefinition: boolean;
57
- authPath: string;
58
- declaredSecrets: readonly DeclaredSecret[];
59
- /** The deployed environment's declaration, from the pre-flight's single read. */
60
- values: ReadonlyMap<string, string>;
61
- }): Promise<{
62
- secrets: Record<string, string>;
63
- missingSecrets: string[];
64
- needsModelCredential: boolean;
65
- }>;
66
- /** Gate a `--run` that has no model credential to carry. */
67
- export declare function gateOnModelCredential(needsModelCredential: boolean): void;
61
+ export declare function boxLoginStep<Args extends unknown[]>(host: DeployHost, params: ResolvedPlacement & {
62
+ boxLogin: string | undefined;
63
+ input: boolean;
64
+ },
65
+ /** The shell, from whatever the driver learns only after deploying (AgentCore's runtime ARN). */
66
+ shell: (...args: Args) => BoxShell): {
67
+ boxLogin?: BoxLoginStep<Args>;
68
+ };
68
69
  /** One artifact's verdict under the ownership rule: the bytes to write, or `undefined` to keep what is there. */
69
70
  type ArtifactStep = {
70
71
  abs: string;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * What every host's deploy command shares: the context the dispatcher hands a host, the artifact writer with its
3
- * ownership rule, and the `--run` credential carry.
2
+ * What every host's deploy command shares: the context the dispatcher hands a host and the artifact writer with its
3
+ * ownership rule.
4
4
  */
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { dirname, join } from "node:path";
@@ -10,9 +10,9 @@ import { registerSlackWebhook } from "../../../channels/slack/register-webhook.j
10
10
  import { registerTelegramWebhook } from "../../../channels/telegram/register-webhook.js";
11
11
  import { isGeneratedDockerfile, isGeneratedDockerignore } from "../../../deploy/container.js";
12
12
  import { RELEASE_FILE } from "../../../deploy/workspace.js";
13
- import { assembleSecrets } from "../../../deploy/secrets.js";
14
13
  import { exists, resolveStateRoot } from "../../../paths.js";
15
- import { failStartup } from "../../fail.js";
14
+ import { deploymentLoginCommand } from "../../../deploy/box-shell.js";
15
+ import { loginOnBox } from "../../box-login.js";
16
16
  /** The registrars every host driver gets. */
17
17
  export function registrarsFor(agentDir) {
18
18
  const attempts = DEPLOY_REGISTRATION_ATTEMPTS;
@@ -23,24 +23,23 @@ export function registrarsFor(agentDir) {
23
23
  };
24
24
  }
25
25
  /**
26
- * The `--run` carry, for every host: the value file's variables plus the local model credential (an env key, or the
27
- * whole auth.json as a `FASTAGENT_AUTH_SEED`).
26
+ * The login `--run` starts on the box it just deployed, before any webhook is pointed at it (a host driver's `boxLogin`): for
27
+ * the model's provider, keeping a credential the box already holds (a redeploy), and asking only when a person can
28
+ * answer. None when the model's credential travels as a variable.
28
29
  */
29
- export async function carryCredentials(params) {
30
- const { modelAuth, modelKeyInDefinition, authPath, declaredSecrets, values } = params;
31
- return assembleSecrets({
32
- modelAuth,
33
- modelKeyInDefinition,
34
- authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
35
- declared: declaredSecrets,
36
- values,
37
- });
38
- }
39
- /** Gate a `--run` that has no model credential to carry. */
40
- export function gateOnModelCredential(needsModelCredential) {
41
- if (!needsModelCredential)
42
- return;
43
- failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
30
+ export function boxLoginStep(host, params,
31
+ /** The shell, from whatever the driver learns only after deploying (AgentCore's runtime ARN). */
32
+ shell) {
33
+ const provider = params.boxLogin;
34
+ if (provider === undefined)
35
+ return {};
36
+ const placement = { agentDir: params.agentDir, workspace: params.workspace };
37
+ return {
38
+ boxLogin: {
39
+ command: deploymentLoginCommand(host, provider),
40
+ run: (...args) => loginOnBox({ host, shell: shell(...args), placement, provider, ifMissing: true, input: params.input }),
41
+ },
42
+ };
44
43
  }
45
44
  /**
46
45
  * Decide what happens to each generated artifact under `target`, under ONE ownership rule — reading only, so a
@@ -2,7 +2,7 @@
2
2
  import { basename, resolve } from "node:path";
3
3
  import { destroyAgentcoreDeployment } from "../../deploy/agentcore/destroy.js";
4
4
  import { agentcoreName } from "../../deploy/agentcore/plan.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 runDestroy(host, dirArg, opts) {
@@ -15,7 +15,7 @@ export async function runDestroy(host, dirArg, opts) {
15
15
  const placement = placementOrExit(resolve(dirArg));
16
16
  enterAgentEnv(placement.agentDir); // AWS_PROFILE/region/proxy may be definition-local, as on deploy
17
17
  const name = agentcoreName(basename(placement.workspace));
18
- const outcome = await destroyAgentcoreDeployment({ name, run: opts.run === true }, spawnRunner("aws", placement.workspace), (message) => console.error(`[fastagent] destroy: ${message}`));
18
+ const outcome = await destroyAgentcoreDeployment({ name, run: opts.run === true }, awsRunner(placement.workspace), (message) => console.error(`[fastagent] destroy: ${message}`));
19
19
  if (!outcome.ok) {
20
20
  // THE WHOLE PICTURE, before the gate: a half-finished teardown is exactly when an operator needs to know
21
21
  // what is out there, what is already gone, and what the retry still has to reach.
@@ -2,12 +2,13 @@
2
2
  import { resolve } from "node:path";
3
3
  import { enterAgentEnv } from "../../env.js";
4
4
  import { inspectChannels } from "../../channels/discover.js";
5
- import { loadConfig, providerOf, resolveAuthLayers, resolveModel, resolveModelSpec } from "../../engines/pi/config.js";
6
- import { createPiModelRuntime, machineModels } from "../../engines/pi/models.js";
5
+ import { loadConfig, providerOf, resolveModel, resolveModelSpec } from "../../engines/pi/config.js";
6
+ import { machineModels } from "../../engines/pi/models.js";
7
+ import { agentModels } from "../../engines/pi/agent-models.js";
7
8
  import { resolveSessionsDir, resolveStateRoot } from "../../paths.js";
8
9
  import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
9
10
  import { loadAgentDefinition } from "../../engines/pi/definition.js";
10
- import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
+ import { describeIndirectTools, reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
11
12
  import { allSecrets, describeSecrets, missingSecrets } from "../../declared-secrets.js";
12
13
  import { log } from "../../log.js";
13
14
  import { reportModuleLoadFailures } from "../../loader.js";
@@ -31,7 +32,7 @@ export async function runInfo(dirArg, opts) {
31
32
  const tools = await resolveAgentTools(config, agentDir, workspace)
32
33
  .then((r) => ({
33
34
  names: r.toolNames,
34
- deferred: r.deferredToolNames,
35
+ indirect: r.indirectTools,
35
36
  collisions: r.toolCollisions,
36
37
  failures: r.toolFailures,
37
38
  secrets: allSecrets(r.toolSecrets),
@@ -39,7 +40,7 @@ export async function runInfo(dirArg, opts) {
39
40
  }))
40
41
  .catch((e) => ({
41
42
  names: [],
42
- deferred: [],
43
+ indirect: [],
43
44
  collisions: [],
44
45
  failures: [],
45
46
  secrets: [],
@@ -74,13 +75,15 @@ export async function runInfo(dirArg, opts) {
74
75
  const sessionsDir = resolveSessionsDir(agentDir);
75
76
  // Both layers: "what is this agent's state" is the question `info` answers, and a credential it runs on can live in
76
77
  // a file the agent dir does not contain.
77
- const auth = resolveAuthLayers(agentDir);
78
+ const models = agentModels(agentDir, {}, { cwd: workspace });
79
+ const { auth } = models;
78
80
  // RESOLVE the spec, do not just echo it: a spec is only real once its provider/model exist in the agent's own
79
81
  // surface (built-ins + its models.json), which is exactly what a custom endpoint changes.
80
82
  const modelError = modelSpec
81
- ? await createPiModelRuntime({ agentDir, auth })
82
- .then((models) => {
83
- resolveModel(models, modelSpec);
83
+ ? await models
84
+ .runtime()
85
+ .then((runtime) => {
86
+ resolveModel(runtime, modelSpec);
84
87
  return undefined;
85
88
  })
86
89
  .catch((error) => error.message)
@@ -104,7 +107,7 @@ export async function runInfo(dirArg, opts) {
104
107
  persona: definition.persona !== undefined,
105
108
  skills: skills.map((skill) => ({ name: skill.name, description: skill.description })),
106
109
  tools: tools.names,
107
- deferredTools: tools.deferred,
110
+ indirectTools: tools.indirect,
108
111
  toolError: tools.error ?? null,
109
112
  channels,
110
113
  routines,
@@ -142,8 +145,8 @@ export async function runInfo(dirArg, opts) {
142
145
  line("persona", definition.persona ? "persona.md" : "(none)");
143
146
  line("skills", skills.map((skill) => skill.name).join(", ") || "(none)");
144
147
  line("tools", tools.error ? "(could not load — see warning below)" : tools.names.join(", ") || "(none)");
145
- if (tools.deferred.length > 0)
146
- line("deferred", `${tools.deferred.join(", ")} (activated via search_tools)`);
148
+ if (tools.indirect.length > 0)
149
+ line("indirect", describeIndirectTools(tools.indirect));
147
150
  line("channels", channels.join(", ") || "(none)");
148
151
  line("routines", routines.map((r) => `${r.name} (${r.cron === null ? "on demand" : `next ${r.next ?? "never"}`})`).join(", ") ||
149
152
  "(none)");
@@ -6,12 +6,12 @@ import { failStartup } from "../fail.js";
6
6
  import { enterAgentCommand, reportAuth } from "../shared.js";
7
7
  export async function runInvoke(message, dirArg, opts) {
8
8
  const placement = await enterAgentCommand(dirArg, opts);
9
- const { agent, modelSpec, auth } = await createPiAgentFromDir(placement.agentDir, {
9
+ const { agent, modelSpec, models } = await createPiAgentFromDir(placement.agentDir, {
10
10
  model: opts.model,
11
11
  }).catch(failStartup);
12
12
  // BOTH directories, like dev/start: which agent ran, and what it worked on.
13
13
  console.error(`[fastagent] invoke: ${placement.agentDir} (workspace ${placement.workspace}, ${modelSpec})`);
14
- await reportAuth(placement.agentDir, modelSpec, auth);
14
+ await reportAuth(models, modelSpec);
15
15
  // Fresh session per invoke (one-shot, no resume). runInvokeStream maps events→IO: reply→stdout,
16
16
  // tool/failure→stderr, exit 1 iff the turn failed (so CI can gate on it).
17
17
  const exitCode = await runInvokeStream(agent.invoke({ session: randomUUID() }, { text: message }), (text) => process.stdout.write(text), (line) => console.error(line));
@@ -1,7 +1,13 @@
1
1
  export interface LoginOptions {
2
- /** `-g`: store in the user-global file every agent on this machine falls back to. */
2
+ /** `-g`: store in the user-global file, which an agent reads for a provider it has no other credential for. */
3
3
  global?: boolean;
4
4
  /** false ⇔ `--no-input`. */
5
5
  input?: boolean;
6
+ /** `--deployment [host]`: log in this workspace's deployment (`true` when no host was named). */
7
+ deployment?: string | boolean;
8
+ /** `--stdio`: the box's half of `--deployment` — the flow speaks the relay wire on stdin/stdout. */
9
+ stdio?: boolean;
10
+ /** `--if-missing` (with `--stdio`): keep a credential already stored for the provider. */
11
+ ifMissing?: boolean;
6
12
  }
7
13
  export declare function runLogin(provider: string | undefined, opts: LoginOptions): Promise<void>;