@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.
- package/dist/agent.d.ts +10 -0
- package/dist/channels/agentcore-protocol.d.ts +2 -1
- package/dist/channels/agentcore-service.js +10 -18
- package/dist/channels/agentcore.js +6 -3
- package/dist/channels/control.js +23 -0
- package/dist/channels/kit/preview-kit.d.ts +6 -1
- package/dist/channels/kit/preview-kit.js +21 -4
- package/dist/channels/slack/config-api.d.ts +2 -2
- package/dist/channels/slack/config-api.js +2 -2
- package/dist/channels/slack/onboard.js +2 -2
- package/dist/channels/slack/preview.js +2 -1
- package/dist/channels/slack/register-webhook.js +2 -2
- package/dist/channels/slack/slack-api.js +4 -4
- package/dist/cli/add-feishu.js +4 -2
- package/dist/cli/auth-view.d.ts +15 -6
- package/dist/cli/auth-view.js +24 -8
- package/dist/cli/box-login.d.ts +33 -0
- package/dist/cli/box-login.js +162 -0
- package/dist/cli/commands/deploy/agentcore.js +48 -11
- package/dist/cli/commands/deploy/docker.js +24 -13
- package/dist/cli/commands/deploy/fly.js +30 -7
- package/dist/cli/commands/deploy/railway.js +21 -7
- package/dist/cli/commands/deploy/shared.d.ts +18 -17
- package/dist/cli/commands/deploy/shared.js +20 -21
- package/dist/cli/commands/destroy.js +2 -2
- package/dist/cli/commands/info.js +15 -12
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/login.d.ts +7 -1
- package/dist/cli/commands/login.js +123 -10
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/models.d.ts +4 -1
- package/dist/cli/commands/models.js +37 -4
- package/dist/cli/commands/routine-run.js +2 -2
- package/dist/cli/commands/start.d.ts +0 -5
- package/dist/cli/commands/start.js +3 -13
- package/dist/cli/invoke-stream.js +14 -0
- package/dist/cli/kernel.d.ts +2 -0
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/login-relay.d.ts +38 -0
- package/dist/cli/login-relay.js +121 -0
- package/dist/cli/program.js +46 -8
- package/dist/cli/shared.d.ts +6 -5
- package/dist/cli/shared.js +29 -36
- package/dist/deploy/agentcore/plan.d.ts +10 -11
- package/dist/deploy/agentcore/plan.js +8 -14
- package/dist/deploy/agentcore/run.d.ts +8 -1
- package/dist/deploy/agentcore/run.js +20 -14
- package/dist/deploy/agentcore/shell.d.ts +25 -0
- package/dist/deploy/agentcore/shell.js +202 -0
- package/dist/deploy/box-shell.d.ts +43 -0
- package/dist/deploy/box-shell.js +28 -0
- package/dist/deploy/build-context.js +16 -7
- package/dist/deploy/channel-ingress.d.ts +20 -1
- package/dist/deploy/channel-ingress.js +34 -7
- package/dist/deploy/container.d.ts +14 -0
- package/dist/deploy/container.js +45 -1
- package/dist/deploy/docker/plan.d.ts +7 -3
- package/dist/deploy/docker/plan.js +9 -14
- package/dist/deploy/docker/run.d.ts +14 -7
- package/dist/deploy/docker/run.js +58 -35
- package/dist/deploy/fly/plan.d.ts +6 -3
- package/dist/deploy/fly/plan.js +6 -8
- package/dist/deploy/fly/run.d.ts +8 -2
- package/dist/deploy/fly/run.js +15 -25
- package/dist/deploy/preflight.d.ts +9 -7
- package/dist/deploy/preflight.js +92 -20
- package/dist/deploy/railway/plan.d.ts +6 -3
- package/dist/deploy/railway/plan.js +7 -9
- package/dist/deploy/railway/run.d.ts +10 -6
- package/dist/deploy/railway/run.js +29 -13
- package/dist/deploy/runner.d.ts +16 -0
- package/dist/deploy/runner.js +31 -0
- package/dist/deploy/secrets.d.ts +4 -13
- package/dist/deploy/secrets.js +8 -27
- package/dist/dev-supervisor.js +7 -4
- package/dist/engines/pi/agent-models.d.ts +83 -0
- package/dist/engines/pi/agent-models.js +120 -0
- package/dist/engines/pi/agent-session-factory.d.ts +13 -13
- package/dist/engines/pi/agent-session-factory.js +132 -159
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +68 -109
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +34 -21
- package/dist/engines/pi/create.js +90 -57
- package/dist/engines/pi/entry-images.d.ts +14 -0
- package/dist/engines/pi/entry-images.js +23 -0
- package/dist/engines/pi/invoke-session.js +88 -20
- package/dist/engines/pi/locked-file.d.ts +9 -0
- package/dist/engines/pi/locked-file.js +102 -0
- package/dist/engines/pi/login.d.ts +27 -14
- package/dist/engines/pi/login.js +41 -18
- package/dist/engines/pi/machine.d.ts +23 -0
- package/dist/engines/pi/machine.js +24 -2
- package/dist/engines/pi/models.d.ts +120 -36
- package/dist/engines/pi/models.js +316 -66
- package/dist/engines/pi/open.d.ts +65 -20
- package/dist/engines/pi/open.js +88 -34
- package/dist/engines/pi/report.d.ts +3 -0
- package/dist/engines/pi/report.js +11 -0
- package/dist/engines/pi/service.d.ts +2 -2
- package/dist/engines/pi/service.js +1 -0
- package/dist/engines/pi/session-builder.d.ts +3 -2
- package/dist/engines/pi/session-builder.js +30 -76
- package/dist/engines/pi/session-control.d.ts +4 -4
- package/dist/engines/pi/session-control.js +86 -43
- package/dist/engines/pi/session-settings.d.ts +9 -0
- package/dist/engines/pi/session-settings.js +17 -0
- package/dist/engines/pi/session-store.d.ts +2 -2
- package/dist/engines/pi/tool-context.d.ts +3 -5
- package/dist/engines/pi/tool-context.js +4 -16
- package/dist/engines/pi/tool.d.ts +32 -21
- package/dist/engines/pi/tool.js +21 -25
- package/dist/engines/pi/turn-kit.d.ts +18 -9
- package/dist/engines/pi/turn-kit.js +115 -32
- package/dist/open-url.d.ts +15 -1
- package/dist/open-url.js +35 -3
- package/dist/paths.d.ts +4 -2
- package/dist/paths.js +6 -2
- package/dist/pi.d.ts +5 -3
- package/dist/pi.js +3 -2
- package/dist/service.d.ts +5 -8
- package/dist/session-remote.js +21 -0
- package/dist/session.d.ts +88 -15
- package/dist/session.js +5 -1
- package/package.json +6 -5
- package/dist/engines/pi/search-tools.d.ts +0 -4
- 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 {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
15
|
+
import { isInteractive } from "../../shared.js";
|
|
16
|
+
import { boxLoginStep } from "./shared.js";
|
|
14
17
|
export const dockerHost = {
|
|
15
|
-
isOurs: (path, content) => path.endsWith(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
80
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
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`
|
|
52
|
-
*
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
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
|
|
3
|
-
* ownership rule
|
|
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 {
|
|
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`
|
|
27
|
-
*
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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 },
|
|
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,
|
|
6
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
82
|
-
.
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
146
|
-
line("
|
|
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,
|
|
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(
|
|
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
|
|
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>;
|