@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/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 {
|
|
@@ -19,12 +39,12 @@ function listHasName(stdout, name) {
|
|
|
19
39
|
}
|
|
20
40
|
}
|
|
21
41
|
/**
|
|
22
|
-
* Run the deploy through `fly`. `log` reports progress;
|
|
23
|
-
*
|
|
24
|
-
*
|
|
42
|
+
* Run the deploy through `fly`. `log` reports progress; the injected Telegram/Feishu/Slack registrars
|
|
43
|
+
* perform post-deploy webhook steps from the builder machine (Slack's control credential never travels
|
|
44
|
+
* to the host). Absent, the manual console
|
|
25
45
|
* instruction is printed. Every gate is fail-visible.
|
|
26
46
|
*/
|
|
27
|
-
export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu) {
|
|
47
|
+
export async function deployFlyRun(plan, fly, log, registerTelegram, registerFeishu, registerSlack) {
|
|
28
48
|
const gate = (g) => ({ ok: false, gate: g });
|
|
29
49
|
// 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
|
|
30
50
|
// either an interactive login or FLY_API_TOKEN, so one check covers both.
|
|
@@ -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
|
|
@@ -91,8 +125,19 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
|
|
|
91
125
|
log(`github: set the webhook in the repo (Settings → Webhooks) → https://${plan.appName}.fly.dev/webhook`);
|
|
92
126
|
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
93
127
|
}
|
|
128
|
+
if (plan.channels.includes("slack")) {
|
|
129
|
+
const baseUrl = `https://${plan.appName}.fly.dev`;
|
|
130
|
+
if (registerSlack) {
|
|
131
|
+
log("registering slack event URL…");
|
|
132
|
+
reg.track("slack", await registerSlack(baseUrl));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
log(`slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`);
|
|
136
|
+
reg.track("slack", "manual");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
94
139
|
for (const kind of ["feishu", "lark"]) {
|
|
95
|
-
if (!plan.channels.includes(kind))
|
|
140
|
+
if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
|
|
96
141
|
continue;
|
|
97
142
|
if (registerFeishu) {
|
|
98
143
|
log(`registering ${kind} event URL…`);
|
|
@@ -1,15 +1,20 @@
|
|
|
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. */
|
|
5
|
-
|
|
6
|
+
interface DeployMessage {
|
|
6
7
|
level: "warn" | "note";
|
|
7
8
|
text: string;
|
|
8
9
|
}
|
|
9
10
|
/** The resolved facts every host plan needs (the container shape, channels, model auth, ports/secrets). */
|
|
10
|
-
|
|
11
|
+
interface DeployFacts {
|
|
11
12
|
messages: DeployMessage[];
|
|
12
13
|
channels: ChannelKind[];
|
|
14
|
+
/** Every structurally detected HTTP-route channel basename, including custom channels. */
|
|
15
|
+
routeChannels: string[];
|
|
16
|
+
/** Every structurally detected long-connection channel basename, including custom channels. */
|
|
17
|
+
longConnectionChannels: string[];
|
|
13
18
|
/** Whether the agent has TIME triggers — `schedules/` files or `selfSchedule` (the wake tool). Cron/wake
|
|
14
19
|
* has no external wake-up, so the deployment must keep one machine running: the fly plan forces
|
|
15
20
|
* `min_machines_running=1`, the railway runbook forbids App Sleeping. */
|
|
@@ -36,18 +41,24 @@ export type DeployPreflight = {
|
|
|
36
41
|
* provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
|
|
37
42
|
*/
|
|
38
43
|
export declare function preflightDeploy(input: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
|
|
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;
|
|
43
50
|
config: FastagentConfig;
|
|
44
51
|
modelSpec: string | undefined;
|
|
45
52
|
/** `--run` fully deploys, so a model that won't travel is a GATE (a known crash-loop); else it warns. */
|
|
46
53
|
run: boolean;
|
|
47
54
|
/** `--force` regenerates artifacts, so the kept-hand-written-Dockerfile apt warning does not apply. */
|
|
48
55
|
force: boolean;
|
|
49
|
-
/**
|
|
50
|
-
|
|
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`)
|
|
60
|
+
* is resolved HERE via {@link resolveAuthPath} — the one owner, same as every serving command. */
|
|
61
|
+
authPathFlag: string | undefined;
|
|
51
62
|
}): Promise<DeployPreflight>;
|
|
52
63
|
/**
|
|
53
64
|
* Why the resolved model won't reach the deployed box, or undefined if it will — host-neutral. `fastagent.config.ts`
|
|
@@ -57,3 +68,4 @@ export declare function preflightDeploy(input: {
|
|
|
57
68
|
* (`--run`). Single source on purpose — a host env block (fly.toml `[env]`) is NOT advertised as a second home.
|
|
58
69
|
*/
|
|
59
70
|
export declare function modelTravelIssue(configModel: string | undefined, modelSpec: string | undefined): string | undefined;
|
|
71
|
+
export {};
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -9,23 +9,59 @@
|
|
|
9
9
|
* the run modules' {@link import("./fly/run.ts").FlyRunOutcome}: a model that won't travel is a GATE the
|
|
10
10
|
* CLI stops on, distinct from the advisory warnings/notes it prints and proceeds past.
|
|
11
11
|
*/
|
|
12
|
-
import { readFile } from "node:fs/promises";
|
|
13
|
-
import { join, relative, sep } from "node:path";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
12
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
+
import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
14
|
+
import ignore from "ignore";
|
|
15
|
+
import { resolveAuthPath } from "../engines/pi/config.js";
|
|
16
|
+
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
17
|
+
import { inspectChannels } from "../engines/pi/channel.js";
|
|
16
18
|
import { discoverScheduleFiles } from "../schedule/discover.js";
|
|
17
19
|
import { createPiModels, probeAuthSource } from "../engines/pi/models.js";
|
|
18
20
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
19
|
-
import { exists } from "../
|
|
21
|
+
import { exists } from "../paths.js";
|
|
20
22
|
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
21
23
|
import { fastagentVersion } from "../version.js";
|
|
22
|
-
import { isGeneratedDockerfile } from "./container.js";
|
|
24
|
+
import { isGeneratedDockerfile, isGeneratedDockerignore } from "./container.js";
|
|
25
|
+
/**
|
|
26
|
+
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore`
|
|
27
|
+
* matcher (the same library the workspace ignore files use), so `!` negation and last-match-wins are
|
|
28
|
+
* the library's problem, not ours. Anchoring is normalized: dockerignore patterns are root-anchored
|
|
29
|
+
* while .gitignore's match at any depth, so a bare `foo` becomes `/foo` — without that, a root-only
|
|
30
|
+
* `.secrets` line would read as covering `fastagent/.secrets` and hand back a false all-clear on the
|
|
31
|
+
* exact check that guards credentials.
|
|
32
|
+
*
|
|
33
|
+
* Known dialect gap: `ignore` keeps git's rule that a path under an EXCLUDED directory cannot be
|
|
34
|
+
* re-included, which docker does not have — so an allowlist file (`*` + `!fastagent` + `!fastagent/**`)
|
|
35
|
+
* can read as excluding a path docker would ship. The callers below absorb that: the drop-the-agent
|
|
36
|
+
* gate requires the agent DIRECTORY itself to read as excluded too, which an allowlist re-includes.
|
|
37
|
+
*/
|
|
38
|
+
function dockerignoreMatcher(text) {
|
|
39
|
+
const anchored = text
|
|
40
|
+
.split("\n")
|
|
41
|
+
.map((raw) => {
|
|
42
|
+
const line = raw.trim();
|
|
43
|
+
if (line === "" || line.startsWith("#"))
|
|
44
|
+
return line;
|
|
45
|
+
const negated = line.startsWith("!");
|
|
46
|
+
const pattern = negated ? line.slice(1) : line;
|
|
47
|
+
if (pattern.startsWith("/") || pattern.startsWith("**/"))
|
|
48
|
+
return line;
|
|
49
|
+
return `${negated ? "!" : ""}/${pattern}`;
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
const matcher = ignore({ ignorecase: false }).add(anchored);
|
|
53
|
+
return (path) => matcher.ignores(path);
|
|
54
|
+
}
|
|
23
55
|
/**
|
|
24
56
|
* Run the host-neutral pre-flight. Throws on a real fault (an unreadable channels/ dir, a throwing
|
|
25
57
|
* provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
|
|
26
58
|
*/
|
|
27
59
|
export async function preflightDeploy(input) {
|
|
28
|
-
const {
|
|
60
|
+
const { placement: { agentDir, workspace }, config, modelSpec, run, force, externalClock, authPathFlag, } = input;
|
|
61
|
+
// The ONE derived placement fact every host plan needs: where the agent's files sit relative to the
|
|
62
|
+
// build context (the workspace). Nested → "fastagent/"; flat → "" (the agent IS the workspace root).
|
|
63
|
+
const nested = agentDir !== workspace;
|
|
64
|
+
const agentPrefix = nested ? `${basename(agentDir)}/` : "";
|
|
29
65
|
const messages = [];
|
|
30
66
|
// The deployed box resolves the model from fastagent.config.ts ONLY (in the image); a model set via
|
|
31
67
|
// env/flag/.env doesn't travel. `--run` would ship a known crash-loop — hard gate; generate-only warns.
|
|
@@ -35,21 +71,53 @@ export async function preflightDeploy(input) {
|
|
|
35
71
|
return { ok: false, gate: modelIssue };
|
|
36
72
|
messages.push({ level: "warn", text: modelIssue });
|
|
37
73
|
}
|
|
74
|
+
// The control plane on a deployed box: `start` honors `sessionControl: true`, so `/control/*`
|
|
75
|
+
// (steer/abort/set_model) rides the PUBLIC host URL — protected only by a per-boot bearer token
|
|
76
|
+
// minted INSIDE the container (`<stateRoot>/control.json`), which external consumers cannot read.
|
|
77
|
+
// Publicly reachable yet unusable is the worst of both; the tunnel path warns loudly and deploy
|
|
78
|
+
// must not be the silent second way to break the loopback trust story.
|
|
79
|
+
if (config.sessionControl === true) {
|
|
80
|
+
messages.push({
|
|
81
|
+
level: "warn",
|
|
82
|
+
text: `sessionControl: true — the deployed box serves /control/* (steer/abort/set_model) at its public URL, ` +
|
|
83
|
+
`protected only by a per-boot token written inside the container. Read the TOKEN from ` +
|
|
84
|
+
`<stateRoot>/control.json on the box (its url field is container-loopback — pair the token with the ` +
|
|
85
|
+
`public host URL: attach --url <public-url> --token …), or front the endpoint with real auth (design §14)`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
38
88
|
// Known channel kinds only — a custom channel's secrets/webhook are unknown to us; note and let the
|
|
39
89
|
// author wire them.
|
|
40
|
-
const
|
|
90
|
+
const inspected = await inspectChannels(agentDir);
|
|
91
|
+
if (inspected.failures.length > 0) {
|
|
92
|
+
throw new Error(`cannot inspect channel modules: ${inspected.failures.map((failure) => `${failure.label}: ${failure.message}`).join("; ")}`);
|
|
93
|
+
}
|
|
94
|
+
const discovered = inspected.channels;
|
|
41
95
|
const channels = discovered.filter((c) => CHANNEL_KINDS.includes(c));
|
|
96
|
+
const routeChannels = inspected.routeChannels;
|
|
97
|
+
const longConnectionChannels = inspected.longConnectionChannels;
|
|
42
98
|
for (const c of discovered) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
99
|
+
if (channels.includes(c))
|
|
100
|
+
continue;
|
|
101
|
+
messages.push({
|
|
102
|
+
level: "note",
|
|
103
|
+
text: longConnectionChannels.includes(c)
|
|
104
|
+
? `long-connection channel "${c}" is custom — configure its secrets yourself; generated deploy plans keep the process running and skip webhook registration`
|
|
105
|
+
: `route channel "${c}" is custom — configure its secrets and webhook yourself`,
|
|
106
|
+
});
|
|
46
107
|
}
|
|
47
108
|
// Time triggers (static schedules or self-scheduling) need a machine kept running — unlike a webhook,
|
|
48
109
|
// nothing external wakes a scale-to-zero box for a cron instant or a wake-up. The note is CONDITIONAL
|
|
49
110
|
// ("the generated plan…"): in KEEP mode an existing fly.toml is not rewritten — the CLI warns separately
|
|
50
111
|
// when a kept fly.toml still scales to zero.
|
|
51
112
|
const hasTimeTriggers = (await discoverScheduleFiles(agentDir)).length > 0 || !!config.selfSchedule;
|
|
52
|
-
if (
|
|
113
|
+
if (longConnectionChannels.length > 0 && !externalClock) {
|
|
114
|
+
messages.push({
|
|
115
|
+
level: "note",
|
|
116
|
+
text: `long-connection channel present (${longConnectionChannels.join(", ")}) — a GENERATED plan keeps one machine running ` +
|
|
117
|
+
`(an outbound connection cannot wake a scaled-to-zero service).`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (hasTimeTriggers && !externalClock) {
|
|
53
121
|
messages.push({
|
|
54
122
|
level: "note",
|
|
55
123
|
text: `schedules/self-scheduling present — a GENERATED plan keeps one machine running (cron/wake has ` +
|
|
@@ -58,46 +126,37 @@ export async function preflightDeploy(input) {
|
|
|
58
126
|
}
|
|
59
127
|
// Probe auth from the SAME project-level file the opener/login use — not the global default, which would
|
|
60
128
|
// miss a `fastagent login` credential and falsely report "none configured".
|
|
61
|
-
const authPath =
|
|
129
|
+
const authPath = resolveAuthPath(agentDir, authPathFlag);
|
|
62
130
|
const modelAuth = modelSpec ? await probeAuthSource(createPiModels({ authPath }), modelSpec) : undefined;
|
|
63
|
-
// Container facts (shared by every host) + the warnings that follow.
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const factsDir = kitDir ? agentDir : target;
|
|
70
|
-
if (kitDir && run) {
|
|
71
|
-
// The repo-as-workspace deployment shape remains experimental for every target. Generation +
|
|
72
|
-
// runbook are supported; automated runners stay gated until an explicit end-to-end smoke validates
|
|
73
|
-
// context packing, ignore rules, installed deps, state, and write-back for this layout.
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
gate: `--run is not yet supported for the agentDir layout — run the same deploy without --run and follow the printed runbook`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
const hasPackageJson = await exists(join(factsDir, "package.json"));
|
|
80
|
-
const pkg = await readPackageJson(factsDir);
|
|
81
|
-
const { runtime, bunVersion, hasLockfile } = detectRuntime(factsDir, pkg);
|
|
131
|
+
// Container facts (shared by every host) + the warnings that follow. The facts describe the AGENT —
|
|
132
|
+
// its package.json/runtime/lockfile drive the image's install step — never the workspace's (the bake
|
|
133
|
+
// ships the whole tree, but the workspace's own manifest belongs to its own deploy).
|
|
134
|
+
const hasPackageJson = await exists(join(agentDir, "package.json"));
|
|
135
|
+
const pkg = await readPackageJson(agentDir);
|
|
136
|
+
const { runtime, bunVersion, hasLockfile } = detectRuntime(agentDir, pkg);
|
|
82
137
|
const install = runtime === "bun" ? "bun install" : "npm install";
|
|
83
138
|
const runner = runtime === "bun" ? "bun run fastagent" : "./node_modules/.bin/fastagent";
|
|
84
139
|
const hasOtherLock = runtime === "node" &&
|
|
85
|
-
((await exists(join(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
140
|
+
((await exists(join(agentDir, "pnpm-lock.yaml"))) || (await exists(join(agentDir, "yarn.lock"))));
|
|
141
|
+
// Does the baked workspace ship a `.git`? ONE fact driving both the image's git install (below)
|
|
142
|
+
// and the plans' runbook wording — the write-back loop needs the history AND the binary together.
|
|
143
|
+
const shipsGit = await exists(join(workspace, ".git"));
|
|
144
|
+
// After the facts: the deps sentence must match the agent's actual shape (a markdown-only agent has
|
|
145
|
+
// no package.json and installs nothing — the note must not point at a file that doesn't exist).
|
|
146
|
+
const deps = hasPackageJson
|
|
147
|
+
? `only the agent's deps (${agentPrefix}package.json) are installed${nested ? " — the workspace's own deps are the agent's runtime concern" : ""}`
|
|
148
|
+
: `the agent has no package.json, so no deps are installed (the pinned global CLI serves the directory)`;
|
|
149
|
+
const durability = shipsGit
|
|
150
|
+
? `Un-pushed changes on the box do not survive a redeploy; freshness and write-back run through git, ` +
|
|
151
|
+
`driven by the agent itself (persona owns the policy; GH_TOKEN etc. go in config.deploy.secrets)`
|
|
152
|
+
: `no .git here, so no history ships and the image does not install git — changes on the box are ` +
|
|
153
|
+
`ephemeral and do not survive a redeploy`;
|
|
154
|
+
messages.push({
|
|
155
|
+
level: "note",
|
|
156
|
+
text: `the whole directory is baked as the agent's workspace (WYSIWYG — what you see is what ships, ` +
|
|
157
|
+
`git or not, clean or not); ${deps}. ${durability}.`,
|
|
158
|
+
});
|
|
159
|
+
// A code agent with no lockfile builds via a non-frozen install (ranges resolve at build time) — not
|
|
101
160
|
// reproducible. A pnpm/yarn user gets an accurate message (their lockfile is ignored by the npm Dockerfile).
|
|
102
161
|
if (hasPackageJson && !hasLockfile) {
|
|
103
162
|
const lock = runtime === "bun" ? "bun.lock" : "package-lock.json";
|
|
@@ -110,7 +169,7 @@ export async function preflightDeploy(input) {
|
|
|
110
169
|
`Run \`${install}\` and commit the lockfile for pinned redeploys.`,
|
|
111
170
|
});
|
|
112
171
|
}
|
|
113
|
-
// The code-path Dockerfile runs `${runner}` — the
|
|
172
|
+
// The code-path Dockerfile runs `${runner}` — the agent's OWN local dependency, never the
|
|
114
173
|
// registry — so a package.json missing it means the container fails at start (no bin to run).
|
|
115
174
|
if (hasPackageJson && !("@fastagent-sh/fastagent" in { ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
116
175
|
messages.push({
|
|
@@ -119,42 +178,167 @@ export async function preflightDeploy(input) {
|
|
|
119
178
|
`so the container fails at start. Add it to dependencies and re-run \`${install}\`.`,
|
|
120
179
|
});
|
|
121
180
|
}
|
|
122
|
-
// A
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
// A KEPT workspace-root .dockerignore silently replaces the generated one's protections — so ASK IT
|
|
182
|
+
// about the exact paths that matter (the generic "kept" line suggests --force, which never clobbers
|
|
183
|
+
// the workspace's own file). Two are GATES under --run, same discipline as the model-travel gate:
|
|
184
|
+
// dropping the agent dir ships a context with no persona/config (the box crash-loops), and an
|
|
185
|
+
// unexcluded secrets path BAKES CREDENTIALS INTO THE IMAGE. The other two are advisory: the build
|
|
186
|
+
// machine's node_modules (native binaries for YOUR OS) clobbering the image's, and an excluded .git
|
|
187
|
+
// killing the agent's pull/push loop (a legitimate slimming choice). Not force-gated — the file is
|
|
188
|
+
// kept even under --force.
|
|
189
|
+
// Which paths INSIDE the build context hold secrets — resolved, then made workspace-relative (the
|
|
190
|
+
// context root). An external secrets dir (a mounted volume) is outside the context: nothing to check
|
|
191
|
+
// and nothing to exclude. Also fed to the generated .dockerignore, so a custom in-tree dir is
|
|
192
|
+
// excluded by PATH even though its name is not `.secrets`.
|
|
193
|
+
const inContext = (p) => {
|
|
194
|
+
const rel = relative(workspace, p);
|
|
195
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? undefined : rel.split(sep).join("/");
|
|
196
|
+
};
|
|
197
|
+
// The secrets DIR is the unit, not the two filenames we happen to know: an atomic-write temp beside
|
|
198
|
+
// auth.json, a second key file, an editor backup of `.env` — all of it must stay out of the image, and
|
|
199
|
+
// `resolveSecretsDir` says as much ("everything fastagent manages that must never leave the machine").
|
|
200
|
+
// The auth path adds an entry only when an override puts it OUTSIDE that dir. An external secrets dir
|
|
201
|
+
// (the deployed posture: a mounted volume) is outside the context — nothing to check, nothing to
|
|
202
|
+
// exclude.
|
|
203
|
+
const secretsRel = inContext(resolveSecretsDir(agentDir));
|
|
204
|
+
const authRel = inContext(authPath);
|
|
205
|
+
const authElsewhere = authRel !== undefined && (secretsRel === undefined || !authRel.startsWith(`${secretsRel}/`));
|
|
206
|
+
const secretPaths = [...(secretsRel ? [secretsRel] : []), ...(authElsewhere ? [authRel] : [])];
|
|
207
|
+
// ONE rule for every checked path: a file that is not there cannot be baked, so gating on it would
|
|
208
|
+
// be a refusal about a spelling rather than about what would ship (an agent that has never run
|
|
209
|
+
// `login` has no auth.json). The generated .dockerignore still excludes them unconditionally —
|
|
210
|
+
// cheap, and correct the moment they appear.
|
|
211
|
+
const present = async (rels) => {
|
|
212
|
+
const found = [];
|
|
213
|
+
for (const rel of rels)
|
|
214
|
+
if (await exists(join(workspace, rel)))
|
|
215
|
+
found.push(rel);
|
|
216
|
+
return found;
|
|
217
|
+
};
|
|
218
|
+
// State gets the same treatment (a custom in-tree FASTAGENT_STATE_DIR is invisible to the
|
|
219
|
+
// name-based `**/.state`), at warn level: shipping stale sessions is waste, not a credential leak.
|
|
220
|
+
const stateRel = inContext(resolveStateRoot(agentDir));
|
|
221
|
+
// Existence gates the WARNING, never the generated exclude (same split as secretPaths vs
|
|
222
|
+
// leakCandidates): an agent that has never run has no `.state/`, so telling its author a kept ignore
|
|
223
|
+
// file fails to exclude one is a remark about a spelling — while the file we generate must still carry
|
|
224
|
+
// the line, since it is written once and correct the moment the directory appears.
|
|
225
|
+
const stateShips = stateRel !== undefined && (await exists(join(workspace, stateRel))) ? stateRel : undefined;
|
|
226
|
+
// The `.env` family at the two levels fastagent is RESPONSIBLE for: the agent dir and the workspace
|
|
227
|
+
// root. Asking only about a root-level `.env` missed both halves that matter — an `<agent>/.env`, the
|
|
228
|
+
// file habit puts there (env.ts warns about it by name), and the `.env.local` / `.env.production`
|
|
229
|
+
// spellings. DISCOVERED rather than spelled, so the existing rule still holds: only a file that is
|
|
230
|
+
// there can be baked, so only it is gated.
|
|
231
|
+
//
|
|
232
|
+
// Deliberately NOT the recursive `**/.env` the generated file carries: walking a whole monorepo for
|
|
233
|
+
// credential files is a secret scanner, not a placement pre-flight, and a bounded walk would be a
|
|
234
|
+
// heuristic pretending to be a guarantee. The division of responsibility is the honest one — the file
|
|
235
|
+
// WE generate covers every level; an author who keeps their own owns its coverage of their own tree,
|
|
236
|
+
// and this gate speaks only for the paths fastagent itself puts credentials in.
|
|
237
|
+
const dotEnvFiles = async (relDir) => {
|
|
238
|
+
const names = await readdir(join(workspace, relDir || ".")).catch(() => []);
|
|
239
|
+
// POSIX separators, like every other context-relative path here (`inContext`): these strings are
|
|
240
|
+
// matched against dockerignore patterns, and a Windows `fastagent\.env` would match none of them —
|
|
241
|
+
// silently turning the one check whose failure mode is "credentials in a published image" into a
|
|
242
|
+
// no-op.
|
|
243
|
+
return names
|
|
244
|
+
.filter((n) => (n === ".env" || n.startsWith(".env.")) && n !== ".env.example")
|
|
245
|
+
.map((n) => join(relDir, n).split(sep).join("/"));
|
|
246
|
+
};
|
|
247
|
+
const envFiles = (await Promise.all([...new Set(["", agentPrefix])].map(dotEnvFiles))).flat();
|
|
248
|
+
const leakCandidates = [...(await present(secretPaths)), ...envFiles];
|
|
249
|
+
// Same existence rule: a node_modules that is not there cannot be uploaded.
|
|
250
|
+
const depDirs = await present([...new Set([`${agentPrefix}node_modules`, "node_modules"])]);
|
|
251
|
+
const machineryPaths = [...secretPaths, ...(stateRel ? [stateRel] : [])];
|
|
252
|
+
// BOTH ignore files deploy emits get the same interrogation. The workspace-root one is what
|
|
253
|
+
// flyctl/railway's packers read; the per-Dockerfile one is what BuildKit PREFERS for a plain
|
|
254
|
+
// `docker build` (so it is the file that actually decides `deploy docker`). Checking only the root one
|
|
255
|
+
// left the credential gate not covering the path it was written for.
|
|
256
|
+
for (const rel of [".dockerignore", `${agentPrefix}Dockerfile.dockerignore`]) {
|
|
257
|
+
const kept = await readFile(join(workspace, rel), "utf8").catch(() => undefined);
|
|
258
|
+
if (kept === undefined)
|
|
259
|
+
continue;
|
|
260
|
+
// One WE generated is regenerated by this very run under --force, so checking the stale content on
|
|
261
|
+
// disk would gate a deploy on a file about to be replaced. Ours + --force: skip. Ours WITHOUT --force
|
|
262
|
+
// is still checked (it is what would ship), but the remedy differs: hand-adding lines to fastagent's
|
|
263
|
+
// own output is not the fix — regenerating it is.
|
|
264
|
+
const keptIsOurs = isGeneratedDockerignore(kept);
|
|
265
|
+
if (force && keptIsOurs)
|
|
266
|
+
continue;
|
|
267
|
+
const remedy = (lines) => keptIsOurs
|
|
268
|
+
? `Re-run with --force to regenerate it.`
|
|
269
|
+
: `Add ${lines.map((p) => `\`${p}\``).join(" and ")} before deploying (the same lines the generated ${rel} writes).`;
|
|
270
|
+
const excluded = dockerignoreMatcher(kept);
|
|
271
|
+
// Asked as a DIRECTORY (trailing slash), which is what it is. A bare-name test answers `false` for
|
|
272
|
+
// the `fastagent/` spelling — a directory-only pattern, and the one a hand-written ignore file is
|
|
273
|
+
// most likely to carry — so the agent would be dropped from the context with no warning at all. The
|
|
274
|
+
// pairing this replaced (the directory AND a file inside it) was dead weight rather than a
|
|
275
|
+
// safeguard: git's rule that a file under an excluded directory cannot be re-included is
|
|
276
|
+
// implemented by the matcher, so `excluded(dir)` already implies `excluded(dir/persona.md)` and the
|
|
277
|
+
// second test could never change the answer. What it was aimed at — an allowlist (`*` +
|
|
278
|
+
// `!fastagent` + `!fastagent/**`) that re-includes the agent — is handled by the first test alone,
|
|
279
|
+
// which reads `false` there, as it should.
|
|
280
|
+
if (nested && excluded(`${basename(agentDir)}/`)) {
|
|
281
|
+
const text = `your ${rel} (kept) excludes \`${basename(agentDir)}\` — the build context would ship WITHOUT the ` +
|
|
282
|
+
`agent entirely (the deployed box has no persona/config and crash-loops). Remove that rule ` +
|
|
283
|
+
`before deploying.`;
|
|
284
|
+
if (run)
|
|
285
|
+
return { ok: false, gate: text };
|
|
286
|
+
messages.push({ level: "warn", text });
|
|
287
|
+
}
|
|
288
|
+
// Resolved paths, not spellings: dockerignore patterns are root-anchored (unlike .gitignore), so a
|
|
289
|
+
// bare `.secrets` line does not cover `fastagent/.secrets` — and FASTAGENT_SECRETS_DIR /
|
|
290
|
+
// FASTAGENT_AUTH_PATH can put credentials anywhere in the baked tree, where the name-based excludes
|
|
291
|
+
// never reach. This is the one check whose failure mode is "credentials in a published image".
|
|
292
|
+
const leaks = leakCandidates.filter((p) => !excluded(p));
|
|
293
|
+
if (leaks.length > 0) {
|
|
294
|
+
const text = `your ${rel} (kept) does not exclude ${leaks.map((p) => `\`${p}\``).join(", ")} — the build ` +
|
|
295
|
+
`context would BAKE SECRETS INTO THE IMAGE. ${remedy(leaks.map((p) => `/${p}`))}`;
|
|
296
|
+
if (run)
|
|
297
|
+
return { ok: false, gate: text };
|
|
298
|
+
messages.push({ level: "warn", text });
|
|
299
|
+
}
|
|
300
|
+
if (stateShips && !excluded(`${stateShips}/sessions`)) {
|
|
131
301
|
messages.push({
|
|
132
302
|
level: "warn",
|
|
133
|
-
text: `your
|
|
134
|
-
`cannot commit/push the baked copy; it must \`git clone\` its repo in the workspace instead ` +
|
|
135
|
-
`(or remove the .git line).`,
|
|
303
|
+
text: `your ${rel} (kept) does not exclude \`${stateRel}\` — the build machine's sessions/channel state would ship in the image. ${remedy([`/${stateRel}`])}`,
|
|
136
304
|
});
|
|
137
305
|
}
|
|
138
|
-
|
|
306
|
+
// Both the agent's own node_modules and the workspace's: either would upload the build machine's
|
|
307
|
+
// deps (native binaries for YOUR OS) and clobber the image's freshly-installed ones. Named by the
|
|
308
|
+
// PATHS actually found unexcluded, like every other check here — not by a rule's spelling.
|
|
309
|
+
const unexcludedDeps = depDirs.filter((p) => !excluded(`${p}/.package-lock.json`));
|
|
310
|
+
if (unexcludedDeps.length > 0) {
|
|
139
311
|
messages.push({
|
|
140
312
|
level: "warn",
|
|
141
|
-
text: `your
|
|
142
|
-
`(native binaries for YOUR OS) would be uploaded and clobber the image's
|
|
143
|
-
`
|
|
313
|
+
text: `your ${rel} does not exclude ${unexcludedDeps.map((p) => `\`${p}\``).join(" or ")} — the ` +
|
|
314
|
+
`build machine's deps (native binaries for YOUR OS) would be uploaded and clobber the image's ` +
|
|
315
|
+
`freshly-installed ones. ${remedy(unexcludedDeps.map((p) => `/${p}`))}`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (excluded(".git/HEAD")) {
|
|
319
|
+
messages.push({
|
|
320
|
+
level: "note",
|
|
321
|
+
text: `your ${rel} excludes .git — the baked copy ships WITHOUT history/remote, so the agent ` +
|
|
322
|
+
`cannot pull/commit/push it; it must \`git clone\` its repo in the workspace instead (or remove the .git line).`,
|
|
144
323
|
});
|
|
145
324
|
}
|
|
146
325
|
}
|
|
147
|
-
// Write-back mechanics are fastagent's (the policy is the persona's):
|
|
148
|
-
//
|
|
149
|
-
|
|
326
|
+
// Write-back mechanics are fastagent's (the policy is the persona's): the image carries the git
|
|
327
|
+
// BINARY iff the baked workspace ships a `.git` (history without the binary is a
|
|
328
|
+
// dead loop; the binary without history is dead weight). A non-git workspace that still needs git
|
|
329
|
+
// (the agent clones repos as its job) declares config.deploy.apt: ["git"] explicitly. Merged with
|
|
330
|
+
// (never duplicating) config.deploy.apt.
|
|
331
|
+
const apt = shipsGit ? [...new Set(["git", ...(config.deploy?.apt ?? [])])] : config.deploy?.apt;
|
|
150
332
|
const container = {
|
|
333
|
+
agentPrefix,
|
|
334
|
+
machineryPaths,
|
|
151
335
|
hasPackageJson,
|
|
152
336
|
runtime,
|
|
153
337
|
bunVersion,
|
|
154
338
|
hasLockfile,
|
|
155
339
|
version: await fastagentVersion(),
|
|
156
340
|
apt,
|
|
157
|
-
|
|
341
|
+
shipsGit,
|
|
158
342
|
};
|
|
159
343
|
const port = config.http?.port ?? 8787;
|
|
160
344
|
// What the agent declared it needs on the box (fastagent.config deploy.secrets) — carried like channel
|
|
@@ -163,7 +347,7 @@ export async function preflightDeploy(input) {
|
|
|
163
347
|
// deploy.apt only shapes the GENERATED Dockerfile. Warn ONLY when the kept Dockerfile is HAND-WRITTEN
|
|
164
348
|
// (its apt won't include these) — a fastagent-generated one is handled by writeArtifacts. Don't suggest
|
|
165
349
|
// --force here: it would overwrite the user's hand-written file.
|
|
166
|
-
const dockerfileHome =
|
|
350
|
+
const dockerfileHome = join(agentDir, "Dockerfile");
|
|
167
351
|
if (config.deploy?.apt?.length && !force && (await exists(dockerfileHome))) {
|
|
168
352
|
if (!isGeneratedDockerfile(await readFile(dockerfileHome, "utf8"))) {
|
|
169
353
|
messages.push({
|
|
@@ -173,7 +357,19 @@ export async function preflightDeploy(input) {
|
|
|
173
357
|
});
|
|
174
358
|
}
|
|
175
359
|
}
|
|
176
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
ok: true,
|
|
362
|
+
messages,
|
|
363
|
+
channels,
|
|
364
|
+
routeChannels,
|
|
365
|
+
longConnectionChannels,
|
|
366
|
+
hasTimeTriggers,
|
|
367
|
+
modelAuth,
|
|
368
|
+
authPath,
|
|
369
|
+
container,
|
|
370
|
+
port,
|
|
371
|
+
extraSecrets,
|
|
372
|
+
};
|
|
177
373
|
}
|
|
178
374
|
/**
|
|
179
375
|
* Why the resolved model won't reach the deployed box, or undefined if it will — host-neutral. `fastagent.config.ts`
|
|
@@ -29,8 +29,10 @@ export interface RailwayPlanInput extends ContainerInput {
|
|
|
29
29
|
serviceName: string;
|
|
30
30
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
31
31
|
modelAuth: string | undefined;
|
|
32
|
-
/**
|
|
32
|
+
/** Known first-party channels — each contributes its secret metadata + webhook step. */
|
|
33
33
|
channels: ChannelKind[];
|
|
34
|
+
/** All long-connection channel basenames, including custom channels — no App Sleeping. */
|
|
35
|
+
longConnectionChannels?: string[];
|
|
34
36
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
35
37
|
extraSecrets?: string[];
|
|
36
38
|
/** Time triggers present (schedules/ or selfSchedule) — the runbook forbids App Sleeping: cron/wake has
|
|
@@ -44,5 +46,12 @@ export interface RailwayPlan {
|
|
|
44
46
|
/** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
|
|
45
47
|
runbook: string[];
|
|
46
48
|
}
|
|
49
|
+
/** The `RAILWAY_DOCKERFILE_PATH` value for an agent under `prefix` — repo-root-anchored with a leading
|
|
50
|
+
* slash, the form Railway's builds/dockerfiles docs use for a Dockerfile in another directory. The
|
|
51
|
+
* config file's `dockerfilePath` spells it WITHOUT the slash (the config-as-code schema's own
|
|
52
|
+
* convention); two mechanisms, two documented spellings, one fact each. */
|
|
53
|
+
export declare const dockerfilePathVar: (prefix: string) => string;
|
|
54
|
+
/** Did fastagent generate this `railway.json`? Unparseable or unmarked reads as the author's. */
|
|
55
|
+
export declare function isGeneratedRailwayJson(content: string): boolean;
|
|
47
56
|
/** Compute the Railway deploy plan from the resolved definition. */
|
|
48
57
|
export declare function planRailwayDeploy(input: RailwayPlanInput): RailwayPlan;
|