@fastagent-sh/fastagent 0.15.0 → 0.16.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/README.md +13 -3
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +258 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +5 -0
- package/dist/channels/context-buffer.js +5 -0
- package/dist/channels/feishu/context-buffer.d.ts +13 -8
- package/dist/channels/feishu/context-buffer.js +60 -6
- package/dist/channels/feishu/feishu-api.js +4 -1
- package/dist/channels/feishu/feishu.d.ts +0 -12
- package/dist/channels/feishu/feishu.js +143 -54
- package/dist/channels/feishu/invoke-turn.js +35 -18
- package/dist/channels/feishu/model.d.ts +0 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +21 -7
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.js +3 -2
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +30 -1
- package/dist/channels/feishu/setup-mode.js +27 -1
- package/dist/channels/github/github.js +8 -1
- package/dist/channels/http.js +1 -1
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +7 -1
- package/dist/channels/preview-kit.js +3 -2
- package/dist/channels/slack/parse.d.ts +16 -1
- package/dist/channels/slack/parse.js +46 -3
- package/dist/channels/slack/preview.d.ts +1 -2
- package/dist/channels/slack/preview.js +68 -24
- package/dist/channels/slack/scaffold/channel.ts +5 -5
- package/dist/channels/slack/slack-api.d.ts +3 -23
- package/dist/channels/slack/slack-api.js +6 -22
- package/dist/channels/slack/slack.d.ts +13 -20
- package/dist/channels/slack/slack.js +95 -50
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +19 -12
- package/dist/channels/tasks.d.ts +0 -6
- package/dist/channels/tasks.js +16 -1
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +1 -1
- package/dist/channels/text.d.ts +14 -0
- package/dist/channels/text.js +14 -0
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +7 -0
- package/dist/cli/add-feishu.d.ts +7 -4
- package/dist/cli/add-feishu.js +57 -37
- package/dist/cli/add-slack.d.ts +2 -1
- package/dist/cli/add-slack.js +6 -11
- package/dist/cli/commands/add.js +50 -51
- package/dist/cli/commands/attach.js +8 -4
- package/dist/cli/commands/chat.js +8 -8
- package/dist/cli/commands/deploy.d.ts +1 -1
- package/dist/cli/commands/deploy.js +323 -77
- package/dist/cli/commands/dev.js +24 -22
- package/dist/cli/commands/fire.js +15 -16
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +9 -6
- package/dist/cli/commands/login.js +35 -21
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +93 -37
- package/dist/cli/commands/tool.js +22 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +57 -36
- package/dist/cli/serve.d.ts +26 -6
- package/dist/cli/serve.js +62 -15
- package/dist/cli/shared.d.ts +15 -2
- package/dist/cli/shared.js +30 -18
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +1 -1
- package/dist/deploy/docker/plan.js +12 -17
- package/dist/deploy/fly/plan.d.ts +2 -0
- package/dist/deploy/fly/plan.js +27 -19
- package/dist/deploy/fly/run.d.ts +12 -1
- package/dist/deploy/fly/run.js +36 -2
- package/dist/deploy/preflight.d.ts +11 -5
- package/dist/deploy/preflight.js +217 -65
- package/dist/deploy/railway/plan.d.ts +7 -0
- package/dist/deploy/railway/plan.js +41 -16
- package/dist/deploy/railway/run.d.ts +8 -1
- package/dist/deploy/railway/run.js +7 -2
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +9 -3
- package/dist/dev-supervisor.d.ts +11 -8
- package/dist/dev-supervisor.js +53 -51
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +1 -1
- package/dist/engines/pi/channel.js +5 -5
- package/dist/engines/pi/chat.js +2 -2
- package/dist/engines/pi/config.d.ts +6 -46
- package/dist/engines/pi/config.js +21 -108
- package/dist/engines/pi/create.d.ts +8 -8
- package/dist/engines/pi/create.js +13 -12
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/login.d.ts +1 -1
- package/dist/engines/pi/models.d.ts +3 -3
- package/dist/engines/pi/models.js +1 -1
- package/dist/engines/pi/{workspace.d.ts → open.d.ts} +29 -23
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +11 -11
- package/dist/engines/pi/tool.js +4 -0
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +2 -2
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +1 -1
- package/dist/pi.js +2 -2
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +7 -3
- package/dist/scaffold/add-channel.js +55 -29
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +1 -1
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -0
- package/dist/schedule/wakeups.js +18 -0
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.js +7 -7
- package/package.json +4 -1
- package/dist/channels/feishu/owned-threads.d.ts +0 -7
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/slack/owned-threads.d.ts +0 -6
- package/dist/channels/slack/owned-threads.js +0 -43
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -7,56 +7,65 @@
|
|
|
7
7
|
* Read-only on the definition; the only writes are generated artifacts (never clobbered without
|
|
8
8
|
* --force). `--run` drives the target CLI instead of printing.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
11
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
11
13
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
14
|
import { registerFeishuWebhook } from "../../channels/feishu/register-webhook.js";
|
|
13
15
|
import { readSlackBotAuthEnv } from "../../channels/slack/bot-auth.js";
|
|
14
16
|
import { registerSlackWebhook } from "../../channels/slack/register-webhook.js";
|
|
15
17
|
import { registerTelegramWebhook } from "../../channels/telegram/register-webhook.js";
|
|
16
|
-
import {
|
|
18
|
+
import { FORWARDER_FILE, TEMPLATE_FILE, isGeneratedAgentcoreTemplate, planAgentcoreDeploy, } from "../../deploy/agentcore/plan.js";
|
|
19
|
+
import { deployAgentcoreRun } from "../../deploy/agentcore/run.js";
|
|
20
|
+
import { isGeneratedDockerfile, isGeneratedDockerignore } from "../../deploy/container.js";
|
|
17
21
|
import { composeHasTunnelService, dockerWebhookPaths, isGeneratedCompose, planDockerDeploy, toDockerProjectName, } from "../../deploy/docker/plan.js";
|
|
18
22
|
import { deployDockerRun } from "../../deploy/docker/run.js";
|
|
19
|
-
import { parseFlyAppName, parseFlyMinMachines, parseFlyRegion, planFlyDeploy, toFlyAppName, } from "../../deploy/fly/plan.js";
|
|
23
|
+
import { isGeneratedFlyToml, parseFlyAppName, parseFlyMinMachines, parseFlyRegion, planFlyDeploy, toFlyAppName, } from "../../deploy/fly/plan.js";
|
|
20
24
|
import { deployFlyRun } from "../../deploy/fly/run.js";
|
|
21
25
|
import { preflightDeploy } from "../../deploy/preflight.js";
|
|
22
|
-
import { planRailwayDeploy } from "../../deploy/railway/plan.js";
|
|
26
|
+
import { dockerfilePathVar, isGeneratedRailwayJson, planRailwayDeploy } from "../../deploy/railway/plan.js";
|
|
23
27
|
import { deployRailwayRun } from "../../deploy/railway/run.js";
|
|
24
28
|
import { spawnRunner } from "../../deploy/runner.js";
|
|
25
29
|
import { assembleSecrets } from "../../deploy/secrets.js";
|
|
26
30
|
import { loadDotEnv } from "../../env.js";
|
|
27
|
-
import { loadConfig,
|
|
31
|
+
import { loadConfig, resolveModelSpec } from "../../engines/pi/config.js";
|
|
32
|
+
import { resolveStateRoot } from "../../paths.js";
|
|
33
|
+
import { loadSchedules } from "../../schedule/discover.js";
|
|
28
34
|
import { installProxyFetch } from "../../proxy.js";
|
|
29
35
|
import { openExternalUrl } from "../../open-url.js";
|
|
30
|
-
import { exists } from "../../
|
|
36
|
+
import { exists } from "../../paths.js";
|
|
31
37
|
import { announceWebhooks } from "../../tunnel.js";
|
|
32
|
-
import { failStartup, failUsage } from "../fail.js";
|
|
38
|
+
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
33
39
|
import { resolveFirstRunModel } from "../shared.js";
|
|
34
40
|
export async function runDeploy(host, dirArg, opts) {
|
|
35
|
-
|
|
41
|
+
// ONE deploy semantic: bake the WORKSPACE (WYSIWYG). Artifacts land under the agent dir
|
|
42
|
+
// (`fastagent/`) plus the one workspace-root `.dockerignore` the packers require; host CLIs run
|
|
43
|
+
// from the workspace, which is the build context.
|
|
44
|
+
const placement = placementOrExit(resolve(dirArg));
|
|
45
|
+
const { agentDir, workspace } = placement;
|
|
36
46
|
if (opts.tunnel && host !== "docker") {
|
|
37
47
|
// A flag/host combination the parser cannot see (host is an argument) — usage class, exit 2.
|
|
38
48
|
failUsage(`deploy stopped: --tunnel is supported only by the local Docker target`);
|
|
39
49
|
}
|
|
40
|
-
loadDotEnv(
|
|
50
|
+
loadDotEnv(agentDir); // a custom provider/tool may read a key at config load
|
|
41
51
|
installProxyFetch(); // post-deploy channel API calls must honor HTTP(S)_PROXY under Node
|
|
42
52
|
// First-run funnel, FULL picker: the write-back lands the model in fastagent.config.* — exactly what
|
|
43
53
|
// the model-travel gate below requires (--model/env don't reach the deployed box) — and an inline
|
|
44
54
|
// login stores the credential `--run` then carries. Runs BEFORE loadConfig; the read-back sees the
|
|
45
55
|
// rewritten file because loadConfig cache-busts on mtime (a failed write-back still gates, correctly).
|
|
46
|
-
await resolveFirstRunModel(
|
|
47
|
-
const { config } = await loadConfig(
|
|
48
|
-
const agentDir = resolveAgentDir(target, config);
|
|
56
|
+
await resolveFirstRunModel(agentDir, { model: opts.model, authPath: opts.authPath, input: opts.input });
|
|
57
|
+
const { config } = await loadConfig(agentDir).catch(failStartup);
|
|
49
58
|
const modelSpec = resolveModelSpec(opts.model, config);
|
|
50
59
|
// The host-neutral pre-flight (model-travel gate, channel discovery, model-auth probe, container facts +
|
|
51
60
|
// their warnings) lives in deploy/preflight.ts — testable in isolation. The CLI prints its messages and
|
|
52
61
|
// stops on its gate; the host branch below adds only the host-specific artifacts + runbook + run drive.
|
|
53
62
|
const pre = await preflightDeploy({
|
|
54
|
-
|
|
55
|
-
agentDir,
|
|
63
|
+
placement,
|
|
56
64
|
config,
|
|
57
65
|
modelSpec,
|
|
58
66
|
run: !!opts.run,
|
|
59
67
|
force: !!opts.force,
|
|
68
|
+
externalClock: host === "agentcore", // cron rides EventBridge there — the resident-host notes don't apply
|
|
60
69
|
authPathFlag: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by preflight (one owner)
|
|
61
70
|
}).catch(failStartup);
|
|
62
71
|
if (!pre.ok)
|
|
@@ -74,7 +83,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
74
83
|
if (opts.intoLinked) {
|
|
75
84
|
console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for local Docker`);
|
|
76
85
|
}
|
|
77
|
-
const projectName = toDockerProjectName(basename(
|
|
86
|
+
const projectName = toDockerProjectName(basename(workspace));
|
|
78
87
|
const dockerPlan = (tunnel) => planDockerDeploy({
|
|
79
88
|
projectName,
|
|
80
89
|
port,
|
|
@@ -92,20 +101,23 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
92
101
|
let plan = dockerPlan(requestedTunnel);
|
|
93
102
|
// An existing Compose file is authoritative: shape its comparison/runbook from the topology on disk,
|
|
94
103
|
// regardless of the current flag. `--force` is the explicit reset to the requested generated shape.
|
|
95
|
-
const composeFile = join(
|
|
104
|
+
const composeFile = join(workspace, plan.composePath);
|
|
96
105
|
let keptWithoutRequestedTunnel = false;
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
// Same ownership question as fly.toml: a hand-owned compose file survives --force, so the plan must
|
|
107
|
+
// describe the topology that will actually be there.
|
|
108
|
+
const composeText = await readFile(composeFile, "utf8").catch(() => undefined);
|
|
109
|
+
if (composeText !== undefined && (!opts.force || !isGeneratedCompose(composeText))) {
|
|
110
|
+
const existingHasTunnel = composeHasTunnelService(composeText);
|
|
99
111
|
plan = dockerPlan(existingHasTunnel);
|
|
100
112
|
keptWithoutRequestedTunnel = requestedTunnel && !existingHasTunnel;
|
|
101
113
|
}
|
|
102
|
-
await writeArtifacts(
|
|
114
|
+
await writeArtifacts(workspace, plan.artifacts, {
|
|
103
115
|
force: !!opts.force,
|
|
104
|
-
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
105
116
|
});
|
|
106
117
|
if (opts.run) {
|
|
107
118
|
return runDeployDocker({
|
|
108
|
-
|
|
119
|
+
agentDir,
|
|
120
|
+
workspace,
|
|
109
121
|
composeFile: plan.composePath,
|
|
110
122
|
port,
|
|
111
123
|
requireTunnel: requestedTunnel,
|
|
@@ -132,7 +144,7 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
132
144
|
}
|
|
133
145
|
// Railway service names are project-scoped (not globally unique like a Fly app); slug the dir
|
|
134
146
|
// basename so a name with spaces/odd chars can't break the `railway add --service <name>` command.
|
|
135
|
-
const serviceName = basename(
|
|
147
|
+
const serviceName = basename(workspace)
|
|
136
148
|
.replace(/[^a-zA-Z0-9-]+/g, "-")
|
|
137
149
|
.replace(/^-+|-+$/g, "") || "agent";
|
|
138
150
|
const plan = planRailwayDeploy({
|
|
@@ -144,13 +156,20 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
144
156
|
hasTimeTriggers,
|
|
145
157
|
...container,
|
|
146
158
|
});
|
|
147
|
-
await writeArtifacts(
|
|
159
|
+
await writeArtifacts(workspace, plan.artifacts, {
|
|
148
160
|
force: !!opts.force,
|
|
149
|
-
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
150
161
|
});
|
|
151
162
|
if (opts.run) {
|
|
163
|
+
// The BUILD entry is guaranteed by the RAILWAY_DOCKERFILE_PATH service variable the runner sets
|
|
164
|
+
// (Railway's documented non-root-Dockerfile route), and Railway's default restart policy already
|
|
165
|
+
// equals the file's ON_FAILURE — the dashboard-only Config-as-code pointer only adds the /health
|
|
166
|
+
// deploy gate (boot-crash visibility), so it is an OPTIONAL note, not a gate.
|
|
167
|
+
console.error(`[fastagent] note: optional — point the service at fastagent/railway.json (Service → Settings → ` +
|
|
168
|
+
`Config-as-code, dashboard-only) so the /health healthcheck marks a boot-crashing deploy as FAILED; ` +
|
|
169
|
+
`the build already uses fastagent/Dockerfile via the RAILWAY_DOCKERFILE_PATH variable`);
|
|
152
170
|
return runDeployRailway({
|
|
153
|
-
|
|
171
|
+
agentDir,
|
|
172
|
+
workspace,
|
|
154
173
|
name: serviceName,
|
|
155
174
|
modelAuth,
|
|
156
175
|
authPath,
|
|
@@ -158,6 +177,113 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
158
177
|
longConnectionChannels,
|
|
159
178
|
extraSecrets,
|
|
160
179
|
intoLinked: !!opts.intoLinked,
|
|
180
|
+
dockerfilePath: dockerfilePathVar(pre.container.agentPrefix),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
console.log(plan.runbook.join("\n"));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// AgentCore: one CloudFormation stack (runtime + forwarder Lambda + EventBridge schedules); no
|
|
187
|
+
// public URL and no resident process — see deploy/agentcore/plan.ts for the topology decisions.
|
|
188
|
+
if (host === "agentcore") {
|
|
189
|
+
if (opts.stop || opts.scaleToZero === false) {
|
|
190
|
+
console.error(`[fastagent] warn: --stop/--no-scale-to-zero are Fly-only — AgentCore's idle/lifetime policy lives in ` +
|
|
191
|
+
`the template's LifecycleConfiguration.`);
|
|
192
|
+
}
|
|
193
|
+
if (opts.intoLinked) {
|
|
194
|
+
console.error(`[fastagent] warn: --into-linked is Railway-only — ignored for AgentCore`);
|
|
195
|
+
}
|
|
196
|
+
// Long-connection channels are STRUCTURALLY unsupported: the connection is the ingress, and a
|
|
197
|
+
// reclaimed session has nothing to wake it — events in the gap are silently lost. `--run` gates
|
|
198
|
+
// (deploying a channel that can't connect); generate-only warns and prints the runbook.
|
|
199
|
+
if (longConnectionChannels.length > 0) {
|
|
200
|
+
const msg = `long-connection channel (${longConnectionChannels.join(", ")}) cannot run on AgentCore — there is no ` +
|
|
201
|
+
`resident process to hold the connection, and nothing wakes a reclaimed session. Switch the channel ` +
|
|
202
|
+
`to webhook mode (its events then ride the forwarder like every other channel).`;
|
|
203
|
+
if (opts.run)
|
|
204
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
205
|
+
console.error(`[fastagent] warn: ${msg}`);
|
|
206
|
+
}
|
|
207
|
+
// selfSchedule is fully supported: pending wake-ups are mirrored into one-shot EventBridge
|
|
208
|
+
// schedules via the forwarder (the wake-alarm mechanism — see deploy/agentcore/plan.ts).
|
|
209
|
+
// Schedules feed EventBridge rules — parsed facts (cron/tz), not just file names, so a bad file
|
|
210
|
+
// must surface here (a schedule silently missing its rule would never fire).
|
|
211
|
+
const loaded = await loadSchedules(agentDir).catch(failStartup);
|
|
212
|
+
if (loaded.failures.length > 0) {
|
|
213
|
+
failStartup(new Error(`deploy stopped: cannot load schedules: ${loaded.failures.map((x) => `${x.label}: ${x.message}`).join("; ")}`));
|
|
214
|
+
}
|
|
215
|
+
const acName = basename(workspace)
|
|
216
|
+
.toLowerCase()
|
|
217
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
218
|
+
.replace(/^-+|-+$/g, "") || "agent";
|
|
219
|
+
// Every derived AWS name embeds acName; the tightest ceiling is the Lambda function name
|
|
220
|
+
// (`fastagent-<name>-forwarder` ≤ 64 chars). Gate the base instead of silently truncating —
|
|
221
|
+
// truncation would break the redeploy identity (a renamed stack starts blank state).
|
|
222
|
+
if (acName.length > 40) {
|
|
223
|
+
failStartup(new Error(`deploy stopped: the directory name maps to "${acName}" (${acName.length} chars) — AWS resource ` +
|
|
224
|
+
`names derived from it exceed their limits past 40 chars. Deploy from a shorter directory name.`));
|
|
225
|
+
}
|
|
226
|
+
const plan = planAgentcoreDeploy({
|
|
227
|
+
name: acName,
|
|
228
|
+
modelAuth,
|
|
229
|
+
channels,
|
|
230
|
+
routeChannels,
|
|
231
|
+
extraSecrets,
|
|
232
|
+
schedules: loaded.schedules.map((s) => ({ name: s.name, cron: s.cron, tz: s.tz })),
|
|
233
|
+
selfSchedule: !!config.selfSchedule,
|
|
234
|
+
...container,
|
|
235
|
+
});
|
|
236
|
+
for (const u of plan.untranslatableSchedules) {
|
|
237
|
+
// Same discipline as Fly's kept-toml time-trigger gate: a deploy whose schedule silently never
|
|
238
|
+
// fires is worse than a stopped deploy — nothing fails visibly when the instant passes.
|
|
239
|
+
const msg = `schedule "${u.name}" cannot be expressed as an EventBridge rule — ${u.reason}`;
|
|
240
|
+
if (opts.run)
|
|
241
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
242
|
+
console.error(`[fastagent] warn: ${msg} — it will NOT fire on this deployment`);
|
|
243
|
+
}
|
|
244
|
+
// Host capability limit, stated at plan time. GitHub's own webhook contract is 25 MiB, but a
|
|
245
|
+
// Lambda Function URL request caps at 6 MB — so on this host a large payload cannot arrive at
|
|
246
|
+
// all. Better a sentence here than an opaque 502 the first time someone pushes a big diff.
|
|
247
|
+
if (channels.includes("github")) {
|
|
248
|
+
console.error(`[fastagent] note: on AgentCore a webhook body is capped at ~${Math.round(MAX_WEBHOOK_BODY_BYTES / (1 << 20))} MiB ` +
|
|
249
|
+
`(the forwarder's Function URL limit); the GitHub channel accepts 25 MiB on a resident host, so the largest ` +
|
|
250
|
+
`payloads are rejected here rather than delivered`);
|
|
251
|
+
}
|
|
252
|
+
// The template IS the topology (EventBridge rules, wake wiring, secrets) — a kept generated
|
|
253
|
+
// template that no longer matches the definition would deploy a stack silently missing the
|
|
254
|
+
// difference (a new schedule with no rule never fires: the exact miss the gate above stops).
|
|
255
|
+
// A hand-written template (marker removed) is the operator's own — kept, never gated.
|
|
256
|
+
const templateArtifact = plan.artifacts.find((a) => a.path.endsWith(TEMPLATE_FILE));
|
|
257
|
+
const templateHome = join(workspace, templateArtifact?.path ?? TEMPLATE_FILE);
|
|
258
|
+
if (!opts.force && templateArtifact && (await exists(templateHome))) {
|
|
259
|
+
const existing = await readFile(templateHome, "utf8");
|
|
260
|
+
if (isGeneratedAgentcoreTemplate(existing) && existing !== templateArtifact.content) {
|
|
261
|
+
const msg = `${templateArtifact.path} no longer matches this definition (channels/schedules/selfSchedule changed) — ` +
|
|
262
|
+
`the kept template would silently drop the difference. Pass --force to regenerate (hand edits are lost), ` +
|
|
263
|
+
`or delete the file.`;
|
|
264
|
+
if (opts.run)
|
|
265
|
+
failStartup(new Error(`deploy stopped: ${msg}`));
|
|
266
|
+
console.error(`[fastagent] warn: ${msg}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
await writeArtifacts(workspace, plan.artifacts, {
|
|
270
|
+
force: !!opts.force,
|
|
271
|
+
alwaysWrite: [`${container.agentPrefix}${FORWARDER_FILE}`],
|
|
272
|
+
});
|
|
273
|
+
if (opts.run) {
|
|
274
|
+
return runDeployAgentcore({
|
|
275
|
+
agentDir,
|
|
276
|
+
workspace,
|
|
277
|
+
agentPrefix: container.agentPrefix,
|
|
278
|
+
name: acName,
|
|
279
|
+
modelAuth,
|
|
280
|
+
authPath,
|
|
281
|
+
channels,
|
|
282
|
+
extraSecrets,
|
|
283
|
+
selfSchedule: !!config.selfSchedule,
|
|
284
|
+
// Same predicate the template uses for the forwarder resource — kept in one expression so
|
|
285
|
+
// the run path and the topology cannot disagree about whether a forwarder exists.
|
|
286
|
+
needsForwarder: routeChannels.length > 0 || loaded.schedules.length > 0 || !!config.selfSchedule,
|
|
161
287
|
});
|
|
162
288
|
}
|
|
163
289
|
console.log(plan.runbook.join("\n"));
|
|
@@ -178,20 +304,26 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
178
304
|
// and the runbook reads its `app=` (Fly app names are globally unique, so the basename guess may be
|
|
179
305
|
// taken and the user renamed it). --force: the template is authoritative — the WHOLE fly.toml resets
|
|
180
306
|
// (app→basename, region→iad, vm→defaults), so we do NOT round-trip `app` and warn that hand edits go.
|
|
181
|
-
//
|
|
182
|
-
// belongs to the host's product deploy and is never read or written here.
|
|
183
|
-
const flyTomlPath =
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
307
|
+
// fly.toml lives in the agent dir (fastagent/fly.toml) — the workspace's own
|
|
308
|
+
// fly.toml (if any) belongs to the host's product deploy and is never read or written here.
|
|
309
|
+
const flyTomlPath = join(agentDir, "fly.toml");
|
|
310
|
+
const flyToml = await readFile(flyTomlPath, "utf8").catch(() => undefined);
|
|
311
|
+
// Every decision below turns on ONE question — will `writeArtifacts` keep this file? — and the answer is
|
|
312
|
+
// OWNERSHIP, not the flag: `--force` resets a fly.toml we generated and leaves a hand-written one alone.
|
|
313
|
+
// Keying these on `--force` alone meant a forced deploy of a hand-owned fly.toml took the app name from
|
|
314
|
+
// the directory basename (shipping that file at a DIFFERENT app than it declares) and skipped the
|
|
315
|
+
// scale-to-zero gate below (a schedules deploy that silently sleeps). Read once, decide once.
|
|
316
|
+
const flyTomlKept = flyToml !== undefined && (!opts.force || !isGeneratedFlyToml(flyToml));
|
|
317
|
+
const keptApp = flyTomlKept ? parseFlyAppName(flyToml) : undefined;
|
|
318
|
+
const appName = keptApp ?? toFlyAppName(basename(workspace));
|
|
187
319
|
if (keptApp)
|
|
188
320
|
console.error(`[fastagent] app: ${keptApp} (from fly.toml)`);
|
|
189
|
-
if (
|
|
321
|
+
if (flyToml !== undefined && !flyTomlKept) {
|
|
190
322
|
console.error(`[fastagent] warn: --force resets fly.toml to defaults (app, region, vm) — re-apply any hand edits`);
|
|
191
323
|
}
|
|
192
324
|
// Autostop flags shape the GENERATED fly.toml only. In KEEP mode (fly.toml exists, no --force) it is
|
|
193
325
|
// not rewritten, so the flags would silently do nothing — surface that instead of a confusing no-op.
|
|
194
|
-
if (
|
|
326
|
+
if (flyTomlKept && (opts.stop || opts.scaleToZero === false)) {
|
|
195
327
|
console.error(`[fastagent] warn: --stop/--no-scale-to-zero only shape a freshly generated fly.toml — yours exists and ` +
|
|
196
328
|
`was kept. Edit auto_stop_machines/min_machines_running in fly.toml, or pass --force to regenerate.`);
|
|
197
329
|
}
|
|
@@ -201,8 +333,8 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
201
333
|
// Under `--run` this is a GATE (same discipline as the model-travel gate): a full deploy whose schedules
|
|
202
334
|
// silently never fire is worse than a crash-loop — nothing fails visibly when a cron instant passes on a
|
|
203
335
|
// sleeping machine, and unlike github's min=0 there is no legitimate trade to accept here.
|
|
204
|
-
if (
|
|
205
|
-
const min = parseFlyMinMachines(
|
|
336
|
+
if (flyTomlKept && (hasTimeTriggers || longConnectionChannels.length > 0)) {
|
|
337
|
+
const min = parseFlyMinMachines(flyToml);
|
|
206
338
|
if ((min ?? 0) === 0) {
|
|
207
339
|
// undefined = the line is absent — Fly's platform default for min_machines_running is 0, so a
|
|
208
340
|
// hand-written fly.toml without the line scales to zero exactly like an explicit 0.
|
|
@@ -228,13 +360,14 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
228
360
|
autostop: opts.stop ? "stop" : "suspend",
|
|
229
361
|
scaleToZero: opts.scaleToZero !== false,
|
|
230
362
|
});
|
|
231
|
-
await writeArtifacts(
|
|
363
|
+
await writeArtifacts(workspace, plan.artifacts, {
|
|
232
364
|
force: !!opts.force,
|
|
233
|
-
neverForce: container.kitDir ? [".dockerignore"] : [],
|
|
234
365
|
});
|
|
235
366
|
if (opts.run) {
|
|
236
367
|
return runDeployFly({
|
|
237
|
-
|
|
368
|
+
agentDir,
|
|
369
|
+
workspace,
|
|
370
|
+
agentPrefix: container.agentPrefix,
|
|
238
371
|
appName,
|
|
239
372
|
modelAuth,
|
|
240
373
|
authPath,
|
|
@@ -247,44 +380,79 @@ export async function runDeploy(host, dirArg, opts) {
|
|
|
247
380
|
console.log(plan.runbook.join("\n"));
|
|
248
381
|
}
|
|
249
382
|
/**
|
|
250
|
-
* Write each generated artifact
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
383
|
+
* Write each generated artifact, under ONE ownership rule: **deploy only ever overwrites its own
|
|
384
|
+
* output.** Each generated file opens with a marker, so a file on disk is either OURS (a previous
|
|
385
|
+
* `deploy` wrote it) or the author's — and `--force` means "my generated artifact is authoritative",
|
|
386
|
+
* which never licenses clobbering a file we did not write. To hand a path back to fastagent, delete it.
|
|
387
|
+
*
|
|
388
|
+
* That single rule replaced a second mechanism (a per-host list of paths exempt from `--force`), which
|
|
389
|
+
* only described one shape: with the agent inside the workspace, the root `.dockerignore` is the
|
|
390
|
+
* WORKSPACE's file and was listed; with the agent flat, the list was empty — so `--force` would have
|
|
391
|
+
* overwritten a hand-written root `Dockerfile` in a repository `init --flat` had explicitly adopted.
|
|
392
|
+
* Ownership is a property of the file, not of where the agent sits.
|
|
393
|
+
*
|
|
394
|
+
* A KEPT file of OURS that no longer matches what deploy would generate now (config/channel/lockfile/
|
|
395
|
+
* version drift — or the author's edits, which we cannot tell apart) is flagged stale; `--force`
|
|
396
|
+
* regenerates that one.
|
|
255
397
|
*/
|
|
398
|
+
/** Did fastagent generate the file at `path`? ONE table for all five artifact kinds: a kind missing from
|
|
399
|
+
* it is permanently classified as the author's, which silently turns `--force` into a no-op for that
|
|
400
|
+
* file (`fly.toml` and `railway.json` were, until the marker predicates below existed). */
|
|
401
|
+
function isOurArtifact(path, content) {
|
|
402
|
+
if (path.endsWith("Dockerfile"))
|
|
403
|
+
return isGeneratedDockerfile(content);
|
|
404
|
+
if (path.endsWith(".dockerignore"))
|
|
405
|
+
return isGeneratedDockerignore(content);
|
|
406
|
+
if (path.endsWith("fastagent.compose.yml"))
|
|
407
|
+
return isGeneratedCompose(content);
|
|
408
|
+
if (path.endsWith("fly.toml"))
|
|
409
|
+
return isGeneratedFlyToml(content);
|
|
410
|
+
if (path.endsWith("railway.json"))
|
|
411
|
+
return isGeneratedRailwayJson(content);
|
|
412
|
+
if (path.endsWith(TEMPLATE_FILE))
|
|
413
|
+
return isGeneratedAgentcoreTemplate(content);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
256
416
|
async function writeArtifacts(target, artifacts, options) {
|
|
257
417
|
for (const a of artifacts) {
|
|
258
418
|
const abs = join(target, a.path);
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
419
|
+
// Pure build output, not operator-owned configuration. It must track the generated template/runbook.
|
|
420
|
+
if (options.alwaysWrite?.includes(a.path)) {
|
|
421
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
422
|
+
await writeFile(abs, a.content);
|
|
423
|
+
console.error(`[fastagent] wrote ${a.path}`);
|
|
264
424
|
continue;
|
|
265
425
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
426
|
+
const existing = (await exists(abs)) ? await readFile(abs, "utf8") : undefined;
|
|
427
|
+
const ours = existing !== undefined && isOurArtifact(a.path, existing);
|
|
428
|
+
if (existing !== undefined && !ours) {
|
|
429
|
+
// Only the `.dockerignore` has content checks in preflight — pointing at "the preflight warnings"
|
|
430
|
+
// for the other kinds would send the reader looking for output that is never printed.
|
|
431
|
+
console.error(`[fastagent] kept ${a.path} — not generated by fastagent, so --force does not touch it ` +
|
|
432
|
+
`(delete it to let deploy own the path)` +
|
|
433
|
+
(a.path.endsWith(".dockerignore")
|
|
434
|
+
? `; see the preflight warnings for what it must exclude`
|
|
435
|
+
: a.path.endsWith("Dockerfile")
|
|
436
|
+
? `; deploy still assumes it listens on $PORT and runs \`fastagent start /app\``
|
|
437
|
+
: ``));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (existing !== undefined && !options.force) {
|
|
441
|
+
console.error(existing !== a.content
|
|
442
|
+
? `[fastagent] kept ${a.path} — it no longer matches what deploy would generate (config changed, or ` +
|
|
443
|
+
`you edited it); pass --force to regenerate.`
|
|
444
|
+
: `[fastagent] kept ${a.path} (unchanged)`);
|
|
277
445
|
continue;
|
|
278
446
|
}
|
|
279
|
-
await mkdir(dirname(abs), { recursive: true }); //
|
|
447
|
+
await mkdir(dirname(abs), { recursive: true }); // artifacts live under fastagent/
|
|
280
448
|
await writeFile(abs, a.content);
|
|
281
449
|
console.error(`[fastagent] wrote ${a.path}`);
|
|
282
450
|
}
|
|
283
451
|
}
|
|
284
|
-
function deployEnvironment(
|
|
452
|
+
function deployEnvironment(agentDir, channels) {
|
|
285
453
|
if (!channels.includes("slack"))
|
|
286
454
|
return process.env;
|
|
287
|
-
const latest = readSlackBotAuthEnv(join(resolveStateRoot(
|
|
455
|
+
const latest = readSlackBotAuthEnv(join(resolveStateRoot(agentDir), "channels", "slack", "bot-auth.json"));
|
|
288
456
|
return { ...process.env, ...latest };
|
|
289
457
|
}
|
|
290
458
|
/**
|
|
@@ -293,16 +461,16 @@ function deployEnvironment(target, channels) {
|
|
|
293
461
|
* when present, yields an ephemeral URL that reuses the same webhook announcer as `dev --tunnel`.
|
|
294
462
|
*/
|
|
295
463
|
async function runDeployDocker(params) {
|
|
296
|
-
const {
|
|
464
|
+
const { agentDir, workspace, composeFile, port, requireTunnel, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, } = params;
|
|
297
465
|
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
298
466
|
modelAuth,
|
|
299
467
|
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
300
468
|
channels,
|
|
301
469
|
longConnectionChannels,
|
|
302
470
|
extraSecrets,
|
|
303
|
-
env: deployEnvironment(
|
|
471
|
+
env: deployEnvironment(agentDir, channels),
|
|
304
472
|
});
|
|
305
|
-
const outcome = await deployDockerRun({ composeFile, port, secrets, missingSecrets, needsModelCredential, requireTunnel }, spawnRunner("docker",
|
|
473
|
+
const outcome = await deployDockerRun({ composeFile, port, secrets, missingSecrets, needsModelCredential, requireTunnel }, spawnRunner("docker", workspace), (message) => console.error(`[fastagent] ${message}`));
|
|
306
474
|
if (!outcome.ok)
|
|
307
475
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
308
476
|
const compose = `docker compose -f ${composeFile}`;
|
|
@@ -313,10 +481,10 @@ async function runDeployDocker(params) {
|
|
|
313
481
|
// Docker Desktop commonly injects a host proxy. The Quick Tunnel hostname may be resolvable only
|
|
314
482
|
// through it, exactly like provider/channel APIs; use the same Node dispatcher as dev/start/login.
|
|
315
483
|
installProxyFetch();
|
|
316
|
-
await announceWebhooks(
|
|
484
|
+
await announceWebhooks(agentDir, outcome.tunnelUrl, {
|
|
317
485
|
openUrl: openExternalUrl,
|
|
318
486
|
routeChannels: channels.filter((kind) => !longConnectionChannels.includes(kind)),
|
|
319
|
-
stateRoot: resolveStateRoot(
|
|
487
|
+
stateRoot: resolveStateRoot(agentDir),
|
|
320
488
|
});
|
|
321
489
|
console.error(`[fastagent] note: Quick Tunnel URLs are ephemeral — after the tunnel container/Docker daemon ` +
|
|
322
490
|
`restarts, re-run this deploy so webhooks receive the new URL`);
|
|
@@ -333,11 +501,11 @@ async function runDeployDocker(params) {
|
|
|
333
501
|
* from the local env — the model key (env auth) or the whole auth.json as a `FASTAGENT_AUTH_SEED` seed
|
|
334
502
|
* (OAuth/stored auth: the deployed box materializes it onto the /data volume on first boot, so a
|
|
335
503
|
* personal deploy runs on the SAME subscription) plus channel secrets — then runs the flyctl steps
|
|
336
|
-
* behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the
|
|
504
|
+
* behind the shared {@link spawnRunner} seam (spawned `fly`, cwd = the workspace so the build context is the whole workspace).
|
|
337
505
|
*/
|
|
338
506
|
async function runDeployFly(params) {
|
|
339
|
-
const {
|
|
340
|
-
const fly = spawnRunner("fly",
|
|
507
|
+
const { agentDir, workspace, agentPrefix, appName, modelAuth, authPath, channels, longConnectionChannels, flyTomlPath, extraSecrets, } = params;
|
|
508
|
+
const fly = spawnRunner("fly", workspace);
|
|
341
509
|
// Fail fast if flyctl is absent (spawn ENOENT → 127), with the install link — not a confusing auth gate.
|
|
342
510
|
if ((await fly(["version"], { capture: true })).code === 127) {
|
|
343
511
|
failStartup(new Error(`flyctl not found — install it: https://fly.io/docs/flyctl/install, then re-run`));
|
|
@@ -349,28 +517,106 @@ async function runDeployFly(params) {
|
|
|
349
517
|
channels,
|
|
350
518
|
longConnectionChannels,
|
|
351
519
|
extraSecrets,
|
|
352
|
-
env: deployEnvironment(
|
|
520
|
+
env: deployEnvironment(agentDir, channels),
|
|
353
521
|
});
|
|
354
522
|
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env) — gate it
|
|
355
523
|
// here, not through missingSecrets, so the message isn't a contradictory mash of both.
|
|
356
524
|
if (needsModelCredential) {
|
|
357
525
|
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
358
526
|
}
|
|
359
|
-
const outcome = await deployFlyRun({
|
|
527
|
+
const outcome = await deployFlyRun({
|
|
528
|
+
appName,
|
|
529
|
+
region,
|
|
530
|
+
secrets,
|
|
531
|
+
missingSecrets,
|
|
532
|
+
channels,
|
|
533
|
+
longConnectionChannels,
|
|
534
|
+
flyConfig: `${agentPrefix}fly.toml`,
|
|
535
|
+
dockerfile: `${agentPrefix}Dockerfile`,
|
|
536
|
+
}, fly, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir) }));
|
|
360
537
|
if (!outcome.ok)
|
|
361
538
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
362
539
|
console.error(`[fastagent] deployed → https://${appName}.fly.dev`);
|
|
363
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* `deploy agentcore --run`: drive aws + docker to completion. Mirrors {@link runDeployFly} — same
|
|
543
|
+
* credential carry via {@link assembleSecrets}, same runner seam (spawned `aws` + `docker`, cwd = the
|
|
544
|
+
* workspace so the build context is the agent). The AgentCore-specific sequence (identity → buildx →
|
|
545
|
+
* ECR → CloudFormation → outputs → webhooks) lives in {@link deployAgentcoreRun}; the params temp
|
|
546
|
+
* file (secret values off argv) is created here — 0600, removed after the run either way.
|
|
547
|
+
*/
|
|
548
|
+
async function runDeployAgentcore(params) {
|
|
549
|
+
const { agentDir, workspace, agentPrefix, name, modelAuth, authPath, channels, extraSecrets, selfSchedule } = params;
|
|
550
|
+
const { secrets, missingSecrets, needsModelCredential } = assembleSecrets({
|
|
551
|
+
modelAuth,
|
|
552
|
+
authFile: (await exists(authPath)) ? await readFile(authPath) : undefined,
|
|
553
|
+
channels,
|
|
554
|
+
extraSecrets,
|
|
555
|
+
env: deployEnvironment(agentDir, channels),
|
|
556
|
+
});
|
|
557
|
+
// The wake-alarm shared secret (container ↔ forwarder). Minted fresh each run — both sides receive
|
|
558
|
+
// the SAME parameter, so rotation is atomic; it never needs to be remembered locally.
|
|
559
|
+
if (selfSchedule)
|
|
560
|
+
secrets.FASTAGENT_WAKE_SECRET = crypto.randomUUID();
|
|
561
|
+
// The forwarder→runtime ingress secret: what makes an envelope the forwarder's rather than any IAM
|
|
562
|
+
// principal's. Minted fresh each run — both sides receive the SAME parameter, so rotation is atomic.
|
|
563
|
+
if (params.needsForwarder)
|
|
564
|
+
secrets.FASTAGENT_INGRESS_SECRET = crypto.randomUUID();
|
|
565
|
+
if (needsModelCredential) {
|
|
566
|
+
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
567
|
+
}
|
|
568
|
+
// The params temp dir holds the ONE file carrying secret values (file:// parameter-overrides —
|
|
569
|
+
// never argv); 0700/0600 and removed after the run, success or gate.
|
|
570
|
+
const paramsDir = await mkdtemp(join(tmpdir(), "fastagent-agentcore-"));
|
|
571
|
+
try {
|
|
572
|
+
const outcome = await deployAgentcoreRun({
|
|
573
|
+
name,
|
|
574
|
+
templatePath: `${agentPrefix}${TEMPLATE_FILE}`,
|
|
575
|
+
dockerfilePath: agentPrefix ? `${agentPrefix}Dockerfile` : undefined,
|
|
576
|
+
tag: new Date()
|
|
577
|
+
.toISOString()
|
|
578
|
+
.replace(/[-:.TZ]/g, "")
|
|
579
|
+
.slice(0, 14),
|
|
580
|
+
region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION,
|
|
581
|
+
secrets,
|
|
582
|
+
missingSecrets,
|
|
583
|
+
channels,
|
|
584
|
+
// Same predicate the template uses: the forwarder exists for route channels, cron schedules
|
|
585
|
+
// or selfSchedule — and it is what mints the state snapshot's presigned URLs.
|
|
586
|
+
needsForwarder: params.needsForwarder,
|
|
587
|
+
}, spawnRunner("aws", workspace), spawnRunner("docker", workspace), (m) => console.error(`[fastagent] ${m}`), async (content) => {
|
|
588
|
+
const path = join(paramsDir, "params.json");
|
|
589
|
+
await writeFile(path, content, { mode: 0o600 });
|
|
590
|
+
await chmod(path, 0o600);
|
|
591
|
+
return path;
|
|
592
|
+
}, async (bytes) => {
|
|
593
|
+
const path = join(paramsDir, "forwarder.zip");
|
|
594
|
+
await writeFile(path, bytes);
|
|
595
|
+
return path;
|
|
596
|
+
}, (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir) }));
|
|
597
|
+
if (!outcome.ok)
|
|
598
|
+
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
599
|
+
console.error(`[fastagent] deployed → ${outcome.runtimeArn}`);
|
|
600
|
+
if (outcome.url)
|
|
601
|
+
console.error(`[fastagent] webhook ingress → ${outcome.url}`);
|
|
602
|
+
console.error(`[fastagent] invoke: aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn ${outcome.runtimeArn} \\\n` +
|
|
603
|
+
` --runtime-session-id "my-conversation-000000000000000000" \\\n` +
|
|
604
|
+
` --payload '{"kind":"invoke","session":"cli","text":"hello"}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
await rm(paramsDir, { recursive: true, force: true });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
364
610
|
/**
|
|
365
611
|
* `deploy railway --run`: drive the railway CLI to completion. Mirrors {@link runDeployFly} — same
|
|
366
612
|
* credential carry (env key OR the OAuth auth.json as `FASTAGENT_AUTH_SEED`) via {@link assembleSecrets},
|
|
367
|
-
* same runner seam (spawned `railway`, cwd = the workspace so `railway up`'s upload is the
|
|
613
|
+
* same runner seam (spawned `railway`, cwd = the workspace so `railway up`'s upload is the whole workspace). The
|
|
368
614
|
* Railway-specific sequence (linked-check → init/add/volume when fresh → variables → up → domain →
|
|
369
615
|
* webhook) lives in {@link deployRailwayRun}; see there for why Railway differs from Fly.
|
|
370
616
|
*/
|
|
371
617
|
async function runDeployRailway(params) {
|
|
372
|
-
const {
|
|
373
|
-
const railway = spawnRunner("railway",
|
|
618
|
+
const { agentDir, workspace, name, modelAuth, authPath, channels, longConnectionChannels, extraSecrets, intoLinked, dockerfilePath, } = params;
|
|
619
|
+
const railway = spawnRunner("railway", workspace);
|
|
374
620
|
// Fail fast if the railway CLI is absent (spawn ENOENT → 127), with the install link.
|
|
375
621
|
if ((await railway(["--version"], { capture: true })).code === 127) {
|
|
376
622
|
failStartup(new Error(`railway CLI not found — install it: https://docs.railway.com/guides/cli, then re-run`));
|
|
@@ -381,13 +627,13 @@ async function runDeployRailway(params) {
|
|
|
381
627
|
channels,
|
|
382
628
|
longConnectionChannels,
|
|
383
629
|
extraSecrets,
|
|
384
|
-
env: deployEnvironment(
|
|
630
|
+
env: deployEnvironment(agentDir, channels),
|
|
385
631
|
});
|
|
386
632
|
// Model credential has its OWN remediation (login), distinct from a missing secret's (.env).
|
|
387
633
|
if (needsModelCredential) {
|
|
388
634
|
failStartup(new Error(`deploy stopped: no model credential — run \`fastagent login\`, or set a provider API key in .env, then re-run`));
|
|
389
635
|
}
|
|
390
|
-
const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, longConnectionChannels, intoLinked }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(
|
|
636
|
+
const outcome = await deployRailwayRun({ name, mountPath: "/data", secrets, missingSecrets, channels, longConnectionChannels, intoLinked, dockerfilePath }, railway, (m) => console.error(`[fastagent] ${m}`), (baseUrl) => registerTelegramWebhook(baseUrl), (baseUrl, kind) => registerFeishuWebhook(baseUrl, kind), (baseUrl) => registerSlackWebhook(baseUrl, { stateRoot: resolveStateRoot(agentDir) }));
|
|
391
637
|
if (!outcome.ok)
|
|
392
638
|
failStartup(new Error(`deploy stopped: ${outcome.gate}`));
|
|
393
639
|
console.error(`[fastagent] deployed → ${outcome.url}`);
|