@fastagent-sh/fastagent 0.14.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 +16 -6
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- 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 +35 -0
- package/dist/channels/context-buffer.js +103 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +15 -13
- package/dist/channels/feishu/context-buffer.js +62 -69
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +24 -3
- package/dist/channels/feishu/feishu.d.ts +27 -27
- package/dist/channels/feishu/feishu.js +318 -207
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +44 -79
- package/dist/channels/feishu/model.d.ts +2 -2
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +22 -8
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +27 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- 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 +37 -0
- package/dist/channels/feishu/setup-mode.js +28 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +16 -7
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +36 -3
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- 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 +115 -0
- package/dist/channels/preview-kit.js +184 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/parse.d.ts +38 -0
- package/dist/channels/slack/parse.js +124 -0
- package/dist/channels/slack/preview.d.ts +23 -0
- package/dist/channels/slack/preview.js +403 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +57 -0
- package/dist/channels/slack/slack-api.js +399 -0
- package/dist/channels/slack/slack.d.ts +51 -0
- package/dist/channels/slack/slack.js +496 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +22 -15
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +7 -0
- package/dist/channels/tasks.js +25 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +24 -10
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
- package/dist/channels/{feishu/text.js → text.js} +15 -1
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +8 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +30 -0
- package/dist/cli/add-feishu.js +366 -0
- package/dist/cli/add-slack.d.ts +11 -0
- package/dist/cli/add-slack.js +199 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.js +191 -60
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +563 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +12 -7
- package/dist/cli/commands/deploy.d.ts +3 -1
- package/dist/cli/commands/deploy.js +384 -82
- package/dist/cli/commands/dev.js +34 -26
- package/dist/cli/commands/fire.js +17 -18
- 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 +11 -9
- package/dist/cli/commands/login.js +48 -45
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.js +102 -39
- package/dist/cli/commands/tool.js +23 -18
- package/dist/cli/fail.d.ts +17 -2
- package/dist/cli/fail.js +25 -1
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +135 -56
- package/dist/cli/serve.d.ts +60 -20
- package/dist/cli/serve.js +252 -47
- package/dist/cli/shared.d.ts +40 -7
- package/dist/cli/shared.js +180 -38
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- 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 +3 -3
- package/dist/deploy/docker/plan.js +20 -23
- package/dist/deploy/fly/plan.d.ts +5 -1
- package/dist/deploy/fly/plan.js +39 -26
- package/dist/deploy/fly/run.d.ts +17 -5
- package/dist/deploy/fly/run.js +52 -7
- package/dist/deploy/preflight.d.ts +20 -8
- package/dist/deploy/preflight.js +267 -71
- package/dist/deploy/railway/plan.d.ts +10 -1
- package/dist/deploy/railway/plan.js +53 -20
- package/dist/deploy/railway/run.d.ts +13 -5
- package/dist/deploy/railway/run.js +22 -7
- package/dist/deploy/runner.d.ts +7 -3
- package/dist/deploy/runner.js +9 -3
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +11 -10
- package/dist/dev-supervisor.js +56 -50
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +92 -62
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -261
- package/dist/engines/pi/config.d.ts +20 -55
- package/dist/engines/pi/config.js +49 -121
- package/dist/engines/pi/create.d.ts +30 -10
- package/dist/engines/pi/create.js +33 -26
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +12 -1
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +59 -13
- package/dist/engines/pi/models.js +62 -24
- package/dist/engines/pi/open.d.ts +114 -0
- package/dist/engines/pi/open.js +127 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +10 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +25 -8
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- 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/observe.js +3 -0
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +6 -5
- package/dist/pi.js +4 -3
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +26 -9
- package/dist/scaffold/add-channel.js +175 -40
- 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 +5 -4
- 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 -3
- package/dist/schedule/wakeups.js +19 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +23 -12
- package/package.json +22 -7
- package/dist/channels/feishu/owned-threads.d.ts +0 -11
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-add-feishu.js +0 -223
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
- package/dist/engines/pi/workspace.d.ts +0 -56
- package/dist/engines/pi/workspace.js +0 -72
- 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
package/dist/deploy/container.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
|
|
3
3
|
* unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
|
|
4
|
-
* runs `fastagent start /app` on `$PORT` with
|
|
5
|
-
* conventions, no host coupling.
|
|
4
|
+
* runs `fastagent start /app` on `$PORT` with machinery under `$FASTAGENT_STATE_DIR` /
|
|
5
|
+
* `$FASTAGENT_SECRETS_DIR` — standard container conventions, no host coupling.
|
|
6
|
+
*
|
|
7
|
+
* ONE deploy semantic: bake the WORKSPACE as the image (`COPY . .` — what you see is what ships),
|
|
8
|
+
* with the agent at `./fastagent` inside it. The resolver in the image finds it exactly like dev does.
|
|
9
|
+
* Deps install under `/app/fastagent`, and the Dockerfile lives there too — namespaced so it never
|
|
10
|
+
* collides with one the workspace already owns.
|
|
6
11
|
*/
|
|
12
|
+
import { SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
|
|
7
13
|
/** Fixed first-line prefix of a fastagent-generated Dockerfile — the ONE source both the generator (the
|
|
8
14
|
* `head` below) and the "is this ours?" check (cli deploy's kept-Dockerfile apt warn) share, so they
|
|
9
15
|
* can't drift into detecting a marker the generator no longer writes. */
|
|
@@ -15,41 +21,13 @@ export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
|
15
21
|
export function isGeneratedDockerfile(content) {
|
|
16
22
|
return content.startsWith(GENERATED_DOCKERFILE_MARKER);
|
|
17
23
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
${apt}WORKDIR /app
|
|
26
|
-
RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
27
|
-
COPY . .
|
|
28
|
-
CMD ["fastagent", "start", "/app"]
|
|
29
|
-
`;
|
|
30
|
-
}
|
|
31
|
-
const isBun = input.runtime === "bun";
|
|
32
|
-
const base = isBun ? `oven/bun:${input.bunVersion ?? "1"}` : "node:22-slim";
|
|
33
|
-
const head = `${GENERATED_DOCKERFILE_MARKER}. Repo-as-workspace: the whole repo is the agent's cwd; the kit lives in ${kit}/.
|
|
34
|
-
# Only the KIT's deps are installed — the host repo's own deps are the agent's runtime concern (it can
|
|
35
|
-
# install them in its workspace when its job needs them). .git IS shipped: write-back needs it.
|
|
36
|
-
FROM ${base}
|
|
37
|
-
${apt}WORKDIR /app
|
|
38
|
-
`;
|
|
39
|
-
if (isBun) {
|
|
40
|
-
const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
|
|
41
|
-
return `${head}COPY ${kit}/package.json ${kit}/bun.lock* ./${kit}/
|
|
42
|
-
RUN cd ${kit} && ${install}
|
|
43
|
-
COPY . .
|
|
44
|
-
CMD ["sh", "-c", "cd ${kit} && bun run fastagent start /app"]
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
const install = input.hasLockfile ? "npm ci" : "npm install";
|
|
48
|
-
return `${head}COPY ${kit}/package.json ${kit}/package-lock.json* ./${kit}/
|
|
49
|
-
RUN cd ${kit} && ${install}
|
|
50
|
-
COPY . .
|
|
51
|
-
CMD ["./${kit}/node_modules/.bin/fastagent", "start", "/app"]
|
|
52
|
-
`;
|
|
24
|
+
/** First line of a generated `.dockerignore` — and, for the workspace-root one, the single artifact
|
|
25
|
+
* deploy writes outside the agent dir. The marker is what splits the two cases: ours is refreshed
|
|
26
|
+
* (regenerated under `--force`), yours is kept. */
|
|
27
|
+
const GENERATED_DOCKERIGNORE_MARKER = "# Generated by `fastagent deploy`";
|
|
28
|
+
/** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
|
|
29
|
+
export function isGeneratedDockerignore(content) {
|
|
30
|
+
return content.startsWith(GENERATED_DOCKERIGNORE_MARKER);
|
|
53
31
|
}
|
|
54
32
|
/** The apt layer (cached right after FROM). Debian default repos only. */
|
|
55
33
|
function aptLayer(packages) {
|
|
@@ -61,21 +39,46 @@ function aptLayer(packages) {
|
|
|
61
39
|
: "";
|
|
62
40
|
}
|
|
63
41
|
function dockerfile(input) {
|
|
42
|
+
// The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which
|
|
43
|
+
// is either a subdirectory of it or /app itself. `into` prefixes a path, `at` runs a command there.
|
|
44
|
+
const prefix = input.agentPrefix;
|
|
45
|
+
const into = (p) => `${prefix}${p}`;
|
|
46
|
+
// A build step that must run IN the agent dir (`RUN` already gets a shell from docker).
|
|
47
|
+
const at = (cmd) => (prefix ? `cd ${prefix.replace(/\/$/, "")} && ${cmd}` : cmd);
|
|
48
|
+
// The entrypoint. Only the BUN path needs a working directory (`bun run` resolves the script from the
|
|
49
|
+
// package.json beside it), and a `cd` there means a shell — with `exec`, or sh stays PID 1 and swallows
|
|
50
|
+
// the container's SIGTERM. The npm path needs no shell at all: the binary path is context-relative and
|
|
51
|
+
// `start /app` is absolute, so exec form is both correct and signal-clean. (A `cd` on that path was a
|
|
52
|
+
// bug: after `cd fastagent`, `./fastagent/node_modules/...` resolves one level too deep.)
|
|
53
|
+
const bunCmd = prefix
|
|
54
|
+
? `["sh", "-c", "cd ${prefix.replace(/\/$/, "")} && exec bun run fastagent start /app"]`
|
|
55
|
+
: `["bun", "run", "fastagent", "start", "/app"]`;
|
|
56
|
+
const layoutNote = prefix
|
|
57
|
+
? `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`
|
|
58
|
+
: `The directory IS the agent — it is also its own workspace.`;
|
|
64
59
|
// apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc.,
|
|
65
60
|
// which node:22-slim lacks. Debian default repos only — a package needing a custom repo (gh) or a
|
|
66
61
|
// different base is the operator's own Dockerfile (kept if present). deploy.apt is package-name-validated.
|
|
67
62
|
const apt = aptLayer(input.apt);
|
|
63
|
+
// PIN the agent into the image. The container re-resolves placement at `/app` (the baked workspace),
|
|
64
|
+
// and a workspace may hold SEVERAL agents — all of which ship, since the build context is the whole
|
|
65
|
+
// tree. Without this the image would pick by the container's own rules (the `fastagent`-named one, or
|
|
66
|
+
// nothing) instead of the agent this deploy was FOR: the artifact would depend on the builder's
|
|
67
|
+
// environment, which is the one thing it must never do. Baked whenever the agent sits inside the
|
|
68
|
+
// workspace, single agent or not — the value is a fact of this image, and asserting it means a build
|
|
69
|
+
// context that dropped the agent fails loudly rather than serving a sibling.
|
|
70
|
+
const pin = prefix ? `ENV FASTAGENT_AGENT=${prefix.replace(/\/$/, "")}\n` : "";
|
|
68
71
|
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent`
|
|
69
72
|
// from PATH. That needs npm + a global bin, which live on node:22-slim, NOT on oven/bun — so this path
|
|
70
73
|
// pins node:22-slim regardless of input.runtime (a stray bun lockfile with no package.json would
|
|
71
74
|
// otherwise select a bun base here and crash the `npm i -g` build). input.runtime is meaningful only
|
|
72
|
-
// for a code
|
|
75
|
+
// for a code agent, so the bun-vs-node base below is scoped to that path.
|
|
73
76
|
if (!input.hasPackageJson) {
|
|
74
|
-
return `${GENERATED_DOCKERFILE_MARKER}.
|
|
77
|
+
return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
|
|
75
78
|
# npm-based (a markdown/skills agent installs the pinned CLI globally; node:22-slim has npm).
|
|
76
79
|
FROM node:22-slim
|
|
77
80
|
${apt}WORKDIR /app
|
|
78
|
-
RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
81
|
+
${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
79
82
|
COPY . .
|
|
80
83
|
CMD ["fastagent", "start", "/app"]
|
|
81
84
|
`;
|
|
@@ -85,14 +88,16 @@ CMD ["fastagent", "start", "/app"]
|
|
|
85
88
|
const note = isBun
|
|
86
89
|
? "# Bun-based (packageManager: bun / a bun lockfile detected). fastagent runs under Bun (bun run)."
|
|
87
90
|
: "# npm-based — for pnpm/yarn, adapt the install line + the lockfile COPY (corepack enable, etc.).";
|
|
88
|
-
const head = `${GENERATED_DOCKERFILE_MARKER}.
|
|
91
|
+
const head = `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
|
|
89
92
|
${note}
|
|
90
93
|
FROM ${base}
|
|
91
94
|
${apt}WORKDIR /app
|
|
92
|
-
`;
|
|
95
|
+
${pin}`;
|
|
93
96
|
// Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on)
|
|
94
97
|
// needs its full toolchain — the build/check tools that live in devDependencies — to do its work, and
|
|
95
|
-
// we can't tell a repo-as-agent from a purpose-built
|
|
98
|
+
// we can't tell a repo-as-agent from a purpose-built agent, so the safe default keeps everything.
|
|
99
|
+
// Only the AGENT's deps are installed — the workspace's own deps are the agent's runtime concern
|
|
100
|
+
// (it can install them in its workspace when its job needs them).
|
|
96
101
|
if (isBun) {
|
|
97
102
|
// `--frozen-lockfile` needs bun.lock and hard-fails without it; fall back to a plain `bun install`
|
|
98
103
|
// (resolves at build time — not reproducible; the CLI warns to commit the lockfile).
|
|
@@ -100,13 +105,13 @@ ${apt}WORKDIR /app
|
|
|
100
105
|
// `bunx fastagent` would fall back to installing the npm package named `fastagent`, which is an
|
|
101
106
|
// unrelated third-party package (ours is the scoped @fastagent-sh/fastagent).
|
|
102
107
|
const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
|
|
103
|
-
return `${head}COPY package.json bun.lock*
|
|
104
|
-
RUN ${install}
|
|
108
|
+
return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ./${prefix}
|
|
109
|
+
RUN ${at(install)}
|
|
105
110
|
COPY . .
|
|
106
|
-
CMD
|
|
111
|
+
CMD ${bunCmd}
|
|
107
112
|
`;
|
|
108
113
|
}
|
|
109
|
-
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install`
|
|
114
|
+
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent);
|
|
110
115
|
// fall back to `npm install` when there is none so the build never breaks. Caveat: `npm install`
|
|
111
116
|
// resolves caret ranges at build time, so THIS branch is NOT reproducible — unlike the pinned
|
|
112
117
|
// `npm ci` (lockfile) and pinned global (markdown) paths. The CLI warns to commit a lockfile.
|
|
@@ -114,60 +119,59 @@ CMD ["bun", "run", "fastagent", "start", "/app"]
|
|
|
114
119
|
// `fastagent` when the dep is absent — an unrelated third-party package (ours is scoped). The local
|
|
115
120
|
// path fails fast and visibly instead.
|
|
116
121
|
const install = input.hasLockfile ? "npm ci" : "npm install";
|
|
117
|
-
return `${head}COPY package.json package-lock.json*
|
|
118
|
-
RUN ${install}
|
|
122
|
+
return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ./${prefix}
|
|
123
|
+
RUN ${at(install)}
|
|
119
124
|
COPY . .
|
|
120
|
-
CMD ["
|
|
125
|
+
CMD ["./${into("node_modules/.bin/fastagent")}", "start", "/app"]
|
|
121
126
|
`;
|
|
122
127
|
}
|
|
123
128
|
/** Patterns are RECURSIVE (`**/`) on purpose — dockerignore patterns are root-anchored (unlike
|
|
124
|
-
* .gitignore), and a
|
|
125
|
-
* build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the
|
|
126
|
-
* `.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
* .gitignore), and a baked workspace can hold nested projects: a bare `node_modules` would upload
|
|
130
|
+
* their build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the
|
|
131
|
+
* image. `.secrets`/`.state` are fastagent machinery — secrets travel through the host's secret
|
|
132
|
+
* store, state lives on the volume; neither may ever enter an image. `.cache` is generic hygiene
|
|
133
|
+
* (a baked project's own build cache), not a fastagent directory.
|
|
134
|
+
* `.git` is deliberately SHIPPED: the deployed agent's write-back (pull/commit/push) needs the
|
|
135
|
+
* repo's history+remote — the WYSIWYG bake's freshness/durability loop runs through git, driven by
|
|
136
|
+
* the agent itself, not by deploy machinery. Shipping `.git` is only half of that loop: preflight
|
|
137
|
+
* bakes the git BINARY into the generated image iff the workspace ships a `.git` (the `shipsGit`
|
|
138
|
+
* fact); a non-git workspace that still needs git declares config.deploy.apt. */
|
|
139
|
+
const dockerignore = (input) => DOCKERIGNORE_BASE +
|
|
140
|
+
(input.machineryPaths ?? [])
|
|
141
|
+
// Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state`
|
|
142
|
+
// and anything inside them. What remains is a relocated dir whose NAME those rules cannot match.
|
|
143
|
+
.filter((p) => [`${input.agentPrefix}${SECRETS_DIRNAME}`, `${input.agentPrefix}${STATE_DIRNAME}`].every((covered) => p !== covered && !p.startsWith(`${covered}/`)))
|
|
144
|
+
.map((p) => `# resolved machinery path (FASTAGENT_SECRETS_DIR / FASTAGENT_AUTH_PATH / FASTAGENT_STATE_DIR)\n/${p}\n`)
|
|
145
|
+
.join("");
|
|
146
|
+
const DOCKERIGNORE_BASE = `${GENERATED_DOCKERIGNORE_MARKER}. Delete this line to take ownership (deploy then keeps your file).
|
|
147
|
+
**/node_modules
|
|
148
|
+
**/${SECRETS_DIRNAME}
|
|
149
|
+
**/${STATE_DIRNAME}
|
|
150
|
+
**/.cache
|
|
129
151
|
**/.env
|
|
130
152
|
**/.env.*
|
|
131
153
|
!**/.env.example
|
|
132
154
|
**/*.log
|
|
133
|
-
# .git is
|
|
134
|
-
# (
|
|
135
|
-
.git
|
|
136
|
-
`;
|
|
137
|
-
/** The kit-layout ignore: `.git` is deliberately NOT excluded (write-back wants it — though whether it
|
|
138
|
-
* survives is host-CLI-dependent; see containerArtifacts). Patterns are RECURSIVE (`**/`) on purpose —
|
|
139
|
-
* dockerignore patterns are root-anchored (unlike .gitignore), so a bare `node_modules` would NOT
|
|
140
|
-
* exclude `agent/node_modules`: the build machine's kit deps (macOS binaries!) would be uploaded by
|
|
141
|
-
* `COPY . .` and clobber the image's freshly-installed linux ones. */
|
|
142
|
-
const KIT_DOCKERIGNORE = `**/node_modules
|
|
143
|
-
**/.fastagent
|
|
144
|
-
**/.env
|
|
145
|
-
**/.env.*
|
|
146
|
-
!**/.env.example
|
|
147
|
-
**/*.log
|
|
148
|
-
# .git is DELIBERATELY shipped: the agent's write-back (commit/push) needs the repo's history+remote.
|
|
155
|
+
# .git is deliberately shipped: the agent can pull to freshen content and push its work back
|
|
156
|
+
# (the generated image installs the git binary when this directory ships a .git; otherwise
|
|
157
|
+
# add deploy.apt ["git"]). For a smaller image with no git needs, add a ".git" line here.
|
|
149
158
|
`;
|
|
150
159
|
/**
|
|
151
|
-
* The Dockerfile + ignore artifacts — spread into any host's artifact list.
|
|
152
|
-
* ContainerInput.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* per-Dockerfile `
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
+
* The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
|
|
161
|
+
* {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
|
|
162
|
+
* colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
|
|
163
|
+
* the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
|
|
164
|
+
* inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own context packers
|
|
165
|
+
* reliably read (kept if the workspace already has one — preflight then checks the machinery/secret
|
|
166
|
+
* excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
|
|
167
|
+
* builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
|
|
168
|
+
* only at deploy time — without it the host CLI's packer would bake `.secrets/` into the image.
|
|
160
169
|
*/
|
|
161
170
|
export function containerArtifacts(input) {
|
|
162
|
-
|
|
163
|
-
return [
|
|
164
|
-
{ path: `${input.kitDir}/Dockerfile`, content: kitDockerfile(input, input.kitDir) },
|
|
165
|
-
{ path: ".dockerignore", content: KIT_DOCKERIGNORE },
|
|
166
|
-
{ path: `${input.kitDir}/Dockerfile.dockerignore`, content: KIT_DOCKERIGNORE },
|
|
167
|
-
];
|
|
168
|
-
}
|
|
171
|
+
const ignore = dockerignore(input);
|
|
169
172
|
return [
|
|
170
|
-
{ path:
|
|
171
|
-
{ path: ".dockerignore", content:
|
|
173
|
+
{ path: `${input.agentPrefix}Dockerfile`, content: dockerfile(input) },
|
|
174
|
+
{ path: ".dockerignore", content: ignore },
|
|
175
|
+
{ path: `${input.agentPrefix}Dockerfile.dockerignore`, content: ignore },
|
|
172
176
|
];
|
|
173
177
|
}
|
|
@@ -15,6 +15,8 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
15
15
|
modelAuth: string | undefined;
|
|
16
16
|
/** Known channels contribute their environment-variable names and webhook registration. */
|
|
17
17
|
channels: ChannelKind[];
|
|
18
|
+
/** All long-connection channel basenames, including custom channels. */
|
|
19
|
+
longConnectionChannels?: string[];
|
|
18
20
|
/** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
|
|
19
21
|
tunnel: boolean;
|
|
20
22
|
/** Extra environment-variable names declared in config.deploy.secrets. */
|
|
@@ -23,13 +25,11 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
23
25
|
export interface DockerPlan {
|
|
24
26
|
/** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
|
|
25
27
|
artifacts: Artifact[];
|
|
26
|
-
/** Compose file path relative to the workspace root (
|
|
28
|
+
/** Compose file path relative to the workspace root (under the agent prefix). */
|
|
27
29
|
composePath: string;
|
|
28
30
|
/** Ordered local build/run/operate instructions. */
|
|
29
31
|
runbook: string[];
|
|
30
32
|
}
|
|
31
|
-
export declare const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
32
|
-
export declare const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
|
|
33
33
|
export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
34
34
|
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
35
35
|
export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { containerArtifacts } from "../container.js";
|
|
2
2
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
3
|
const MOUNT = "/data";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
5
|
+
const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
|
|
6
6
|
export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
7
7
|
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
8
8
|
export const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
|
|
@@ -29,6 +29,7 @@ export function dockerWebhookPaths(channels) {
|
|
|
29
29
|
const path = {
|
|
30
30
|
github: "/webhook",
|
|
31
31
|
telegram: "/telegram",
|
|
32
|
+
slack: "/slack",
|
|
32
33
|
feishu: "/feishu",
|
|
33
34
|
lark: "/lark",
|
|
34
35
|
};
|
|
@@ -38,23 +39,16 @@ export function dockerWebhookPaths(channels) {
|
|
|
38
39
|
function composeInterpolation(name) {
|
|
39
40
|
return `\${${name}:-}`;
|
|
40
41
|
}
|
|
41
|
-
/** Relative path from the namespaced Compose file's directory back to the workspace/build root. */
|
|
42
|
-
function buildContext(kitDir) {
|
|
43
|
-
if (!kitDir)
|
|
44
|
-
return ".";
|
|
45
|
-
return kitDir
|
|
46
|
-
.split("/")
|
|
47
|
-
.map(() => "..")
|
|
48
|
-
.join("/");
|
|
49
|
-
}
|
|
50
42
|
function composeYaml(input) {
|
|
51
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
43
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
|
|
52
44
|
// Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
|
|
53
45
|
// it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
|
|
54
46
|
const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
|
|
55
47
|
const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
// Compose sits beside the Dockerfile, under the agent prefix; the build context is always the
|
|
49
|
+
// WORKSPACE, so it climbs back out of the prefix (`..` per level, `.` when there is none).
|
|
50
|
+
const context = input.agentPrefix ? ".." : ".";
|
|
51
|
+
const dockerfile = `${input.agentPrefix}Dockerfile`;
|
|
58
52
|
const tunnelService = input.tunnel
|
|
59
53
|
? `
|
|
60
54
|
# Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
|
|
@@ -89,7 +83,9 @@ services:
|
|
|
89
83
|
- "127.0.0.1:${input.port}:${input.port}"
|
|
90
84
|
environment:
|
|
91
85
|
PORT: "${input.port}"
|
|
92
|
-
|
|
86
|
+
# Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets.
|
|
87
|
+
FASTAGENT_STATE_DIR: "${MOUNT}/.state"
|
|
88
|
+
FASTAGENT_SECRETS_DIR: "${MOUNT}/.secrets"
|
|
93
89
|
${secretEnv}
|
|
94
90
|
volumes:
|
|
95
91
|
- state:${MOUNT}
|
|
@@ -101,20 +97,21 @@ volumes:
|
|
|
101
97
|
}
|
|
102
98
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
103
99
|
export function planDockerDeploy(input) {
|
|
104
|
-
const composePath =
|
|
100
|
+
const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
|
|
105
101
|
const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
|
|
106
102
|
const compose = `docker compose -f ${composePath}`;
|
|
107
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
103
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
|
|
108
104
|
const required = secrets.filter((secret) => secret.required);
|
|
109
105
|
const optional = secrets.filter((secret) => !secret.required);
|
|
110
|
-
const
|
|
106
|
+
const routeChannels = input.channels.filter((kind) => !input.longConnectionChannels?.includes(kind));
|
|
107
|
+
const paths = dockerWebhookPaths(routeChannels);
|
|
111
108
|
const runbook = [
|
|
112
109
|
`# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
|
|
113
110
|
`# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
|
|
114
111
|
`# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
|
|
115
112
|
];
|
|
116
113
|
if (required.length > 0) {
|
|
117
|
-
runbook.push(`# Required environment values (put them in the
|
|
114
|
+
runbook.push(`# Required environment values (put them in the agent's .secrets/.env or export them):`, ...required.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
118
115
|
}
|
|
119
116
|
if (optional.length > 0) {
|
|
120
117
|
runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
@@ -124,12 +121,12 @@ export function planDockerDeploy(input) {
|
|
|
124
121
|
? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
|
|
125
122
|
: `# Model auth: local source is "${input.modelAuth}" — \`--run\` carries auth.json as FASTAGENT_AUTH_SEED.`, `# For a manual Compose run, provide a provider API key or set FASTAGENT_AUTH_SEED yourself.`);
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
runbook.push(input.agentPrefix
|
|
125
|
+
? `# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`
|
|
126
|
+
: `# Run from this directory — it is both the agent and its workspace.`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
|
|
130
127
|
runbook.push(``, `# Build/create/reconcile the service. Re-running this is the complete local redeploy; state stays`, `# in the Compose volume mounted at ${MOUNT}.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: also deletes auth, sessions, and channel state`);
|
|
131
128
|
if (input.tunnel) {
|
|
132
|
-
runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `#
|
|
129
|
+
runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; GitHub/manual Slack print console URLs. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
|
|
133
130
|
}
|
|
134
131
|
else if (paths.length > 0) {
|
|
135
132
|
runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then configure the`, `# detected default endpoint(s): ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead.`);
|
|
@@ -28,8 +28,10 @@ export interface FlyPlanInput extends ContainerInput {
|
|
|
28
28
|
* `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
|
|
29
29
|
*/
|
|
30
30
|
modelAuth: string | undefined;
|
|
31
|
-
/**
|
|
31
|
+
/** Known first-party channels — each contributes its secret metadata + webhook step. */
|
|
32
32
|
channels: ChannelKind[];
|
|
33
|
+
/** All long-connection channel basenames, including custom channels — require one running machine. */
|
|
34
|
+
longConnectionChannels?: string[];
|
|
33
35
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
34
36
|
extraSecrets?: string[];
|
|
35
37
|
/** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
|
|
@@ -47,6 +49,8 @@ export interface FlyPlan {
|
|
|
47
49
|
/** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
|
|
48
50
|
runbook: string[];
|
|
49
51
|
}
|
|
52
|
+
/** Did fastagent generate this `fly.toml`? */
|
|
53
|
+
export declare function isGeneratedFlyToml(content: string): boolean;
|
|
50
54
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
51
55
|
export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
|
|
52
56
|
/**
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { containerArtifacts } from "../container.js";
|
|
2
2
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
|
-
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers) {
|
|
3
|
+
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
|
|
4
4
|
// min_machines_running: 1 (keep one up) when a github channel is present, TIME triggers exist, OR the
|
|
5
5
|
// operator opted out of scale-to-zero. GitHub's is a SAFETY default — its fire-and-forget turns have no
|
|
6
6
|
// replay, so scaling to zero could drop an in-flight review. Time triggers (schedules/wake) have no
|
|
@@ -10,20 +10,23 @@ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTrigger
|
|
|
10
10
|
? ` min_machines_running = 1 # github turns have no replay — don't scale to zero (an in-flight review would be lost)`
|
|
11
11
|
: hasTimeTriggers
|
|
12
12
|
? ` min_machines_running = 1 # schedules/wake-ups need a running machine (no external wake-up for a cron instant)`
|
|
13
|
-
:
|
|
14
|
-
? ` min_machines_running = 1 #
|
|
15
|
-
:
|
|
13
|
+
: hasLongConnectionChannel
|
|
14
|
+
? ` min_machines_running = 1 # long-connection channel needs a running machine (cannot wake from zero)`
|
|
15
|
+
: !scaleToZero
|
|
16
|
+
? ` min_machines_running = 1 # kept running (--no-scale-to-zero)`
|
|
17
|
+
: ` min_machines_running = 0 # scale to zero`;
|
|
16
18
|
const stopLine = autostop === "stop"
|
|
17
19
|
? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
|
|
18
20
|
: ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
|
|
19
|
-
return
|
|
21
|
+
return `${GENERATED_FLY_TOML_MARKER}. Edit freely — it is not regenerated unless you pass --force.
|
|
20
22
|
app = "${appName}"
|
|
21
23
|
primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
22
24
|
|
|
23
25
|
[build]
|
|
24
26
|
|
|
25
27
|
[env]
|
|
26
|
-
FASTAGENT_STATE_DIR = "/data"
|
|
28
|
+
FASTAGENT_STATE_DIR = "/data/.state" # mutable machine state — sessions, channel state, schedule
|
|
29
|
+
FASTAGENT_SECRETS_DIR = "/data/.secrets" # seeded (and rotated) credentials — must persist across restarts
|
|
27
30
|
PORT = "${port}"
|
|
28
31
|
|
|
29
32
|
[http_service]
|
|
@@ -35,24 +38,31 @@ ${min}
|
|
|
35
38
|
|
|
36
39
|
[mounts]
|
|
37
40
|
source = "data"
|
|
38
|
-
destination = "/data" #
|
|
41
|
+
destination = "/data" # .state + .secrets — persists across stop/suspend/redeploy
|
|
39
42
|
|
|
40
43
|
[[vm]]
|
|
41
44
|
size = "shared-cpu-1x"
|
|
42
45
|
memory = "512mb" # suspend is not recommended above 2 GB
|
|
43
46
|
`;
|
|
44
47
|
}
|
|
48
|
+
/** First line of a generated `fly.toml`, and the predicate that reads it back. Ownership is what decides
|
|
49
|
+
* whether `--force` may reset the file (app/region/vm state a user tuned by hand is theirs). */
|
|
50
|
+
const GENERATED_FLY_TOML_MARKER = "# Generated by `fastagent deploy fly`";
|
|
51
|
+
/** Did fastagent generate this `fly.toml`? */
|
|
52
|
+
export function isGeneratedFlyToml(content) {
|
|
53
|
+
return content.startsWith(GENERATED_FLY_TOML_MARKER);
|
|
54
|
+
}
|
|
45
55
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
46
56
|
export function planFlyDeploy(input) {
|
|
47
|
-
const { appName, port, modelAuth, channels
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
const flyTomlPath =
|
|
57
|
+
const { appName, port, modelAuth, channels } = input;
|
|
58
|
+
// Artifacts sit under the agent prefix, so a NESTED agent never touches the workspace's own deploy
|
|
59
|
+
// files; the runbook passes explicit -c/--dockerfile flags either way (unambiguous across flyctl
|
|
60
|
+
// versions — no reliance on config-relative path resolution).
|
|
61
|
+
const flyTomlPath = `${input.agentPrefix}fly.toml`;
|
|
52
62
|
const artifacts = [
|
|
53
63
|
{
|
|
54
64
|
path: flyTomlPath,
|
|
55
|
-
content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers),
|
|
65
|
+
content: flyToml(appName, port, channels.includes("github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, (input.longConnectionChannels?.length ?? 0) > 0),
|
|
56
66
|
},
|
|
57
67
|
...containerArtifacts(input),
|
|
58
68
|
];
|
|
@@ -60,12 +70,10 @@ export function planFlyDeploy(input) {
|
|
|
60
70
|
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
61
71
|
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
62
72
|
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
63
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
73
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
|
|
64
74
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
65
75
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
66
|
-
const deployCmd =
|
|
67
|
-
? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
|
|
68
|
-
: `fly deploy --app ${appName}`;
|
|
76
|
+
const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
|
|
69
77
|
const runbook = [
|
|
70
78
|
`# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
|
|
71
79
|
`# Prereqs: flyctl installed (https://fly.io/docs/flyctl/install) and \`fly auth login\`.`,
|
|
@@ -74,7 +82,7 @@ export function planFlyDeploy(input) {
|
|
|
74
82
|
`# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
|
|
75
83
|
`# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
|
|
76
84
|
`fly apps create ${appName}`,
|
|
77
|
-
`# volume persists
|
|
85
|
+
`# volume persists /data/.state (sessions, channel state) + /data/.secrets (seeded auth) across stop/suspend/redeploy.`,
|
|
78
86
|
`# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
|
|
79
87
|
`# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
|
|
80
88
|
`fly volumes create data --app ${appName} --region <region> --size 1`,
|
|
@@ -85,20 +93,22 @@ export function planFlyDeploy(input) {
|
|
|
85
93
|
if (optionalSecrets.length > 0) {
|
|
86
94
|
runbook.push(``, `# Optional secrets — set only when the matching feature is configured:`, ...optionalSecrets.map((s) => `# ${s.name}: ${s.hint}`), `# fly secrets set --app ${appName} ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
87
95
|
}
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, ...(input.agentPrefix
|
|
97
|
+
? [`# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`]
|
|
98
|
+
: []), `# Run this from ${input.agentPrefix ? "the workspace root" : "this directory"}:`, deployCmd);
|
|
99
|
+
if (input.shipsGit) {
|
|
100
|
+
runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. Freshness/durability run through git, driven`, `# by the agent itself: .git ships in the image (see .dockerignore) and git is baked in, so the agent`, `# can pull to freshen content and commit/push its work back (creds ride config.deploy.secrets; the`, `# POLICY — push vs PR, identity — lives in persona.md). CAVEAT: whether .git survives the upload is`, `# host-CLI-dependent — verify \`git status\` on the box; if missing, have the agent clone instead.`, `# Un-pushed changes on the box never survive a redeploy; durability lives in git.`);
|
|
90
101
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
runbook.push(``, `# Write-back mechanics: git ships in the image and GH_TOKEN-style creds ride config.deploy.secrets;`, `# the POLICY (push vs PR, identity, remote) lives in persona.md. CAVEAT — whether .git survives is`, `# host-CLI-dependent (some context packers strip it from the upload): verify \`git status\` on the`, `# box after the first deploy; if it is missing, have the agent \`git clone\` its repo in the`, `# workspace instead (same token). Un-pushed changes never survive a redeploy — the image is a`, `# snapshot; durability lives in git.`);
|
|
102
|
+
else {
|
|
103
|
+
runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. No .git here, so no history ships and the`, `# generated image does not install git — changes on the box are ephemeral and never survive a`, `# redeploy. If the agent should clone/push repos as part of its work, add deploy: { apt: ["git"] }.`);
|
|
94
104
|
}
|
|
95
105
|
// Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
|
|
96
106
|
// credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
|
|
97
107
|
// `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
|
|
98
108
|
if (!isEnvKey(modelAuth)) {
|
|
99
109
|
runbook.push(``, modelAuth === undefined
|
|
100
|
-
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/auth.json), or \`--run\` carries it automatically.`
|
|
101
|
-
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json on the
|
|
110
|
+
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
111
|
+
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
|
|
102
112
|
}
|
|
103
113
|
// The fastagent-only post-step: point each channel's webhook at the live URL. Only fastagent knows the routes.
|
|
104
114
|
// The URLs below assume each channel's DEFAULT route key (POST /telegram, POST /webhook). Reading the
|
|
@@ -111,8 +121,11 @@ export function planFlyDeploy(input) {
|
|
|
111
121
|
if (channels.includes("github")) {
|
|
112
122
|
post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
113
123
|
}
|
|
124
|
+
if (channels.includes("slack")) {
|
|
125
|
+
post.push(`# After deploy — set Slack Event Subscriptions → Request URL. Path assumes POST /slack;`, `# Slack verifies the running endpoint with a challenge:`, `# Request URL = https://${appName}.fly.dev/slack`, `# Ensure OAuth scopes + message.* subscriptions match the groupBehavior in channels/slack.ts.`);
|
|
126
|
+
}
|
|
114
127
|
for (const kind of ["feishu", "lark"]) {
|
|
115
|
-
if (!channels.includes(kind))
|
|
128
|
+
if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
|
|
116
129
|
continue;
|
|
117
130
|
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
118
131
|
post.push(`# After deploy — set the ${label} event Request URL (developer console → Events & Callbacks).`, `# Path assumes the default route (POST /${kind}); the app must be RUNNING when you save (the console`, `# verifies the URL with a challenge):`, `# Request URL = https://${appName}.fly.dev/${kind}`);
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -26,6 +26,14 @@ import type { CliRunner } from "../runner.ts";
|
|
|
26
26
|
* is never overwritten by the stale seed, so a box that ran its own OAuth refresh is not rolled back.
|
|
27
27
|
*/
|
|
28
28
|
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
31
|
+
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
32
|
+
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
33
|
+
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
34
|
+
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
35
|
+
*/
|
|
36
|
+
export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
|
|
29
37
|
export interface FlyRunPlan {
|
|
30
38
|
appName: string;
|
|
31
39
|
region: string;
|
|
@@ -35,8 +43,12 @@ export interface FlyRunPlan {
|
|
|
35
43
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
36
44
|
missingSecrets: string[];
|
|
37
45
|
channels: ChannelKind[];
|
|
38
|
-
|
|
46
|
+
longConnectionChannels?: string[];
|
|
47
|
+
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
|
|
39
48
|
flyConfig: string;
|
|
49
|
+
/** Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context —
|
|
50
|
+
* flyctl would otherwise resolve it relative to the config's own directory). */
|
|
51
|
+
dockerfile: string;
|
|
40
52
|
}
|
|
41
53
|
/** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
|
|
42
54
|
export type FlyRunOutcome = {
|
|
@@ -46,9 +58,9 @@ export type FlyRunOutcome = {
|
|
|
46
58
|
gate: string;
|
|
47
59
|
};
|
|
48
60
|
/**
|
|
49
|
-
* Run the deploy through `fly`. `log` reports progress;
|
|
50
|
-
*
|
|
51
|
-
*
|
|
61
|
+
* Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
|
|
62
|
+
* perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
|
|
63
|
+
* to the host). Absent, the manual console
|
|
52
64
|
* instruction is printed. Every gate is fail-visible.
|
|
53
65
|
*/
|
|
54
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;
|
|
66
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registerTelegram: (baseUrl: string) => Promise<RegistrationOutcome>, registerFeishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>, registerSlack?: (baseUrl: string) => Promise<RegistrationOutcome>): Promise<FlyRunOutcome>;
|