@fastagent-sh/fastagent 0.15.0 → 0.16.1
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 +14 -4
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -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 +266 -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 +14 -1
- package/dist/cli/commands/deploy.js +330 -78
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +38 -25
- 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.d.ts +1 -0
- package/dist/cli/commands/start.js +101 -37
- package/dist/cli/commands/tool.js +30 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +66 -36
- package/dist/cli/serve.d.ts +52 -8
- package/dist/cli/serve.js +133 -32
- package/dist/cli/shared.d.ts +21 -2
- package/dist/cli/shared.js +44 -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 +235 -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 +12 -48
- package/dist/engines/pi/config.js +29 -110
- package/dist/engines/pi/create.d.ts +32 -24
- package/dist/engines/pi/create.js +47 -23
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- 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} +31 -25
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +18 -13
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/tool.js +4 -0
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +4 -2
- package/dist/host/node.js +2 -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/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +2 -2
- 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 +7 -4
- 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
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
|
}
|
|
@@ -25,7 +25,7 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
25
25
|
export interface DockerPlan {
|
|
26
26
|
/** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
|
|
27
27
|
artifacts: Artifact[];
|
|
28
|
-
/** Compose file path relative to the workspace root (
|
|
28
|
+
/** Compose file path relative to the workspace root (under the agent prefix). */
|
|
29
29
|
composePath: string;
|
|
30
30
|
/** Ordered local build/run/operate instructions. */
|
|
31
31
|
runbook: string[];
|
|
@@ -39,23 +39,16 @@ export function dockerWebhookPaths(channels) {
|
|
|
39
39
|
function composeInterpolation(name) {
|
|
40
40
|
return `\${${name}:-}`;
|
|
41
41
|
}
|
|
42
|
-
/** Relative path from the namespaced Compose file's directory back to the workspace/build root. */
|
|
43
|
-
function buildContext(kitDir) {
|
|
44
|
-
if (!kitDir)
|
|
45
|
-
return ".";
|
|
46
|
-
return kitDir
|
|
47
|
-
.split("/")
|
|
48
|
-
.map(() => "..")
|
|
49
|
-
.join("/");
|
|
50
|
-
}
|
|
51
42
|
function composeYaml(input) {
|
|
52
43
|
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
|
|
53
44
|
// Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
|
|
54
45
|
// it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
|
|
55
46
|
const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
|
|
56
47
|
const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
|
|
57
|
-
|
|
58
|
-
|
|
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`;
|
|
59
52
|
const tunnelService = input.tunnel
|
|
60
53
|
? `
|
|
61
54
|
# Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
|
|
@@ -90,7 +83,9 @@ services:
|
|
|
90
83
|
- "127.0.0.1:${input.port}:${input.port}"
|
|
91
84
|
environment:
|
|
92
85
|
PORT: "${input.port}"
|
|
93
|
-
|
|
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"
|
|
94
89
|
${secretEnv}
|
|
95
90
|
volumes:
|
|
96
91
|
- state:${MOUNT}
|
|
@@ -102,7 +97,7 @@ volumes:
|
|
|
102
97
|
}
|
|
103
98
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
104
99
|
export function planDockerDeploy(input) {
|
|
105
|
-
const composePath =
|
|
100
|
+
const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
|
|
106
101
|
const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
|
|
107
102
|
const compose = `docker compose -f ${composePath}`;
|
|
108
103
|
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets, input.longConnectionChannels);
|
|
@@ -116,7 +111,7 @@ export function planDockerDeploy(input) {
|
|
|
116
111
|
`# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
|
|
117
112
|
];
|
|
118
113
|
if (required.length > 0) {
|
|
119
|
-
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}`));
|
|
120
115
|
}
|
|
121
116
|
if (optional.length > 0) {
|
|
122
117
|
runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
@@ -126,9 +121,9 @@ export function planDockerDeploy(input) {
|
|
|
126
121
|
? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
|
|
127
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.`);
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.`);
|
|
132
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`);
|
|
133
128
|
if (input.tunnel) {
|
|
134
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`);
|
|
@@ -49,6 +49,8 @@ export interface FlyPlan {
|
|
|
49
49
|
/** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
|
|
50
50
|
runbook: string[];
|
|
51
51
|
}
|
|
52
|
+
/** Did fastagent generate this `fly.toml`? */
|
|
53
|
+
export declare function isGeneratedFlyToml(content: string): boolean;
|
|
52
54
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
53
55
|
export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
|
|
54
56
|
/**
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -18,14 +18,15 @@ function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTrigger
|
|
|
18
18
|
const stopLine = autostop === "stop"
|
|
19
19
|
? ` auto_stop_machines = "stop" # stop on idle (cold start on the next webhook)`
|
|
20
20
|
: ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
|
|
21
|
-
return
|
|
21
|
+
return `${GENERATED_FLY_TOML_MARKER}. Edit freely — it is not regenerated unless you pass --force.
|
|
22
22
|
app = "${appName}"
|
|
23
23
|
primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
24
24
|
|
|
25
25
|
[build]
|
|
26
26
|
|
|
27
27
|
[env]
|
|
28
|
-
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
|
|
29
30
|
PORT = "${port}"
|
|
30
31
|
|
|
31
32
|
[http_service]
|
|
@@ -37,20 +38,27 @@ ${min}
|
|
|
37
38
|
|
|
38
39
|
[mounts]
|
|
39
40
|
source = "data"
|
|
40
|
-
destination = "/data" #
|
|
41
|
+
destination = "/data" # .state + .secrets — persists across stop/suspend/redeploy
|
|
41
42
|
|
|
42
43
|
[[vm]]
|
|
43
44
|
size = "shared-cpu-1x"
|
|
44
45
|
memory = "512mb" # suspend is not recommended above 2 GB
|
|
45
46
|
`;
|
|
46
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
|
+
}
|
|
47
55
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
48
56
|
export function planFlyDeploy(input) {
|
|
49
|
-
const { appName, port, modelAuth, channels
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
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`;
|
|
54
62
|
const artifacts = [
|
|
55
63
|
{
|
|
56
64
|
path: flyTomlPath,
|
|
@@ -65,9 +73,7 @@ export function planFlyDeploy(input) {
|
|
|
65
73
|
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets, input.longConnectionChannels);
|
|
66
74
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
67
75
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
68
|
-
const deployCmd =
|
|
69
|
-
? `fly deploy . --config ${kitDir}/fly.toml --dockerfile ${kitDir}/Dockerfile --app ${appName}`
|
|
70
|
-
: `fly deploy --app ${appName}`;
|
|
76
|
+
const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
|
|
71
77
|
const runbook = [
|
|
72
78
|
`# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
|
|
73
79
|
`# Prereqs: flyctl installed (https://fly.io/docs/flyctl/install) and \`fly auth login\`.`,
|
|
@@ -76,7 +82,7 @@ export function planFlyDeploy(input) {
|
|
|
76
82
|
`# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
|
|
77
83
|
`# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
|
|
78
84
|
`fly apps create ${appName}`,
|
|
79
|
-
`# volume persists
|
|
85
|
+
`# volume persists /data/.state (sessions, channel state) + /data/.secrets (seeded auth) across stop/suspend/redeploy.`,
|
|
80
86
|
`# <region> MUST equal primary_region in fly.toml (a volume in another region can't mount) — fly.toml`,
|
|
81
87
|
`# is the single source for the region; skip this if the volume exists (fly volumes list --app ${appName}):`,
|
|
82
88
|
`fly volumes create data --app ${appName} --region <region> --size 1`,
|
|
@@ -87,20 +93,22 @@ export function planFlyDeploy(input) {
|
|
|
87
93
|
if (optionalSecrets.length > 0) {
|
|
88
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(" ")}`);
|
|
89
95
|
}
|
|
90
|
-
|
|
91
|
-
|
|
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.`);
|
|
92
101
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
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"] }.`);
|
|
96
104
|
}
|
|
97
105
|
// Model-auth guidance: an env key becomes a secret above. Otherwise the plan can't read the local
|
|
98
106
|
// credential's VALUE to set as a secret — true for OAuth AND a stored API key (both are
|
|
99
107
|
// `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
|
|
100
108
|
if (!isEnvKey(modelAuth)) {
|
|
101
109
|
runbook.push(``, modelAuth === undefined
|
|
102
|
-
? `# 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.`
|
|
103
|
-
: `# 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.`);
|
|
104
112
|
}
|
|
105
113
|
// The fastagent-only post-step: point each channel's webhook at the live URL. Only fastagent knows the routes.
|
|
106
114
|
// The URLs below assume each channel's DEFAULT route key (POST /telegram, POST /webhook). Reading the
|
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;
|
|
@@ -36,8 +44,11 @@ export interface FlyRunPlan {
|
|
|
36
44
|
missingSecrets: string[];
|
|
37
45
|
channels: ChannelKind[];
|
|
38
46
|
longConnectionChannels?: string[];
|
|
39
|
-
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace
|
|
47
|
+
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
|
|
40
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;
|
|
41
52
|
}
|
|
42
53
|
/** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
|
|
43
54
|
export type FlyRunOutcome = {
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -7,6 +7,26 @@ import { registrationGate } from "../registration-gate.js";
|
|
|
7
7
|
export function authSeedBytes(seed, fileExists) {
|
|
8
8
|
return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
12
|
+
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
13
|
+
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
14
|
+
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
15
|
+
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
16
|
+
*/
|
|
17
|
+
export function collectAuthSeed(env) {
|
|
18
|
+
const first = env.FASTAGENT_AUTH_SEED;
|
|
19
|
+
if (!first)
|
|
20
|
+
return undefined;
|
|
21
|
+
let seed = first;
|
|
22
|
+
for (let i = 2;; i++) {
|
|
23
|
+
const part = env[`FASTAGENT_AUTH_SEED_${i}`];
|
|
24
|
+
if (!part)
|
|
25
|
+
break;
|
|
26
|
+
seed += part;
|
|
27
|
+
}
|
|
28
|
+
return seed;
|
|
29
|
+
}
|
|
10
30
|
/** Whether a `fly … list --json` array contains an object named `name` (Fly capitalizes `Name`; accept both). */
|
|
11
31
|
function listHasName(stdout, name) {
|
|
12
32
|
try {
|
|
@@ -74,9 +94,23 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
|
|
|
74
94
|
return gate("`fly secrets import` failed — see the flyctl output above");
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
|
-
// 6. Deploy — remote builder (no local Docker), one machine.
|
|
97
|
+
// 6. Deploy — remote builder (no local Docker), one machine. Context + Dockerfile are passed
|
|
98
|
+
// explicitly (the workspace root is the context; the Dockerfile lives under fastagent/).
|
|
78
99
|
log("deploying (remote build)…");
|
|
79
|
-
|
|
100
|
+
const deployArgs = [
|
|
101
|
+
"deploy",
|
|
102
|
+
".",
|
|
103
|
+
"-a",
|
|
104
|
+
plan.appName,
|
|
105
|
+
"-c",
|
|
106
|
+
plan.flyConfig,
|
|
107
|
+
"--dockerfile",
|
|
108
|
+
plan.dockerfile,
|
|
109
|
+
"--remote-only",
|
|
110
|
+
"--yes",
|
|
111
|
+
"--ha=false",
|
|
112
|
+
];
|
|
113
|
+
if ((await fly(deployArgs)).code !== 0) {
|
|
80
114
|
return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
|
|
81
115
|
}
|
|
82
116
|
// 7. Post-deploy webhook — telegram end-to-end (fastagent has the token + the live URL); github is a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FastagentConfig } from "../engines/pi/config.ts";
|
|
2
|
+
import { type ResolvedPlacement } from "../paths.ts";
|
|
2
3
|
import { type ChannelKind } from "../scaffold/add-channel.ts";
|
|
3
4
|
import { type ContainerInput } from "./container.ts";
|
|
4
5
|
/** A stderr line the CLI prints (`[fastagent] warn: …` / `[fastagent] note: …`). Host-neutral advisories. */
|
|
@@ -40,17 +41,22 @@ export type DeployPreflight = {
|
|
|
40
41
|
* provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
|
|
41
42
|
*/
|
|
42
43
|
export declare function preflightDeploy(input: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
|
|
44
|
+
/** The resolved placement. `agentDir` is where channels/schedules are discovered and where the
|
|
45
|
+
* container facts (package.json/lockfile) are read — the AGENT's manifest drives the image's install
|
|
46
|
+
* step, never the workspace's (whose manifest belongs to its own deploy); `workspace` is the build
|
|
47
|
+
* context (the whole tree is baked). One value, because one derives from the other: two loose
|
|
48
|
+
* strings could be handed in disagreeing, and nothing would notice. */
|
|
49
|
+
placement: ResolvedPlacement;
|
|
47
50
|
config: FastagentConfig;
|
|
48
51
|
modelSpec: string | undefined;
|
|
49
52
|
/** `--run` fully deploys, so a model that won't travel is a GATE (a known crash-loop); else it warns. */
|
|
50
53
|
run: boolean;
|
|
51
54
|
/** `--force` regenerates artifacts, so the kept-hand-written-Dockerfile apt warning does not apply. */
|
|
52
55
|
force: boolean;
|
|
53
|
-
/** The
|
|
56
|
+
/** The target delivers cron slots from an external clock and holds no resident process (AgentCore):
|
|
57
|
+
* resident-host keep-alive notes do not apply; the host branch owns its capability gates. */
|
|
58
|
+
externalClock?: boolean;
|
|
59
|
+
/** The raw `--auth-path` flag; the chain (flag > FASTAGENT_AUTH_PATH > `<agentDir>/.secrets/auth.json`)
|
|
54
60
|
* is resolved HERE via {@link resolveAuthPath} — the one owner, same as every serving command. */
|
|
55
61
|
authPathFlag: string | undefined;
|
|
56
62
|
}): Promise<DeployPreflight>;
|