@fastagent-sh/fastagent 0.21.0 → 0.22.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 +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -43
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +209 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +153 -228
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- package/dist/schedule/schedule.js +0 -23
package/dist/deploy/preflight.js
CHANGED
|
@@ -1,96 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The host-NEUTRAL deploy pre-flight: everything `fastagent deploy <host>` computes and checks BEFORE
|
|
3
|
-
*
|
|
4
|
-
* container facts + their warnings, and the hand-written-Dockerfile apt warning are identical on every
|
|
5
|
-
* host — so they live here, out of the CLI dispatcher, testable in isolation (call it against a temp dir
|
|
6
|
-
* and assert the gate / messages / facts). The CLI stays thin: run this, print the messages, branch by host.
|
|
7
|
-
*
|
|
8
|
-
* It returns messages rather than printing them (the CLI owns stderr) and a `{ ok }` outcome mirroring
|
|
9
|
-
* the run modules' {@link import("./fly/run.ts").FlyRunOutcome}: a model that won't travel is a GATE the
|
|
10
|
-
* CLI stops on, distinct from the advisory warnings/notes it prints and proceeds past.
|
|
2
|
+
* The host-NEUTRAL deploy pre-flight: everything `fastagent deploy <host>` computes and checks BEFORE the target
|
|
3
|
+
* branch (Docker / Fly / Railway).
|
|
11
4
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import { resolveAuthPath } from "../engines/pi/config.js";
|
|
17
|
-
import {
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { basename, join, relative } from "node:path";
|
|
8
|
+
import { isModelSpec, isReleaseAgentName } from "./workspace.js";
|
|
9
|
+
import { providerOf, resolveAuthPath } from "../engines/pi/config.js";
|
|
10
|
+
import { AGENT_MODELS_FILE, exists } from "../paths.js";
|
|
18
11
|
import { inspectChannels } from "../channels/discover.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
12
|
+
import { loadRoutines } from "../schedule/discover.js";
|
|
13
|
+
import { resolveAgentTools } from "../engines/pi/create.js";
|
|
14
|
+
import { allSecrets } from "../declared-secrets.js";
|
|
15
|
+
import { createPiModelRuntime, literalKeyProviders, isBuiltinProvider, machineModels, modelCredentialCarry, probeAuthSource, } from "../engines/pi/models.js";
|
|
21
16
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
22
17
|
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
23
18
|
import { fastagentVersion } from "../version.js";
|
|
24
|
-
import { isGeneratedDockerfile
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* can read as excluding a path docker would ship. The callers below absorb that: the drop-the-agent
|
|
38
|
-
* gate requires the agent DIRECTORY itself to read as excluded too, which an allowlist re-includes.
|
|
39
|
-
*/
|
|
40
|
-
function dockerignoreMatcher(text) {
|
|
41
|
-
const anchored = text
|
|
42
|
-
.split("\n")
|
|
43
|
-
.map((raw) => {
|
|
44
|
-
const line = raw.trim();
|
|
45
|
-
if (line === "" || line.startsWith("#"))
|
|
46
|
-
return line;
|
|
47
|
-
const negated = line.startsWith("!");
|
|
48
|
-
const pattern = negated ? line.slice(1) : line;
|
|
49
|
-
if (pattern.startsWith("/") || pattern.startsWith("**/"))
|
|
50
|
-
return line;
|
|
51
|
-
return `${negated ? "!" : ""}/${pattern}`;
|
|
52
|
-
})
|
|
53
|
-
.join("\n");
|
|
54
|
-
const matcher = ignore({ ignorecase: false }).add(anchored);
|
|
55
|
-
return (path) => matcher.ignores(path);
|
|
19
|
+
import { isGeneratedDockerfile } from "./container.js";
|
|
20
|
+
import { buildContextPaths, checkKeptIgnoreFiles } from "./build-context.js";
|
|
21
|
+
import { CRON_CAN_BE_EXTERNAL, residencyFor } from "./residency.js";
|
|
22
|
+
import { dotEnvPath, loadEnvValues } from "../env.js";
|
|
23
|
+
import { deploymentSecrets, isEnvKey } from "./secrets.js";
|
|
24
|
+
import { DEFAULT_HTTP_PORT, describeAnonymousSurface, shouldServeRun } from "../service.js";
|
|
25
|
+
import { CONTROL_PREFIX } from "../channels/control.js";
|
|
26
|
+
/** The pre-flight's one early exit: thrown by a check, turned into `{ ok: false, gate }` by {@link preflightDeploy}. */
|
|
27
|
+
class DeployGate {
|
|
28
|
+
text;
|
|
29
|
+
constructor(text) {
|
|
30
|
+
this.text = text;
|
|
31
|
+
}
|
|
56
32
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Run the host-neutral pre-flight. Throws on a real fault (an unreadable channels/ dir, a throwing
|
|
59
|
-
* provider) — the CLI wraps the call in its `failStartup` so the fault surfaces and exits, never silently.
|
|
60
|
-
*/
|
|
33
|
+
/** Run the host-neutral pre-flight. */
|
|
61
34
|
export async function preflightDeploy(input) {
|
|
62
|
-
const { placement: { agentDir, workspace }, config, modelSpec, run, force, externalClock, authPathFlag, } = input;
|
|
63
|
-
// The ONE derived placement fact every host plan needs: where the agent's files sit relative to the
|
|
64
|
-
// build context (the workspace). Nested → "fastagent/"; flat → "" (the agent IS the workspace root).
|
|
65
|
-
const nested = agentDir !== workspace;
|
|
66
|
-
const agentPrefix = nested ? `${basename(agentDir)}/` : "";
|
|
67
35
|
const messages = [];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
36
|
+
const report = {
|
|
37
|
+
note: (text) => void messages.push({ level: "note", text }),
|
|
38
|
+
warn: (text) => void messages.push({ level: "warn", text }),
|
|
39
|
+
issue: (text) => {
|
|
40
|
+
if (input.run)
|
|
41
|
+
throw new DeployGate(text);
|
|
42
|
+
messages.push({ level: "warn", text });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, messages, ...(await gatherFacts(input, report)) };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof DeployGate)
|
|
50
|
+
return { ok: false, gate: error.text };
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function gatherFacts(input, report) {
|
|
55
|
+
const { placement: { agentDir, workspace }, config, force, externalClock, publicUrl = true, } = input;
|
|
56
|
+
// The release manifest carries this name into the container, where it is joined onto the storage root — so `init`'s
|
|
57
|
+
// "one path segment" is not enough here.
|
|
58
|
+
if (!isReleaseAgentName(basename(agentDir))) {
|
|
59
|
+
throw new DeployGate(`the agent directory "${basename(agentDir)}" cannot be deployed — a deployed agent directory ` +
|
|
60
|
+
`may use only letters, digits, "-" and "_"; rename it (the fastagent.config.ts inside is what ` +
|
|
61
|
+
`makes it an agent, never its name)`);
|
|
75
62
|
}
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`same value to callers: attach --url <public-url> --token …. Unset, the box mints its own per boot — ` +
|
|
87
|
-
`readable only by shelling in (\`docker compose exec\`/\`fly ssh console\`: <stateRoot>/control.json, whose ` +
|
|
88
|
-
`url field is container-loopback) and replaced on every restart. Front the endpoint with real auth for ` +
|
|
89
|
-
`anything wider (docs/design/session-control.md §14)`,
|
|
90
|
-
});
|
|
63
|
+
const agentPrefix = `${basename(agentDir)}/`;
|
|
64
|
+
// The model this deployment will run on, and where it came from. Resolved HERE so the plan side and the run side
|
|
65
|
+
// cannot disagree about it.
|
|
66
|
+
const valueFile = relative(workspace, dotEnvPath(agentDir));
|
|
67
|
+
const values = loadEnvValues(dotEnvPath(agentDir));
|
|
68
|
+
const model = resolveDeployModel(config, values, valueFile);
|
|
69
|
+
if (model.invalid !== undefined) {
|
|
70
|
+
// A gate rather than a warning even without `--run`: the release manifest validates the spec on the way out, so
|
|
71
|
+
// there is no artifact to produce either. Same class as the agent-directory-name gate above.
|
|
72
|
+
throw new DeployGate(`the model in ${model.source} is not a "provider/modelId" spec: ${JSON.stringify(model.invalid)}`);
|
|
91
73
|
}
|
|
92
|
-
|
|
93
|
-
|
|
74
|
+
if (!model.spec) {
|
|
75
|
+
const issue = `no model resolves for this deployment — set \`model: "provider/id"\` in fastagent.config.ts (it travels ` +
|
|
76
|
+
`in the image), or FASTAGENT_MODEL in ${valueFile} (deploy records that one in the release manifest)` +
|
|
77
|
+
// Whoever has the variable set right here sees `fastagent info` report a model, so "no model resolves" reads
|
|
78
|
+
// like a bug until the message says which environment was read. It states that fact WITHOUT attributing the
|
|
79
|
+
// value: it may be the operator's shell, or the first-run picker's own pick a second earlier (which prints
|
|
80
|
+
// its own "set `model:` in your config" hint), and the two remedies are the two sources named above.
|
|
81
|
+
(process.env.FASTAGENT_MODEL
|
|
82
|
+
? `. Note that a FASTAGENT_MODEL in the environment running deploy is not one of those sources — it ` +
|
|
83
|
+
`belongs to this machine, not to the deployment`
|
|
84
|
+
: ``);
|
|
85
|
+
report.issue(issue);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
report.note(`model ${model.spec} (source: ${model.source})`);
|
|
89
|
+
}
|
|
90
|
+
const modelSpec = model.spec;
|
|
91
|
+
// Time triggers (static routines or self-scheduling) need a machine kept running, and a declared schedule also
|
|
92
|
+
// decides whether `POST /run` mounts — so the load happens HERE, before the warning that has to name it.
|
|
93
|
+
// Loaded, not just listed: the same load answers "are there time triggers" AND "what did they declare they need".
|
|
94
|
+
// A file that FAILED to load still counts as a trigger — the author will fix it, and a plan that scaled to zero
|
|
95
|
+
// because a cron was broken on deploy day would sleep through it afterwards.
|
|
96
|
+
const loadedRoutines = await loadRoutines(agentDir);
|
|
97
|
+
// What the PUBLIC host URL answers with no authentication of ours, NAMED FROM WHAT WILL ACTUALLY MOUNT (through the
|
|
98
|
+
// same `shouldServeRun` the assembly uses), never from the host alone: listing an endpoint this deployment does not
|
|
99
|
+
// serve is how an operator learns to skim past every deploy warning — the same reason `publicUrl` exists.
|
|
100
|
+
const unauthenticated = describeAnonymousSurface({
|
|
101
|
+
invoke: config.http?.invoke !== false,
|
|
102
|
+
run: loadedRoutines.routines.length > 0 && shouldServeRun(config.http),
|
|
103
|
+
...(config.sessionControl === true ? { controlPrefix: CONTROL_PREFIX } : {}),
|
|
104
|
+
});
|
|
105
|
+
if (publicUrl && unauthenticated.length > 0) {
|
|
106
|
+
report.warn(`the deployed box answers ${unauthenticated.join(" and ")} at its public URL, UNAUTHENTICATED — ` +
|
|
107
|
+
`fastagent authenticates nothing. Put a gateway, an IdP-backed proxy or a private network in front of that ` +
|
|
108
|
+
`URL (docs/design/session-control.md §14)`);
|
|
109
|
+
}
|
|
110
|
+
// Known channel kinds only — a custom channel's webhook (and, unless it declared them, its secrets) are unknown to
|
|
111
|
+
// us; note and let the author wire them.
|
|
94
112
|
const inspected = await inspectChannels(agentDir);
|
|
95
113
|
if (inspected.failures.length > 0) {
|
|
96
114
|
throw new Error(`cannot inspect channel modules: ${inspected.failures.map((failure) => `${failure.label}: ${failure.message}`).join("; ")}`);
|
|
@@ -99,46 +117,45 @@ export async function preflightDeploy(input) {
|
|
|
99
117
|
for (const { name, ingress } of channels) {
|
|
100
118
|
if (CHANNEL_KINDS.includes(name))
|
|
101
119
|
continue;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: `route channel "${name}" is custom — configure its secrets and webhook yourself`,
|
|
107
|
-
});
|
|
120
|
+
const secretsPart = `its variables travel from ${valueFile} like every other`;
|
|
121
|
+
report.note(ingress === "long-connection"
|
|
122
|
+
? `long-connection channel "${name}" is custom — ${secretsPart}; generated deploy plans keep the process running and skip webhook registration`
|
|
123
|
+
: `route channel "${name}" is custom — ${secretsPart}; configure its webhook yourself`);
|
|
108
124
|
}
|
|
109
125
|
const longConnectionChannels = channels.filter((c) => c.ingress === "long-connection").map((c) => c.name);
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
126
|
+
// A ROUTINE IS NOT A CRON. `cron` is a field, so counting routine FILES answered a different question: a
|
|
127
|
+
// definition whose only routine is reached by name (`POST /run`) would pin one machine up forever and print a
|
|
128
|
+
// note about a cron instant it does not have. `deploy agentcore` already filtered the same way when it turned
|
|
129
|
+
// routines into EventBridge rules; this is the other reader of that fact, and they must agree.
|
|
130
|
+
//
|
|
131
|
+
// A FAILED file still counts, on the conservative side: it may well declare a cron, and a plan that scaled to
|
|
132
|
+
// zero because the file did not parse would hide that behind silence.
|
|
133
|
+
const hasCron = loadedRoutines.routines.some((r) => r.cron !== undefined) || loadedRoutines.failures.length > 0;
|
|
115
134
|
if (longConnectionChannels.length > 0 && !externalClock) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
text: `long-connection channel present (${longConnectionChannels.join(", ")}) — a GENERATED plan keeps one machine running ` +
|
|
119
|
-
`(an outbound connection cannot wake a scaled-to-zero service).`,
|
|
120
|
-
});
|
|
135
|
+
report.note(`long-connection channel present (${longConnectionChannels.join(", ")}) — a GENERATED plan keeps one machine running ` +
|
|
136
|
+
`(an outbound connection cannot wake a scaled-to-zero service).`);
|
|
121
137
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
// A cron has an external substitute, and an operator who is paying for an idle box should be told so — unless a
|
|
139
|
+
// long connection pins the box anyway, which residency.ts decides.
|
|
140
|
+
if (hasCron && !externalClock) {
|
|
141
|
+
const wayOut = residencyFor({ channels, hasCron })?.reason === CRON_CAN_BE_EXTERNAL;
|
|
142
|
+
report.note(`routines/ present — a GENERATED plan keeps one machine running (nothing wakes this box at a cron ` +
|
|
143
|
+
`instant).` +
|
|
144
|
+
(wayOut
|
|
145
|
+
? ` To scale to zero instead, keep the time in a scheduler you own and let it call ` +
|
|
146
|
+
`\`POST /run\` (an API that runs one declared unit of work by name — docs/api-reference.md#post-run).`
|
|
147
|
+
: ""));
|
|
128
148
|
}
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
const authPath = resolveAuthPath(agentDir
|
|
134
|
-
const models = await createPiModelRuntime({ agentDir, authPath });
|
|
149
|
+
await checkMachineModels(agentDir, modelSpec, report);
|
|
150
|
+
// Probe auth from the SAME project-level file the opener/login use, on the registry the DEPLOYED agent has: the
|
|
151
|
+
// machine's models.json does not ship, so an entry there (a gateway over a built-in provider, a key) must not decide
|
|
152
|
+
// how the credential reaches the host.
|
|
153
|
+
const authPath = resolveAuthPath(agentDir);
|
|
154
|
+
const models = await createPiModelRuntime({ agentDir, auth: { path: authPath }, machineLayer: false });
|
|
135
155
|
let modelAuth = modelSpec ? await probeAuthSource(models, modelSpec) : undefined;
|
|
136
156
|
let modelKeyInDefinition = false;
|
|
137
|
-
// probeAuthSource answers "is it authenticated here", which is not the deploy question ("how does the
|
|
138
|
-
//
|
|
139
|
-
// env-var name — so without this the gate below sees no credential and stops the deploy with two
|
|
140
|
-
// remedies that are both wrong for such an agent: `fastagent login` cannot serve a custom provider,
|
|
141
|
-
// and the key is already in the environment.
|
|
157
|
+
// probeAuthSource answers "is it authenticated here", which is not the deploy question ("how does the credential
|
|
158
|
+
// REACH the host").
|
|
142
159
|
if (modelSpec && !isEnvKey(modelAuth)) {
|
|
143
160
|
const carry = modelCredentialCarry(models, modelSpec);
|
|
144
161
|
if (carry.envVar)
|
|
@@ -146,9 +163,19 @@ export async function preflightDeploy(input) {
|
|
|
146
163
|
else
|
|
147
164
|
modelKeyInDefinition = carry.inDefinition;
|
|
148
165
|
}
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
166
|
+
// Reported, not refused. Whether a string is a credential is the AUTHOR's knowledge: pi's docs prescribe
|
|
167
|
+
// `"apiKey": "ollama"` for a keyless local server, and no static rule separates that from a leaked key. The
|
|
168
|
+
// `.dockerignore` check next to this one IS a gate because it catches a packing rule putting FastAgent's OWN
|
|
169
|
+
// `.secrets/auth.json` into the image — the framework's doing. This is the author's own committed file.
|
|
170
|
+
// Asked of EVERY provider, not just the selected model's: the file ships whole.
|
|
171
|
+
const literalKeys = await literalKeyProviders(agentDir);
|
|
172
|
+
if (literalKeys.length > 0) {
|
|
173
|
+
report.warn(`${AGENT_MODELS_FILE} carries a literal apiKey for ${literalKeys.map((id) => `"${id}"`).join(", ")} — that ` +
|
|
174
|
+
`file ships inside the image, where anyone who can pull it reads the layer. If it is a credential, use ` +
|
|
175
|
+
`"$YOUR_ENV_VAR" (deploy carries it like any provider key) or "!command" (it runs on the box and never ` +
|
|
176
|
+
`travels); a placeholder for a keyless local server is fine as it is.`);
|
|
177
|
+
}
|
|
178
|
+
// Container facts (shared by every host) + the warnings that follow.
|
|
152
179
|
const hasPackageJson = await exists(join(agentDir, "package.json"));
|
|
153
180
|
const pkg = await readPackageJson(agentDir);
|
|
154
181
|
const { runtime, bunVersion, hasLockfile } = detectRuntime(agentDir, pkg);
|
|
@@ -156,300 +183,172 @@ export async function preflightDeploy(input) {
|
|
|
156
183
|
const runner = runtime === "bun" ? "bun run fastagent" : "./node_modules/.bin/fastagent";
|
|
157
184
|
const hasOtherLock = runtime === "node" &&
|
|
158
185
|
((await exists(join(agentDir, "pnpm-lock.yaml"))) || (await exists(join(agentDir, "yarn.lock"))));
|
|
159
|
-
// Does the baked workspace ship a `.git`?
|
|
160
|
-
// and the plans' runbook wording — the write-back loop needs the history AND the binary together.
|
|
186
|
+
// Does the baked workspace ship a `.git`?
|
|
161
187
|
const shipsGit = await exists(join(workspace, ".git"));
|
|
162
|
-
// After the facts: the deps sentence must match the agent's actual shape (a markdown-only agent has
|
|
163
|
-
//
|
|
188
|
+
// After the facts: the deps sentence must match the agent's actual shape (a markdown-only agent has no package.json
|
|
189
|
+
// and installs nothing — the note must not point at a file that doesn't exist).
|
|
164
190
|
const deps = hasPackageJson
|
|
165
|
-
? `only the agent's deps (${agentPrefix}package.json) are installed
|
|
191
|
+
? `only the agent's deps (${agentPrefix}package.json) are installed — the workspace's own deps are the agent's runtime concern`
|
|
166
192
|
: `the agent has no package.json, so no deps are installed (the pinned global CLI serves the directory)`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
text: `the whole directory is baked as the agent's workspace (WYSIWYG — what you see is what ships, ` +
|
|
175
|
-
`git or not, clean or not); ${deps}. ${durability}.`,
|
|
176
|
-
});
|
|
177
|
-
// A code agent with no lockfile builds via a non-frozen install (ranges resolve at build time) — not
|
|
178
|
-
// reproducible. A pnpm/yarn user gets an accurate message (their lockfile is ignored by the npm Dockerfile).
|
|
193
|
+
// What a RELEASE does — host-neutral, because how long the storage under it lives is the host's own answer and its
|
|
194
|
+
// runbook gives it (a Fly volume outlives every deploy; AgentCore's mount does not).
|
|
195
|
+
report.note(`the whole directory is baked as the agent's workspace (WYSIWYG — what you see is what ships, ` +
|
|
196
|
+
`git or not, clean or not); ${deps}. The image seeds the storage once; a later release replaces ` +
|
|
197
|
+
`only ${agentPrefix} and leaves the rest of the workspace, state and credentials in place — for ` +
|
|
198
|
+
`how long, see this host's storage note below`);
|
|
199
|
+
// A code agent with no lockfile builds via a non-frozen install (ranges resolve at build time) — not reproducible.
|
|
179
200
|
if (hasPackageJson && !hasLockfile) {
|
|
180
201
|
const lock = runtime === "bun" ? "bun.lock" : "package-lock.json";
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
: `no ${lock} — the image build resolves deps at build time (not reproducible). ` +
|
|
187
|
-
`Run \`${install}\` and commit the lockfile for pinned redeploys.`,
|
|
188
|
-
});
|
|
202
|
+
report.warn(hasOtherLock
|
|
203
|
+
? `the generated Dockerfile is npm-based — your pnpm/yarn lockfile is NOT used (build runs ` +
|
|
204
|
+
`\`npm install\`, not reproducible). Edit the Dockerfile for your package manager, or vendor a package-lock.json.`
|
|
205
|
+
: `no ${lock} — the image build resolves deps at build time (not reproducible). ` +
|
|
206
|
+
`Run \`${install}\` and commit the lockfile for pinned redeploys.`);
|
|
189
207
|
}
|
|
190
|
-
// The code-path Dockerfile runs `${runner}
|
|
191
|
-
// registry — so a package.json missing it means the container fails at start (no bin to run).
|
|
208
|
+
// The code-path Dockerfile runs `${runner}`.
|
|
192
209
|
if (hasPackageJson && !("@fastagent-sh/fastagent" in { ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
text: `package.json does not list @fastagent-sh/fastagent — the image's \`${runner}\` has no local bin to run, ` +
|
|
196
|
-
`so the container fails at start. Add it to dependencies and re-run \`${install}\`.`,
|
|
197
|
-
});
|
|
210
|
+
report.warn(`package.json does not list @fastagent-sh/fastagent — the image's \`${runner}\` has no local bin to run, ` +
|
|
211
|
+
`so the container fails at start. Add it to dependencies and re-run \`${install}\`.`);
|
|
198
212
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
// dropping the agent dir ships a context with no persona/config (the box crash-loops), and an
|
|
203
|
-
// unexcluded secrets path BAKES CREDENTIALS INTO THE IMAGE. The other two are advisory: the build
|
|
204
|
-
// machine's node_modules (native binaries for YOUR OS) clobbering the image's, and an excluded .git
|
|
205
|
-
// killing the agent's pull/push loop (a legitimate slimming choice). Not force-gated — the file is
|
|
206
|
-
// kept even under --force.
|
|
207
|
-
// Which paths INSIDE the build context hold secrets — resolved, then made workspace-relative (the
|
|
208
|
-
// context root). An external secrets dir (a mounted volume) is outside the context: nothing to check
|
|
209
|
-
// and nothing to exclude. Also fed to the generated .dockerignore, so a custom in-tree dir is
|
|
210
|
-
// excluded by PATH even though its name is not `.secrets`.
|
|
211
|
-
const inContext = (p) => {
|
|
212
|
-
const rel = relative(workspace, p);
|
|
213
|
-
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? undefined : rel.split(sep).join("/");
|
|
214
|
-
};
|
|
215
|
-
// The secrets DIR is the unit of RESPONSIBILITY, but never the unit of the leak QUESTION below: the
|
|
216
|
-
// generated ignore excludes the dir's CONTENTS (`**/.secrets/**`) so its two value-free tracked
|
|
217
|
-
// scaffolds can be re-included, and a directory-level question reads that correct file as "not
|
|
218
|
-
// excluded" — the generator's own default output gated its own deploy (field-hit: a fresh
|
|
219
|
-
// kit-layout workspace without --force; --force skips checking our own file, which is why the
|
|
220
|
-
// combination stayed invisible). What leaks is a FILE, so files are what the gate asks about — see
|
|
221
|
-
// secretDirFiles below, which enumerates what is actually inside (an atomic-write temp beside
|
|
222
|
-
// auth.json, a second key file, an editor backup of `.env`: the dir-as-unit worry, covered per
|
|
223
|
-
// file). The auth path adds an entry only when an override puts it OUTSIDE that dir. An external
|
|
224
|
-
// secrets dir (the deployed posture: a mounted volume) is outside the context — nothing to check,
|
|
225
|
-
// nothing to exclude.
|
|
226
|
-
const secretsRel = inContext(resolveSecretsDir(agentDir));
|
|
227
|
-
const authRel = inContext(authPath);
|
|
228
|
-
const authElsewhere = authRel !== undefined && (secretsRel === undefined || !authRel.startsWith(`${secretsRel}/`));
|
|
229
|
-
const secretPaths = [...(secretsRel ? [secretsRel] : []), ...(authElsewhere ? [authRel] : [])];
|
|
230
|
-
// ONE rule for every checked path: a file that is not there cannot be baked, so gating on it would
|
|
231
|
-
// be a refusal about a spelling rather than about what would ship (an agent that has never run
|
|
232
|
-
// `login` has no auth.json). The generated .dockerignore still excludes them unconditionally —
|
|
233
|
-
// cheap, and correct the moment they appear.
|
|
234
|
-
const present = async (rels) => {
|
|
235
|
-
const found = [];
|
|
236
|
-
for (const rel of rels)
|
|
237
|
-
if (await exists(join(workspace, rel)))
|
|
238
|
-
found.push(rel);
|
|
239
|
-
return found;
|
|
240
|
-
};
|
|
241
|
-
// State gets the same treatment (a custom in-tree FASTAGENT_STATE_DIR is invisible to the
|
|
242
|
-
// name-based `**/.state`), at warn level: shipping stale sessions is waste, not a credential leak.
|
|
243
|
-
const stateRel = inContext(resolveStateRoot(agentDir));
|
|
244
|
-
// Existence gates the WARNING, never the generated exclude (same split as secretPaths vs
|
|
245
|
-
// leakCandidates): an agent that has never run has no `.state/`, so telling its author a kept ignore
|
|
246
|
-
// file fails to exclude one is a remark about a spelling — while the file we generate must still carry
|
|
247
|
-
// the line, since it is written once and correct the moment the directory appears.
|
|
248
|
-
const stateShips = stateRel !== undefined && (await exists(join(workspace, stateRel))) ? stateRel : undefined;
|
|
249
|
-
// The `.env` family at the two levels fastagent is RESPONSIBLE for: the agent dir and the workspace
|
|
250
|
-
// root. Asking only about a root-level `.env` missed both halves that matter — an `<agent>/.env`, the
|
|
251
|
-
// file habit puts there (env.ts warns about it by name), and the `.env.local` / `.env.production`
|
|
252
|
-
// spellings. DISCOVERED rather than spelled, so the existing rule still holds: only a file that is
|
|
253
|
-
// there can be baked, so only it is gated.
|
|
254
|
-
//
|
|
255
|
-
// Deliberately NOT the recursive `**/.env` the generated file carries: walking a whole monorepo for
|
|
256
|
-
// credential files is a secret scanner, not a placement pre-flight, and a bounded walk would be a
|
|
257
|
-
// heuristic pretending to be a guarantee. The division of responsibility is the honest one — the file
|
|
258
|
-
// WE generate covers every level; an author who keeps their own owns its coverage of their own tree,
|
|
259
|
-
// and this gate speaks only for the paths fastagent itself puts credentials in.
|
|
260
|
-
const dotEnvFiles = async (relDir) => {
|
|
261
|
-
const names = await readdir(join(workspace, relDir || ".")).catch(() => []);
|
|
262
|
-
// POSIX separators, like every other context-relative path here (`inContext`): these strings are
|
|
263
|
-
// matched against dockerignore patterns, and a Windows `fastagent\.env` would match none of them —
|
|
264
|
-
// silently turning the one check whose failure mode is "credentials in a published image" into a
|
|
265
|
-
// no-op.
|
|
266
|
-
return names
|
|
267
|
-
.filter((n) => (n === ".env" || n.startsWith(".env.")) && n !== ".env.example")
|
|
268
|
-
.map((n) => join(relDir, n).split(sep).join("/"));
|
|
269
|
-
};
|
|
270
|
-
const envFiles = (await Promise.all([...new Set(["", agentPrefix])].map(dotEnvFiles))).flat();
|
|
271
|
-
// Everything ACTUALLY inside the secrets dir, minus the two tracked scaffolds the image ships on
|
|
272
|
-
// purpose (they carry no values; the generated ignore re-includes them by name). Existence is the
|
|
273
|
-
// enumeration itself — readdir lists exactly what could be baked — and a hand-written ignore that
|
|
274
|
-
// misses the dir now gates NAMING the leaking file, a better diagnostic than pointing at a
|
|
275
|
-
// directory. Recurses: a subdirectory inside .secrets is unusual but its files leak all the same.
|
|
276
|
-
const secretDirFiles = async (dirRel) => {
|
|
277
|
-
const entries = await readdir(join(workspace, dirRel), { withFileTypes: true }).catch(() => []);
|
|
278
|
-
const files = [];
|
|
279
|
-
for (const entry of entries) {
|
|
280
|
-
if (entry.name === ".gitignore" || entry.name === ".env.example")
|
|
281
|
-
continue;
|
|
282
|
-
if (entry.isDirectory())
|
|
283
|
-
files.push(...(await secretDirFiles(`${dirRel}/${entry.name}`)));
|
|
284
|
-
else
|
|
285
|
-
files.push(`${dirRel}/${entry.name}`);
|
|
286
|
-
}
|
|
287
|
-
return files;
|
|
288
|
-
};
|
|
289
|
-
const leakCandidates = [
|
|
290
|
-
...(secretsRel ? await secretDirFiles(secretsRel) : []),
|
|
291
|
-
...(await present(authElsewhere && authRel !== undefined ? [authRel] : [])),
|
|
292
|
-
...envFiles,
|
|
293
|
-
];
|
|
294
|
-
// Same existence rule: a node_modules that is not there cannot be uploaded.
|
|
295
|
-
const depDirs = await present([...new Set([`${agentPrefix}node_modules`, "node_modules"])]);
|
|
296
|
-
const machineryPaths = [...secretPaths, ...(stateRel ? [stateRel] : [])];
|
|
297
|
-
// BOTH ignore files deploy emits get the same interrogation. The workspace-root one is what
|
|
298
|
-
// flyctl/railway's packers read; the per-Dockerfile one is what BuildKit PREFERS for a plain
|
|
299
|
-
// `docker build` (so it is the file that actually decides `deploy docker`). Checking only the root one
|
|
300
|
-
// left the credential gate not covering the path it was written for.
|
|
301
|
-
for (const rel of [".dockerignore", `${agentPrefix}Dockerfile.dockerignore`]) {
|
|
302
|
-
const kept = await readTextIfExists(join(workspace, rel));
|
|
303
|
-
if (kept === undefined)
|
|
304
|
-
continue;
|
|
305
|
-
// One WE generated is regenerated by this very run under --force, so checking the stale content on
|
|
306
|
-
// disk would gate a deploy on a file about to be replaced. Ours + --force: skip. Ours WITHOUT --force
|
|
307
|
-
// is still checked (it is what would ship), but the remedy differs: hand-adding lines to fastagent's
|
|
308
|
-
// own output is not the fix — regenerating it is.
|
|
309
|
-
const keptIsOurs = isGeneratedDockerignore(kept);
|
|
310
|
-
if (force && keptIsOurs)
|
|
311
|
-
continue;
|
|
312
|
-
const remedy = (lines) => keptIsOurs
|
|
313
|
-
? `Re-run with --force to regenerate it.`
|
|
314
|
-
: `Add ${lines.map((p) => `\`${p}\``).join(" and ")} before deploying (the same lines the generated ${rel} writes).`;
|
|
315
|
-
const excluded = dockerignoreMatcher(kept);
|
|
316
|
-
// Asked as a DIRECTORY (trailing slash), which is what it is. A bare-name test answers `false` for
|
|
317
|
-
// the `fastagent/` spelling — a directory-only pattern, and the one a hand-written ignore file is
|
|
318
|
-
// most likely to carry — so the agent would be dropped from the context with no warning at all. The
|
|
319
|
-
// pairing this replaced (the directory AND a file inside it) was dead weight rather than a
|
|
320
|
-
// safeguard: git's rule that a file under an excluded directory cannot be re-included is
|
|
321
|
-
// implemented by the matcher, so `excluded(dir)` already implies `excluded(dir/persona.md)` and the
|
|
322
|
-
// second test could never change the answer. What it was aimed at — an allowlist (`*` +
|
|
323
|
-
// `!fastagent` + `!fastagent/**`) that re-includes the agent — is handled by the first test alone,
|
|
324
|
-
// which reads `false` there, as it should.
|
|
325
|
-
if (nested && excluded(`${basename(agentDir)}/`)) {
|
|
326
|
-
const text = `your ${rel} (kept) excludes \`${basename(agentDir)}\` — the build context would ship WITHOUT the ` +
|
|
327
|
-
`agent entirely (the deployed box has no persona/config and crash-loops). Remove that rule ` +
|
|
328
|
-
`before deploying.`;
|
|
329
|
-
if (run)
|
|
330
|
-
return { ok: false, gate: text };
|
|
331
|
-
messages.push({ level: "warn", text });
|
|
332
|
-
}
|
|
333
|
-
// Resolved paths, not spellings: dockerignore patterns are root-anchored (unlike .gitignore), so a
|
|
334
|
-
// bare `.secrets` line does not cover `fastagent/.secrets` — and FASTAGENT_SECRETS_DIR /
|
|
335
|
-
// FASTAGENT_AUTH_PATH can put credentials anywhere in the baked tree, where the name-based excludes
|
|
336
|
-
// never reach. This is the one check whose failure mode is "credentials in a published image".
|
|
337
|
-
const leaks = leakCandidates.filter((p) => !excluded(p));
|
|
338
|
-
if (leaks.length > 0) {
|
|
339
|
-
const text = `your ${rel} (kept) does not exclude ${leaks.map((p) => `\`${p}\``).join(", ")} — the build ` +
|
|
340
|
-
`context would BAKE SECRETS INTO THE IMAGE. ${remedy(leaks.map((p) => `/${p}`))}`;
|
|
341
|
-
if (run)
|
|
342
|
-
return { ok: false, gate: text };
|
|
343
|
-
messages.push({ level: "warn", text });
|
|
344
|
-
}
|
|
345
|
-
if (stateShips && !excluded(`${stateShips}/sessions`)) {
|
|
346
|
-
messages.push({
|
|
347
|
-
level: "warn",
|
|
348
|
-
text: `your ${rel} (kept) does not exclude \`${stateRel}\` — the build machine's sessions/channel state would ship in the image. ${remedy([`/${stateRel}`])}`,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
// Both the agent's own node_modules and the workspace's: either would upload the build machine's
|
|
352
|
-
// deps (native binaries for YOUR OS) and clobber the image's freshly-installed ones. Named by the
|
|
353
|
-
// PATHS actually found unexcluded, like every other check here — not by a rule's spelling.
|
|
354
|
-
const unexcludedDeps = depDirs.filter((p) => !excluded(`${p}/.package-lock.json`));
|
|
355
|
-
if (unexcludedDeps.length > 0) {
|
|
356
|
-
messages.push({
|
|
357
|
-
level: "warn",
|
|
358
|
-
text: `your ${rel} does not exclude ${unexcludedDeps.map((p) => `\`${p}\``).join(" or ")} — the ` +
|
|
359
|
-
`build machine's deps (native binaries for YOUR OS) would be uploaded and clobber the image's ` +
|
|
360
|
-
`freshly-installed ones. ${remedy(unexcludedDeps.map((p) => `/${p}`))}`,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
if (excluded(".git/HEAD")) {
|
|
364
|
-
messages.push({
|
|
365
|
-
level: "note",
|
|
366
|
-
text: `your ${rel} excludes .git — the baked copy ships WITHOUT history/remote, so the agent ` +
|
|
367
|
-
`cannot pull/commit/push it; it must \`git clone\` its repo in the workspace instead (or remove the .git line).`,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// Write-back mechanics are fastagent's (the policy is the persona's): the image carries the git
|
|
372
|
-
// BINARY iff the baked workspace ships a `.git` (history without the binary is a
|
|
373
|
-
// dead loop; the binary without history is dead weight). A non-git workspace that still needs git
|
|
374
|
-
// (the agent clones repos as its job) declares config.deploy.apt: ["git"] explicitly. Merged with
|
|
375
|
-
// (never duplicating) config.deploy.apt.
|
|
213
|
+
const paths = await buildContextPaths(workspace, agentDir, agentPrefix, authPath);
|
|
214
|
+
await checkKeptIgnoreFiles({ workspace, agentDir, agentPrefix, force, paths }, report);
|
|
215
|
+
// Write-back mechanics are fastagent's (the policy is the persona's).
|
|
376
216
|
const apt = shipsGit ? [...new Set(["git", ...(config.deploy?.apt ?? [])])] : config.deploy?.apt;
|
|
377
217
|
const container = {
|
|
218
|
+
releaseId: randomUUID(),
|
|
378
219
|
agentPrefix,
|
|
379
|
-
machineryPaths,
|
|
220
|
+
machineryPaths: paths.machineryPaths,
|
|
380
221
|
hasPackageJson,
|
|
381
222
|
runtime,
|
|
382
223
|
bunVersion,
|
|
383
224
|
hasLockfile,
|
|
384
225
|
version: await fastagentVersion(),
|
|
385
226
|
apt,
|
|
227
|
+
...(model.envValue !== undefined ? { modelSpec: model.envValue } : {}),
|
|
386
228
|
shipsGit,
|
|
387
229
|
};
|
|
388
|
-
const port = config.http?.port ??
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (run)
|
|
403
|
-
return { ok: false, gate: issue };
|
|
404
|
-
messages.push({ level: "warn", text: issue });
|
|
405
|
-
}
|
|
406
|
-
// What the agent declared it needs on the box (fastagent.config deploy.secrets) — carried like channel
|
|
407
|
-
// secrets: listed in the runbook, set from the local env under --run, gated if a value is missing.
|
|
408
|
-
const extraSecrets = [...(config.deploy?.secrets ?? [])];
|
|
409
|
-
// The plane's bearer token is the DEPLOYMENT's secret, not the container's: minted inside the box it
|
|
410
|
-
// is unreadable from outside and replaced on every restart, which is what makes a public /control/*
|
|
411
|
-
// unusable. Carried like any declared secret — listed in the runbook, taken from the local env under
|
|
412
|
-
// --run, gated when absent (never minted: a value minted per deploy rotates under its holder).
|
|
413
|
-
if (config.sessionControl === true)
|
|
414
|
-
extraSecrets.push(CONTROL_TOKEN_ENV);
|
|
415
|
-
// deploy.apt only shapes the GENERATED Dockerfile. Warn ONLY when the kept Dockerfile is HAND-WRITTEN
|
|
416
|
-
// (its apt won't include these) — a fastagent-generated one is handled by writeArtifacts. Don't suggest
|
|
417
|
-
// --force here: it would overwrite the user's hand-written file.
|
|
418
|
-
const dockerfileHome = join(agentDir, "Dockerfile");
|
|
419
|
-
if (config.deploy?.apt?.length && !force && (await exists(dockerfileHome))) {
|
|
420
|
-
if (!isGeneratedDockerfile(await readFile(dockerfileHome, "utf8"))) {
|
|
421
|
-
messages.push({
|
|
422
|
-
level: "warn",
|
|
423
|
-
text: `kept your hand-written Dockerfile — deploy.apt (${config.deploy.apt.join(", ")}) is ` +
|
|
424
|
-
`NOT applied; install those packages in your Dockerfile.`,
|
|
425
|
-
});
|
|
426
|
-
}
|
|
230
|
+
const port = config.http?.port ?? DEFAULT_HTTP_PORT;
|
|
231
|
+
// EVERYTHING the definition declared it needs, from wherever it was declared. Read through the SAME resolver
|
|
232
|
+
// dev/start mount with, so "which tool declarations count" has one answer (config.tools declare too; a shadowed
|
|
233
|
+
// file's declaration is dropped in both places).
|
|
234
|
+
const resolvedTools = await resolveAgentTools(config, agentDir, workspace);
|
|
235
|
+
// A code input we could not READ is a code input whose declarations we cannot carry — and the box
|
|
236
|
+
// WILL read it (its deps are installed there), so its gate fires after the deploy reported success:
|
|
237
|
+
// a crash loop, which is the failure mode this whole mechanism exists to move to build time. Under
|
|
238
|
+
// `--run` that is a gate, like a channel that fails to inspect; generate-only warns, since the
|
|
239
|
+
// operator may be producing artifacts from a machine that never installed the agent's deps.
|
|
240
|
+
for (const failure of [...resolvedTools.toolFailures, ...loadedRoutines.failures]) {
|
|
241
|
+
const issue = `${failure.label} failed to load (${failure.message}) — any secrets it declares cannot be carried ` +
|
|
242
|
+
`to the host, so the deployed box would refuse to start`;
|
|
243
|
+
report.issue(issue);
|
|
427
244
|
}
|
|
245
|
+
const declaredSecrets = [
|
|
246
|
+
...allSecrets(resolvedTools.toolSecrets),
|
|
247
|
+
...allSecrets(loadedRoutines.secrets),
|
|
248
|
+
...allSecrets(inspected.secrets),
|
|
249
|
+
];
|
|
250
|
+
await checkKeptDockerfile(agentDir, config, model.envValue, valueFile, report);
|
|
428
251
|
return {
|
|
429
|
-
ok: true,
|
|
430
|
-
messages,
|
|
431
252
|
channels,
|
|
432
|
-
|
|
253
|
+
hasCron,
|
|
254
|
+
values,
|
|
255
|
+
valueFile,
|
|
433
256
|
modelAuth,
|
|
434
257
|
modelKeyInDefinition,
|
|
435
258
|
authPath,
|
|
436
259
|
container,
|
|
437
260
|
port,
|
|
438
|
-
|
|
261
|
+
declaredSecrets,
|
|
262
|
+
secrets: deploymentSecrets(modelAuth, declaredSecrets, values, valueFile),
|
|
439
263
|
};
|
|
440
264
|
}
|
|
441
265
|
/**
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
|
|
446
|
-
|
|
266
|
+
* The machine's models.json is this box's environment, not the artifact: whatever the model takes from it is
|
|
267
|
+
* absent wherever the agent is deployed. Said in so many words here; the credential probe already reads the deployed
|
|
268
|
+
* registry.
|
|
269
|
+
*/
|
|
270
|
+
async function checkMachineModels(agentDir, modelSpec, report) {
|
|
271
|
+
const machine = await machineModels(agentDir);
|
|
272
|
+
const provider = modelSpec ? providerOf(modelSpec) : undefined;
|
|
273
|
+
if (provider === undefined || !machine?.inherited.includes(provider))
|
|
274
|
+
return;
|
|
275
|
+
if (isBuiltinProvider(provider)) {
|
|
276
|
+
report.warn(`model "${modelSpec}" takes its "${provider}" entry from ${machine.path}, the machine's models.json, which ` +
|
|
277
|
+
`does not ship — the deployed agent runs pi's built-in "${provider}" without it. Declare the entry in the ` +
|
|
278
|
+
`agent's own ${AGENT_MODELS_FILE} to deploy it.`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// No built-in to fall back on: the deployed agent cannot resolve the model at all.
|
|
282
|
+
const issue = `model "${modelSpec}" exists only in ${machine.path}, the machine's models.json, which does not ship — the ` +
|
|
283
|
+
`deployed agent would fail with an unknown model. Declare "${provider}" in the agent's own ` +
|
|
284
|
+
`${AGENT_MODELS_FILE} to deploy it.`;
|
|
285
|
+
report.issue(issue);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* What a KEPT hand-written Dockerfile drops. `deploy.apt` is the obvious one; the resolved model is the one that
|
|
290
|
+
* looks safe and is not: the manifest is always written, but only the generated Dockerfile sets
|
|
291
|
+
* FASTAGENT_RELEASE_FILE, and without it `prepareStartWorkspace` never reads the manifest — so a model that lives
|
|
292
|
+
* ONLY in the value file would be reported here and absent on the box.
|
|
293
|
+
*
|
|
294
|
+
* NOT conditioned on `!force`: `writeArtifacts` refuses a file it did not generate whatever the flag says, so a
|
|
295
|
+
* hand-written Dockerfile survives `--force` and drops exactly the same things. Short-circuiting here let
|
|
296
|
+
* `--run --force` ship the crash-loop this gate exists to stop.
|
|
297
|
+
*/
|
|
298
|
+
async function checkKeptDockerfile(agentDir, config,
|
|
299
|
+
/** The model the release manifest carries (set only when the value file named it). */
|
|
300
|
+
modelFromValueFile, valueFile, report) {
|
|
301
|
+
const dockerfileHome = join(agentDir, "Dockerfile");
|
|
302
|
+
const dockerfileText = (await exists(dockerfileHome)) ? await readFile(dockerfileHome, "utf8") : undefined;
|
|
303
|
+
if (dockerfileText === undefined || isGeneratedDockerfile(dockerfileText))
|
|
304
|
+
return;
|
|
305
|
+
if (config.deploy?.apt?.length) {
|
|
306
|
+
report.warn(`kept your hand-written Dockerfile — deploy.apt (${config.deploy.apt.join(", ")}) is ` +
|
|
307
|
+
`NOT applied; install those packages in your Dockerfile.`);
|
|
308
|
+
}
|
|
309
|
+
// The INSTRUCTION is the question, not the file's authorship: `prepareStartWorkspace` returns early without
|
|
310
|
+
// FASTAGENT_RELEASE_FILE, so a Dockerfile that sets it reads the manifest whoever wrote it. This gates rather
|
|
311
|
+
// than warns because it is about FastAgent's OWN delivery arriving — the model would be reported here and
|
|
312
|
+
// missing on the box.
|
|
313
|
+
// Anywhere in an `ENV` instruction, not just first: `ENV A=1 FASTAGENT_RELEASE_FILE=/app/x` is ordinary
|
|
314
|
+
// Dockerfile style and hard-refusing it would be a false gate. A backslash continuation still reads as absent
|
|
315
|
+
// (covering it means joining lines first) — the remaining over-strict edge.
|
|
316
|
+
if (modelFromValueFile !== undefined && !/^\s*ENV\s[^\n]*\bFASTAGENT_RELEASE_FILE[=\s]/m.test(dockerfileText)) {
|
|
317
|
+
const issue = `your Dockerfile does not set FASTAGENT_RELEASE_FILE, and the model comes from ${valueFile} — it travels ` +
|
|
318
|
+
`in the release manifest, which is only read when that ENV points at it. Add it (see a generated ` +
|
|
319
|
+
`Dockerfile), or set \`model\` in fastagent.config.ts so it ships in the config instead.`;
|
|
320
|
+
report.issue(issue);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* WHICH model this deployment runs on, and where that came from.
|
|
325
|
+
*
|
|
326
|
+
* There is ONE precedence chain — `flag > environment > config.model` — and this is it evaluated in the environment
|
|
327
|
+
* BEING DEPLOYED rather than in this machine's. That environment is declared by the value file, so the operator's
|
|
328
|
+
* `process.env` simply is not part of it (the same way `dev` never reads another machine's shell); it needs no rule
|
|
329
|
+
* of its own. `deploy` has no flag layer either, and that follows from the same model rather than from policy: a
|
|
330
|
+
* generated Dockerfile is rewritten on every deploy, so a flag baked into one would silently vanish on the next run
|
|
331
|
+
* that omits it. A file does not.
|
|
332
|
+
*
|
|
333
|
+
* The value file is the half of the deployed environment we can DECLARE. The other half — variables the platform
|
|
334
|
+
* already holds — is still on the box and still outranks the image's own `ENV`, which is exactly why the model is
|
|
335
|
+
* baked rather than delivered as one more platform variable that nothing would ever clear.
|
|
447
336
|
*/
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
337
|
+
function resolveDeployModel(config, values,
|
|
338
|
+
/** The value file AS READ (it follows `FASTAGENT_SECRETS_DIR`), so the reported source is the real one. */
|
|
339
|
+
valueFile) {
|
|
340
|
+
const fromEnv = values.get("FASTAGENT_MODEL");
|
|
341
|
+
const source = fromEnv ? valueFile : "fastagent.config";
|
|
342
|
+
// BOTH sources are checked, at the one point that reads them: a `provider`-less spec resolves to nothing on the
|
|
343
|
+
// box, so letting `config.model` through would ship exactly the crash-loop this resolution exists to prevent —
|
|
344
|
+
// `probeAuthSource` reports "unconfigured" for it here and the failure only appears after deployment.
|
|
345
|
+
const spec = fromEnv || config.model;
|
|
346
|
+
if (spec && !isModelSpec(spec))
|
|
347
|
+
return { source, invalid: spec };
|
|
348
|
+
// `envValue` is what the release manifest records (ContainerInput.modelSpec), so it is set only when the value file
|
|
349
|
+
// is the source: a `config.model` already travels in the config itself. The manifest is rewritten by every deploy,
|
|
350
|
+
// so deleting the line and redeploying simply drops it — nothing stale survives.
|
|
351
|
+
if (fromEnv)
|
|
352
|
+
return { spec: fromEnv, source, envValue: fromEnv };
|
|
353
|
+
return { spec: config.model, source };
|
|
455
354
|
}
|