@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/container.js
CHANGED
|
@@ -1,35 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The portable container — Dockerfile + .dockerignore, host-neutral. Every deploy target ships these
|
|
3
|
-
* unchanged; only the host config (fly.toml / railway.json) and the CLI sequence differ. The image
|
|
4
|
-
* runs `fastagent start /app` on `$PORT` with machinery under `$FASTAGENT_STATE_DIR` /
|
|
5
|
-
* `$FASTAGENT_SECRETS_DIR` — standard container conventions, no host coupling.
|
|
6
|
-
*
|
|
7
|
-
* ONE deploy semantic: bake the WORKSPACE as the image (`COPY . .` — what you see is what ships),
|
|
8
|
-
* with the agent at `./fastagent` inside it. The resolver in the image finds it exactly like dev does.
|
|
9
|
-
* Deps install under `/app/fastagent`, and the Dockerfile lives there too — namespaced so it never
|
|
10
|
-
* collides with one the workspace already owns.
|
|
11
|
-
*/
|
|
1
|
+
/** The portable container — Dockerfile + .dockerignore, host-neutral. */
|
|
12
2
|
import { SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* can't drift into detecting a marker the generator no longer writes. */
|
|
3
|
+
import { RELEASE_FILE, parseDeploymentRelease } from "./workspace.js";
|
|
4
|
+
/** Fixed first-line prefix of a fastagent-generated Dockerfile. */
|
|
16
5
|
export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
17
|
-
/** The ONE predicate for "did fastagent generate this Dockerfile?"
|
|
18
|
-
* marker, so a hand-written one (or a file where the marker was dropped to take ownership) reads false.
|
|
19
|
-
* Both the "keep vs flag-as-stale" decision and the apt warning share this; a split would let them
|
|
20
|
-
* disagree on the same file (e.g. a marker not at byte 0). */
|
|
6
|
+
/** The ONE predicate for "did fastagent generate this Dockerfile?". */
|
|
21
7
|
export function isGeneratedDockerfile(content) {
|
|
22
8
|
return content.startsWith(GENERATED_DOCKERFILE_MARKER);
|
|
23
9
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
10
|
+
/**
|
|
11
|
+
* First line of a generated `.dockerignore` — and, for the workspace-root one, the single artifact deploy writes
|
|
12
|
+
* outside the agent dir.
|
|
13
|
+
*/
|
|
27
14
|
const GENERATED_DOCKERIGNORE_MARKER = "# Generated by `fastagent deploy`";
|
|
28
|
-
/** Did fastagent generate this `.dockerignore`? (Same ownership rule as {@link isGeneratedDockerfile}.) */
|
|
29
15
|
export function isGeneratedDockerignore(content) {
|
|
30
16
|
return content.startsWith(GENERATED_DOCKERIGNORE_MARKER);
|
|
31
17
|
}
|
|
32
|
-
/** The apt layer (cached right after FROM).
|
|
18
|
+
/** The apt layer (cached right after FROM). */
|
|
33
19
|
function aptLayer(packages) {
|
|
34
20
|
return packages && packages.length > 0
|
|
35
21
|
? `RUN apt-get update \\
|
|
@@ -39,46 +25,30 @@ function aptLayer(packages) {
|
|
|
39
25
|
: "";
|
|
40
26
|
}
|
|
41
27
|
function dockerfile(input) {
|
|
42
|
-
// The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which
|
|
43
|
-
//
|
|
28
|
+
// The whole workspace is baked at /app; deps install (and the local bin lives) under the agent, which is always a
|
|
29
|
+
// SUBDIRECTORY of it (the workspace is the agent dir's parent), so the prefix is never empty.
|
|
44
30
|
const prefix = input.agentPrefix;
|
|
31
|
+
const dir = prefix.replace(/\/$/, "");
|
|
45
32
|
const into = (p) => `${prefix}${p}`;
|
|
46
33
|
// A build step that must run IN the agent dir (`RUN` already gets a shell from docker).
|
|
47
|
-
const at = (cmd) =>
|
|
48
|
-
// The entrypoint.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
const bunCmd = prefix
|
|
54
|
-
? `["sh", "-c", "cd ${prefix.replace(/\/$/, "")} && exec bun run fastagent start /app"]`
|
|
55
|
-
: `["bun", "run", "fastagent", "start", "/app"]`;
|
|
56
|
-
const layoutNote = prefix
|
|
57
|
-
? `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`
|
|
58
|
-
: `The directory IS the agent — it is also its own workspace.`;
|
|
59
|
-
// apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc.,
|
|
60
|
-
// which node:22-slim lacks. Debian default repos only — a package needing a custom repo (gh) or a
|
|
61
|
-
// different base is the operator's own Dockerfile (kept if present). deploy.apt is package-name-validated.
|
|
34
|
+
const at = (cmd) => `cd ${dir} && ${cmd}`;
|
|
35
|
+
// The entrypoint.
|
|
36
|
+
const bunCmd = `["sh", "-c", "cd ${dir} && exec bun run fastagent start /app"]`;
|
|
37
|
+
const layoutNote = `The whole directory is the agent's workspace; the agent itself lives in ${prefix}`;
|
|
38
|
+
// apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc., which
|
|
39
|
+
// node:22-slim lacks.
|
|
62
40
|
const apt = aptLayer(input.apt);
|
|
63
|
-
// PIN the agent into the image.
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
// workspace, single agent or not — the value is a fact of this image, and asserting it means a build
|
|
69
|
-
// context that dropped the agent fails loudly rather than serving a sibling.
|
|
70
|
-
const pin = prefix ? `ENV FASTAGENT_AGENT=${prefix.replace(/\/$/, "")}\n` : "";
|
|
71
|
-
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent`
|
|
72
|
-
// from PATH. That needs npm + a global bin, which live on node:22-slim, NOT on oven/bun — so this path
|
|
73
|
-
// pins node:22-slim regardless of input.runtime (a stray bun lockfile with no package.json would
|
|
74
|
-
// otherwise select a bun base here and crash the `npm i -g` build). input.runtime is meaningful only
|
|
75
|
-
// for a code agent, so the bun-vs-node base below is scoped to that path.
|
|
41
|
+
// PIN the agent into the image.
|
|
42
|
+
const pin = `ENV FASTAGENT_AGENT=${dir}\n`;
|
|
43
|
+
// The caches stay off the volume: they are rebuildable, and a host mount is the slow disk.
|
|
44
|
+
const deployment = `ENV FASTAGENT_RELEASE_FILE=/app/${into(RELEASE_FILE)}\nENV FASTAGENT_STORAGE_DIR=/data\nENV npm_config_cache=/tmp/fastagent/npm\nENV BUN_INSTALL_CACHE_DIR=/tmp/fastagent/bun\n`;
|
|
45
|
+
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent` from PATH.
|
|
76
46
|
if (!input.hasPackageJson) {
|
|
77
47
|
return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
|
|
78
48
|
# npm-based (a markdown/skills agent installs the pinned CLI globally; node:22-slim has npm).
|
|
79
49
|
FROM node:22-slim
|
|
80
50
|
${apt}WORKDIR /app
|
|
81
|
-
${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
51
|
+
${deployment}${pin}RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
82
52
|
COPY . .
|
|
83
53
|
CMD ["fastagent", "start", "/app"]
|
|
84
54
|
`;
|
|
@@ -92,56 +62,37 @@ CMD ["fastagent", "start", "/app"]
|
|
|
92
62
|
${note}
|
|
93
63
|
FROM ${base}
|
|
94
64
|
${apt}WORKDIR /app
|
|
95
|
-
${pin}`;
|
|
96
|
-
// Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on)
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
65
|
+
${deployment}${pin}`;
|
|
66
|
+
// Install ALL deps (no --omit=dev / --production): a repo-as-agent (e.g. an Astro site it operates on) needs its
|
|
67
|
+
// full toolchain.
|
|
68
|
+
//
|
|
69
|
+
// `*.tgz` RIDES ALONG with the manifest, because the install layer runs BEFORE `COPY . .` and a
|
|
70
|
+
// `"dep": "file:./something.tgz"` resolves against the directory it is installing in. Without this the file is
|
|
71
|
+
// not there yet and the build fails on a dependency the author can see sitting in the agent directory. It is
|
|
72
|
+
// also what lets a live probe deploy the CHECKOUT (`npm pack` output) rather than the published release.
|
|
101
73
|
if (isBun) {
|
|
102
|
-
// `--frozen-lockfile` needs bun.lock and hard-fails without it
|
|
103
|
-
// (resolves at build time — not reproducible; the CLI warns to commit the lockfile).
|
|
104
|
-
// `bun run` executes the LOCAL node_modules/.bin entry only — never the registry. A bare
|
|
105
|
-
// `bunx fastagent` would fall back to installing the npm package named `fastagent`, which is an
|
|
106
|
-
// unrelated third-party package (ours is the scoped @fastagent-sh/fastagent).
|
|
74
|
+
// `--frozen-lockfile` needs bun.lock and hard-fails without it.
|
|
107
75
|
const install = input.hasLockfile ? "bun install --frozen-lockfile" : "bun install";
|
|
108
|
-
return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ./${prefix}
|
|
76
|
+
return `${head}COPY ${into("package.json")} ${into("bun.lock*")} ${into("*.tgz")} ./${prefix}
|
|
109
77
|
RUN ${at(install)}
|
|
110
78
|
COPY . .
|
|
111
79
|
CMD ${bunCmd}
|
|
112
80
|
`;
|
|
113
81
|
}
|
|
114
|
-
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent);
|
|
115
|
-
//
|
|
116
|
-
// resolves caret ranges at build time, so THIS branch is NOT reproducible — unlike the pinned
|
|
117
|
-
// `npm ci` (lockfile) and pinned global (markdown) paths. The CLI warns to commit a lockfile.
|
|
118
|
-
// The explicit local bin, not `npx fastagent`: npx falls back to fetching the npm package named
|
|
119
|
-
// `fastagent` when the dep is absent — an unrelated third-party package (ours is scoped). The local
|
|
120
|
-
// path fails fast and visibly instead.
|
|
82
|
+
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent); fall back to `npm
|
|
83
|
+
// install` when there is none so the build never breaks.
|
|
121
84
|
const install = input.hasLockfile ? "npm ci" : "npm install";
|
|
122
|
-
return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ./${prefix}
|
|
85
|
+
return `${head}COPY ${into("package.json")} ${into("package-lock.json*")} ${into("*.tgz")} ./${prefix}
|
|
123
86
|
RUN ${at(install)}
|
|
124
87
|
COPY . .
|
|
125
88
|
CMD ["./${into("node_modules/.bin/fastagent")}", "start", "/app"]
|
|
126
89
|
`;
|
|
127
90
|
}
|
|
128
|
-
/** Patterns are RECURSIVE (`**/`) on purpose
|
|
129
|
-
* .gitignore), and a baked workspace can hold nested projects: a bare `node_modules` would upload
|
|
130
|
-
* their build-machine deps (macOS binaries!) and a bare `.env` would bake their secrets into the
|
|
131
|
-
* image. `.secrets`/`.state` are fastagent machinery — credential contents travel through the host's
|
|
132
|
-
* secret store and state lives on the volume, so neither may enter an image. The two tracked secrets
|
|
133
|
-
* scaffolds (`.env.example` + `.gitignore`) are the narrow exception: they carry no values and must stay
|
|
134
|
-
* beside the shipped `.git`, or the image starts with tracked deletions. `.cache` is generic hygiene
|
|
135
|
-
* (a baked project's own build cache), not a fastagent directory.
|
|
136
|
-
* `.git` is deliberately SHIPPED: the deployed agent's write-back (pull/commit/push) needs the
|
|
137
|
-
* repo's history+remote — the WYSIWYG bake's freshness/durability loop runs through git, driven by
|
|
138
|
-
* the agent itself, not by deploy machinery. Shipping `.git` is only half of that loop: preflight
|
|
139
|
-
* bakes the git BINARY into the generated image iff the workspace ships a `.git` (the `shipsGit`
|
|
140
|
-
* fact); a non-git workspace that still needs git declares config.deploy.apt. */
|
|
91
|
+
/** Patterns are RECURSIVE (`**/`) on purpose. */
|
|
141
92
|
const dockerignore = (input) => DOCKERIGNORE_BASE +
|
|
142
93
|
(input.machineryPaths ?? [])
|
|
143
|
-
// Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state`
|
|
144
|
-
//
|
|
94
|
+
// Skip what the name-based rules above already cover: the default `<agent>/.secrets` / `<agent>/.state` and
|
|
95
|
+
// anything inside them.
|
|
145
96
|
.filter((p) => [`${input.agentPrefix}${SECRETS_DIRNAME}`, `${input.agentPrefix}${STATE_DIRNAME}`].every((covered) => p !== covered && !p.startsWith(`${covered}/`)))
|
|
146
97
|
.map((p) => `# resolved machinery path (FASTAGENT_SECRETS_DIR / FASTAGENT_AUTH_PATH / FASTAGENT_STATE_DIR)\n/${p}\n`)
|
|
147
98
|
.join("");
|
|
@@ -159,20 +110,18 @@ const DOCKERIGNORE_BASE = `${GENERATED_DOCKERIGNORE_MARKER}. Delete this line to
|
|
|
159
110
|
# (the generated image installs the git binary when this directory ships a .git; otherwise
|
|
160
111
|
# add deploy.apt ["git"]). For a smaller image with no git needs, add a ".git" line here.
|
|
161
112
|
`;
|
|
162
|
-
/**
|
|
163
|
-
* The Dockerfile + ignore artifacts — spread into any host's artifact list. Everything is prefixed with
|
|
164
|
-
* {@link ContainerInput.agentPrefix}, so a NESTED agent's Dockerfile lands under `fastagent/` (never
|
|
165
|
-
* colliding with one the workspace already owns) while an agent that IS the workspace lands it at the root, where it IS
|
|
166
|
-
* the agent's own file. The ignore ships in TWO forms because context packing is host-CLI-owned and
|
|
167
|
-
* inconsistent: (1) a ROOT `.dockerignore` — the only form flyctl/railway's own context packers
|
|
168
|
-
* reliably read (kept if the workspace already has one — preflight then checks the machinery/secret
|
|
169
|
-
* excludes it must carry) — and (2) a per-Dockerfile `Dockerfile.dockerignore` for plain docker/buildx
|
|
170
|
-
* builds. When the agent sits inside the workspace that root file is the ONE write deploy makes outside the agent dir, and
|
|
171
|
-
* only at deploy time — without it the host CLI's packer would bake `.secrets/` into the image.
|
|
172
|
-
*/
|
|
113
|
+
/** The Dockerfile + ignore artifacts — spread into any host's artifact list. */
|
|
173
114
|
export function containerArtifacts(input) {
|
|
174
115
|
const ignore = dockerignore(input);
|
|
116
|
+
const release = {
|
|
117
|
+
version: 1,
|
|
118
|
+
id: input.releaseId,
|
|
119
|
+
agent: input.agentPrefix.replace(/\/$/, ""),
|
|
120
|
+
...(input.modelSpec !== undefined ? { model: input.modelSpec } : {}),
|
|
121
|
+
};
|
|
122
|
+
parseDeploymentRelease(JSON.stringify(release));
|
|
175
123
|
return [
|
|
124
|
+
{ path: `${input.agentPrefix}${RELEASE_FILE}`, content: `${JSON.stringify(release, null, 2)}\n` },
|
|
176
125
|
{ path: `${input.agentPrefix}Dockerfile`, content: dockerfile(input) },
|
|
177
126
|
{ path: ".dockerignore", content: ignore },
|
|
178
127
|
{ path: `${input.agentPrefix}Dockerfile.dockerignore`, content: ignore },
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fastagent deploy docker` — the local-Docker plan. The application image stays ingress-neutral:
|
|
3
|
-
* this target generates only the shared Dockerfile plus a small Compose service for the FastAgent
|
|
4
|
-
* process, its loopback port, exact environment-variable names, and one persistent state volume.
|
|
5
|
-
* `--tunnel` can add an ephemeral Cloudflare Quick Tunnel service; durable ingress remains operator-owned.
|
|
6
|
-
*/
|
|
1
|
+
/** `fastagent deploy docker` — the local-Docker plan. */
|
|
7
2
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
8
3
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
4
|
+
import { type DeploymentSecret } from "../secrets.ts";
|
|
9
5
|
export interface DockerPlanInput extends ContainerInput {
|
|
10
6
|
/** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
|
|
11
7
|
projectName: string;
|
|
12
|
-
/** Container + default host port.
|
|
8
|
+
/** Container + default host port. */
|
|
13
9
|
port: number;
|
|
14
10
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
15
11
|
modelAuth: string | undefined;
|
|
16
|
-
/** Every declared channel and its ingress — the source of the
|
|
12
|
+
/** Every declared channel and its ingress — the source of the ingress note. */
|
|
17
13
|
channels: readonly DeclaredChannel[];
|
|
18
|
-
/** Generate an optional Cloudflare Quick Tunnel service in Compose.
|
|
14
|
+
/** Generate an optional Cloudflare Quick Tunnel service in Compose. */
|
|
19
15
|
tunnel: boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
|
|
18
|
+
* carries.
|
|
19
|
+
*/
|
|
20
|
+
secrets?: readonly DeploymentSecret[];
|
|
21
|
+
/** The value file as the pre-flight resolved it (it follows `FASTAGENT_SECRETS_DIR`), workspace-relative. */
|
|
22
|
+
valueFile: string;
|
|
22
23
|
}
|
|
23
24
|
export interface DockerPlan {
|
|
24
25
|
/** fastagent.compose.yml + the shared Dockerfile/ignore artifacts. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { webhookPaths } from "../channel-ingress.js";
|
|
2
2
|
import { containerArtifacts } from "../container.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isEnvKey } from "../secrets.js";
|
|
4
|
+
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
4
5
|
const MOUNT = "/data";
|
|
5
6
|
const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
6
7
|
const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
|
|
@@ -30,14 +31,9 @@ function composeInterpolation(name) {
|
|
|
30
31
|
return `\${${name}:-}`;
|
|
31
32
|
}
|
|
32
33
|
function composeYaml(input) {
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
|
|
37
|
-
const secretEnv = envNames.map((name) => ` ${name}: "${composeInterpolation(name)}"`).join("\n");
|
|
38
|
-
// Compose sits beside the Dockerfile, under the agent prefix; the build context is always the
|
|
39
|
-
// WORKSPACE, so it climbs back out of the prefix (`..` per level, `.` when there is none).
|
|
40
|
-
const context = input.agentPrefix ? ".." : ".";
|
|
34
|
+
// Compose sits beside the Dockerfile, under the agent prefix; the build context is always the WORKSPACE, so it
|
|
35
|
+
// climbs back out of the one-level prefix deploy requires.
|
|
36
|
+
const context = "..";
|
|
41
37
|
const dockerfile = `${input.agentPrefix}Dockerfile`;
|
|
42
38
|
const tunnelService = input.tunnel
|
|
43
39
|
? `
|
|
@@ -57,6 +53,12 @@ function composeYaml(input) {
|
|
|
57
53
|
# bypass it; preserve any operator NO_PROXY entries after the required service/loopback names.
|
|
58
54
|
NO_PROXY: "agent,localhost,127.0.0.1,${composeInterpolation("NO_PROXY")}"
|
|
59
55
|
no_proxy: "agent,localhost,127.0.0.1,${composeInterpolation("no_proxy")}"
|
|
56
|
+
# Quieter than the agent, but bounded for the same reason: nothing else on this host bounds it.
|
|
57
|
+
logging:
|
|
58
|
+
driver: json-file
|
|
59
|
+
options:
|
|
60
|
+
max-size: "10m"
|
|
61
|
+
max-file: "3"
|
|
60
62
|
restart: "no"
|
|
61
63
|
`
|
|
62
64
|
: "";
|
|
@@ -71,14 +73,36 @@ services:
|
|
|
71
73
|
dockerfile: ${dockerfile}
|
|
72
74
|
ports:
|
|
73
75
|
- "127.0.0.1:${input.port}:${input.port}"
|
|
76
|
+
# The deployed environment's own declaration, read by the container itself. \`env_file\`, not per-name
|
|
77
|
+
# \${NAME:-} interpolation: interpolation resolves from the SHELL or the project .env, which is exactly the
|
|
78
|
+
# source a deployment must not have. A FIXED path, never the builder's FASTAGENT_SECRETS_DIR — this file is a
|
|
79
|
+
# committed artifact and must mean the same thing on every machine. \`deploy\` creates it if it is missing.
|
|
80
|
+
env_file:
|
|
81
|
+
- ${SECRETS_DIRNAME}/.env
|
|
74
82
|
environment:
|
|
75
83
|
PORT: "${input.port}"
|
|
76
|
-
# Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets.
|
|
84
|
+
# Machinery on the ONE state volume: mutable state and (seeded, possibly rotated) secrets. Pinned AFTER
|
|
85
|
+
# env_file so a local path in that file (the scaffold lists these) cannot send the container's state, sessions
|
|
86
|
+
# or credentials somewhere outside the volume — or at a host path that does not exist here at all.
|
|
77
87
|
FASTAGENT_STATE_DIR: "${MOUNT}/.state"
|
|
78
88
|
FASTAGENT_SECRETS_DIR: "${MOUNT}/.secrets"
|
|
79
|
-
${
|
|
89
|
+
FASTAGENT_AUTH_PATH: "${MOUNT}/.secrets/auth.json"
|
|
90
|
+
# The ONE value that is not in the value file: \`--run\` mints it from the local auth.json. Left as a seam in
|
|
91
|
+
# the committed topology so a hand-run \`up\` can supply it the same way — from the ENVIRONMENT of that \`up\`.
|
|
92
|
+
# Writing this one key into the value file does nothing: \`environment:\` is applied after \`env_file\`, so the
|
|
93
|
+
# line below would blank it and the container would start without seeding auth.json.
|
|
94
|
+
FASTAGENT_AUTH_SEED: "${composeInterpolation("FASTAGENT_AUTH_SEED")}"
|
|
80
95
|
volumes:
|
|
81
96
|
- state:${MOUNT}
|
|
97
|
+
# The agent's log is where a failure's reason is recorded (a scheduled fire logs the detail; what the turn
|
|
98
|
+
# SAID is in its session, not here), and Docker's default json-file driver never rotates — so on this host
|
|
99
|
+
# the bound has to be written down. Fly and Railway bound their own log retention; AgentCore does NOT
|
|
100
|
+
# (CloudWatch keeps data indefinitely until an operator sets a retention policy — its runbook says so).
|
|
101
|
+
logging:
|
|
102
|
+
driver: json-file
|
|
103
|
+
options:
|
|
104
|
+
max-size: "10m"
|
|
105
|
+
max-file: "3"
|
|
82
106
|
restart: unless-stopped
|
|
83
107
|
${tunnelService}
|
|
84
108
|
volumes:
|
|
@@ -89,33 +113,28 @@ volumes:
|
|
|
89
113
|
export function planDockerDeploy(input) {
|
|
90
114
|
const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
|
|
91
115
|
const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
|
|
116
|
+
// One spelling for every command: the generated file names the value file itself (`env_file`), so no command
|
|
117
|
+
// needs a flag to reach the deployed environment's declaration.
|
|
92
118
|
const compose = `docker compose -f ${composePath}`;
|
|
93
|
-
const secrets =
|
|
94
|
-
const required = secrets.filter((secret) => secret.required);
|
|
95
|
-
const optional = secrets.filter((secret) => !secret.required);
|
|
119
|
+
const secrets = input.secrets ?? [];
|
|
96
120
|
const paths = webhookPaths(input.channels);
|
|
97
121
|
const runbook = [
|
|
98
122
|
`# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
|
|
99
123
|
`# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
|
|
100
124
|
`# Prereqs: Docker Engine/Desktop with Compose >= ${MIN_DOCKER_COMPOSE_VERSION} (\`docker compose version\`).`,
|
|
101
125
|
];
|
|
102
|
-
if (
|
|
103
|
-
runbook.push(`#
|
|
104
|
-
}
|
|
105
|
-
if (optional.length > 0) {
|
|
106
|
-
runbook.push(`# Optional environment values:`, ...optional.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
126
|
+
if (secrets.length > 0) {
|
|
127
|
+
runbook.push(`# Environment: the container reads ${input.valueFile} whole (Compose \`env_file\`) — that file declares the`, `# deployed environment (in CI, write it before running the command):`, ...secrets.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
107
128
|
}
|
|
108
129
|
if (!isEnvKey(input.modelAuth)) {
|
|
109
130
|
runbook.push(input.modelAuth === undefined
|
|
110
131
|
? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
|
|
111
132
|
: `# Model auth: local source is "${input.modelAuth}" — \`--run\` carries auth.json as FASTAGENT_AUTH_SEED.`, `# For a manual Compose run, provide a provider API key or set FASTAGENT_AUTH_SEED yourself.`);
|
|
112
133
|
}
|
|
113
|
-
runbook.push(input.agentPrefix
|
|
114
|
-
|
|
115
|
-
: `# Run from this directory — it is both the agent and its workspace.`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
|
|
116
|
-
runbook.push(``, `# Build/create/reconcile the service. Re-running this is the complete local redeploy; state stays`, `# in the Compose volume mounted at ${MOUNT}.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: also deletes auth, sessions, and channel state`);
|
|
134
|
+
runbook.push(`# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
|
|
135
|
+
runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT} retains the workspace, state and credentials.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: deletes workspace, credentials and all agent state`);
|
|
117
136
|
if (input.tunnel) {
|
|
118
|
-
runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register;
|
|
137
|
+
runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; manual Slack prints its console URL. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
|
|
119
138
|
}
|
|
120
139
|
else if (paths.length > 0) {
|
|
121
140
|
runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then configure the`, `# detected default endpoint(s): ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead.`);
|
|
@@ -5,15 +5,19 @@ export interface DockerRunPlan {
|
|
|
5
5
|
composeFile: string;
|
|
6
6
|
/** Container port from config; used to ask Compose for the effective published host port. */
|
|
7
7
|
port: number;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* What `--run` assembled. The container reads the value file itself through the generated `env_file`, so the only
|
|
10
|
+
* entry that has to travel through this process is `FASTAGENT_AUTH_SEED`, which is minted here; the rest is
|
|
11
|
+
* reported to the operator and otherwise unused.
|
|
12
|
+
*/
|
|
9
13
|
secrets: Record<string, string>;
|
|
10
|
-
/**
|
|
14
|
+
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
11
15
|
missingSecrets: string[];
|
|
16
|
+
/** That value file, workspace-relative, so the gate names the file this deploy actually read. */
|
|
17
|
+
valueFile: string;
|
|
12
18
|
/** Neither an env-key credential nor a readable auth.json is available. */
|
|
13
19
|
needsModelCredential: boolean;
|
|
14
|
-
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar
|
|
15
|
-
* answered. REQUIRED, not optional: an absent one would delete this whole step in silence, which
|
|
16
|
-
* is the failure this driver's gate exists to end. A topology with no tunnel never calls it. */
|
|
20
|
+
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
|
|
17
21
|
announce: DockerAnnounce;
|
|
18
22
|
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
19
23
|
requireTunnel: boolean;
|
|
@@ -23,24 +27,27 @@ export type DockerRunOutcome = {
|
|
|
23
27
|
url?: string;
|
|
24
28
|
tunnelUrl?: string;
|
|
25
29
|
}
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
30
|
+
/**
|
|
31
|
+
* `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know where it is and what
|
|
32
|
+
* to re-run.
|
|
33
|
+
*/
|
|
28
34
|
| {
|
|
29
35
|
ok: false;
|
|
30
36
|
gate: string;
|
|
31
37
|
url?: string;
|
|
32
38
|
tunnelUrl?: string;
|
|
33
39
|
};
|
|
34
|
-
/** Register the deployment's webhooks against its public URL, reporting what each registrar
|
|
35
|
-
* answered. Injected so the driver stays free of channel specifics — and so a test can fail one. */
|
|
40
|
+
/** Register the deployment's webhooks against its public URL, reporting what each registrar answered. */
|
|
36
41
|
type DockerAnnounce = (baseUrl: string) => Promise<{
|
|
37
42
|
kind: string;
|
|
38
43
|
outcome: RegistrationOutcome;
|
|
39
44
|
}[]>;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
|
|
45
|
+
/**
|
|
46
|
+
* `stillStarting` answers whether the agent container is still up; a probe that ignores it simply waits out its whole
|
|
47
|
+
* budget.
|
|
48
|
+
*/
|
|
49
|
+
export type DockerHealthProbe = (healthUrl: string, stillStarting: () => Promise<boolean>) => Promise<boolean>;
|
|
50
|
+
/** A published Quick Tunnel URL, and whether its tunnel ever reported an edge connection. */
|
|
44
51
|
export interface ComposeTunnel {
|
|
45
52
|
url: string;
|
|
46
53
|
connected: boolean;
|
|
@@ -49,23 +56,13 @@ export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string, env:
|
|
|
49
56
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
50
57
|
export declare function localUrlFromComposePort(stdout: string): string | undefined;
|
|
51
58
|
/**
|
|
52
|
-
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel
|
|
53
|
-
*
|
|
54
|
-
* has and for the same reason — the hostname does not exist until then (#435), and this driver hands
|
|
55
|
-
* the URL straight to `announce`. Returns a URL that never connected rather than nothing: the
|
|
56
|
-
* registrars downstream report their own outcome, and a gate saying "no URL" would misname it — but it
|
|
57
|
-
* says WHICH it is returning, because the caller owes the operator that sentence.
|
|
59
|
+
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
60
|
+
* connection.
|
|
58
61
|
*/
|
|
59
62
|
export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv, options?: {
|
|
60
63
|
attempts?: number;
|
|
61
64
|
intervalMs?: number;
|
|
62
65
|
sleep?: (ms: number) => Promise<void>;
|
|
63
66
|
}): Promise<ComposeTunnel | undefined>;
|
|
64
|
-
/**
|
|
65
|
-
* Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
|
|
66
|
-
* only assumes the generated service contract (`agent`, config's container port) for optional URL/
|
|
67
|
-
* readiness reporting. If the service intentionally has no host-published port, a successful running
|
|
68
|
-
* service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
|
|
69
|
-
*/
|
|
70
67
|
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
|
|
71
68
|
export {};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally.
|
|
3
|
-
* Compose owns container/network/volume lifecycle; this driver owns only actionable gates, secret/auth
|
|
4
|
-
* carry through the child environment (never argv), and a readiness check on the published loopback port.
|
|
5
|
-
*/
|
|
1
|
+
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
6
2
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
7
3
|
import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
|
|
8
4
|
import { registrationGate } from "../registration-gate.js";
|
|
9
5
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
6
|
+
import { missingValuesGate } from "../secrets.js";
|
|
10
7
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
11
8
|
export function localUrlFromComposePort(stdout) {
|
|
12
9
|
const line = stdout
|
|
@@ -16,15 +13,15 @@ export function localUrlFromComposePort(stdout) {
|
|
|
16
13
|
const port = line?.match(/:(\d+)$/)?.[1];
|
|
17
14
|
return port ? `http://127.0.0.1:${port}` : undefined;
|
|
18
15
|
}
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The FIRST boot seeds the whole workspace onto the volume (the image's `node_modules` included) before it binds a
|
|
18
|
+
* port, so this budget covers a copy on a slow Docker Desktop disk, not a listen.
|
|
19
|
+
*/
|
|
20
|
+
const defaultHealthProbe = (healthUrl, stillStarting) => waitForHealth(healthUrl, 180_000, 500, stillStarting);
|
|
20
21
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
22
|
/**
|
|
22
|
-
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel
|
|
23
|
-
*
|
|
24
|
-
* has and for the same reason — the hostname does not exist until then (#435), and this driver hands
|
|
25
|
-
* the URL straight to `announce`. Returns a URL that never connected rather than nothing: the
|
|
26
|
-
* registrars downstream report their own outcome, and a gate saying "no URL" would misname it — but it
|
|
27
|
-
* says WHICH it is returning, because the caller owes the operator that sentence.
|
|
23
|
+
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
24
|
+
* connection.
|
|
28
25
|
*/
|
|
29
26
|
export async function waitForComposeTunnelUrl(docker, composeFile, env, options = {}) {
|
|
30
27
|
const compose = ["compose", "-f", composeFile];
|
|
@@ -45,16 +42,13 @@ export async function waitForComposeTunnelUrl(docker, composeFile, env, options
|
|
|
45
42
|
return assigned ? { url: assigned, connected: false } : undefined;
|
|
46
43
|
}
|
|
47
44
|
const defaultTunnelUrlProbe = (docker, composeFile, env) => waitForComposeTunnelUrl(docker, composeFile, env);
|
|
48
|
-
/**
|
|
49
|
-
* Drive Docker Compose. A custom Compose file remains authoritative: the driver invokes it as-is and
|
|
50
|
-
* only assumes the generated service contract (`agent`, config's container port) for optional URL/
|
|
51
|
-
* readiness reporting. If the service intentionally has no host-published port, a successful running
|
|
52
|
-
* service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
|
|
53
|
-
*/
|
|
54
45
|
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, tunnelUrlProbe = defaultTunnelUrlProbe) {
|
|
55
46
|
const gate = (message) => ({ ok: false, gate: message });
|
|
56
47
|
const compose = ["compose", "-f", plan.composeFile];
|
|
57
|
-
|
|
48
|
+
// The ONE value that is not in the value file, so the ONE that has to cross this process. Set even when absent, so
|
|
49
|
+
// a same-named variable in the builder's shell cannot interpolate into the container in its place — `spawnRunner`
|
|
50
|
+
// merges over `process.env`.
|
|
51
|
+
const env = { FASTAGENT_AUTH_SEED: plan.secrets.FASTAGENT_AUTH_SEED ?? "" };
|
|
58
52
|
// CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
|
|
59
53
|
const version = await docker(["compose", "version"], { capture: true });
|
|
60
54
|
if (version.code === 127) {
|
|
@@ -67,14 +61,20 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
67
61
|
if (plan.needsModelCredential) {
|
|
68
62
|
return gate("no model credential — run `fastagent login`, or set a provider API key in .env, then re-run");
|
|
69
63
|
}
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
65
|
+
if (missingValues)
|
|
66
|
+
return gate(missingValues);
|
|
67
|
+
// Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
|
|
68
|
+
// hands Compose nothing but the seed. Saying "passing N secrets to Compose" would be false, and doubly so on a
|
|
69
|
+
// hand-owned Compose file that has no `env_file` entry at all.
|
|
70
|
+
const secretNames = Object.keys(plan.secrets).filter((name) => name !== "FASTAGENT_AUTH_SEED");
|
|
71
|
+
if (secretNames.length > 0) {
|
|
72
|
+
log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
|
|
72
73
|
}
|
|
73
74
|
if ((await docker(["info"], { capture: true })).code !== 0) {
|
|
74
75
|
return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
|
|
75
76
|
}
|
|
76
|
-
// The file on disk is authoritative.
|
|
77
|
-
// both to protect the `agent` run contract and to catch `--tunnel` against a kept non-tunnel topology.
|
|
77
|
+
// The file on disk is authoritative.
|
|
78
78
|
const configured = await docker([...compose, "config", "--services"], { capture: true, env });
|
|
79
79
|
if (configured.code !== 0) {
|
|
80
80
|
return gate(`could not load ${plan.composeFile} — generated files require Docker Compose >= ` +
|
|
@@ -89,9 +89,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
89
89
|
return gate(`--tunnel was requested but the kept ${plan.composeFile} has no "tunnel" service — ` +
|
|
90
90
|
`edit it, delete it and regenerate, or pass --force`);
|
|
91
91
|
}
|
|
92
|
-
// Quick Tunnel logs are the control-plane output (the assigned URL).
|
|
93
|
-
// a rerun cannot read a stale URL from accumulated logs; `up` below creates one fresh tunnel, then the
|
|
94
|
-
// CLI registers that URL. The app container/volume are untouched.
|
|
92
|
+
// Quick Tunnel logs are the control-plane output (the assigned URL).
|
|
95
93
|
if (hasTunnel) {
|
|
96
94
|
log("recreating the ephemeral tunnel service…");
|
|
97
95
|
if ((await docker([...compose, "rm", "-s", "-f", "tunnel"], { env })).code !== 0) {
|
|
@@ -102,8 +100,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
102
100
|
if ((await docker([...compose, "up", "-d", "--build"], { env })).code !== 0) {
|
|
103
101
|
return gate(`\`docker compose up\` failed — see the Docker output above; fix ${plan.composeFile} and re-run`);
|
|
104
102
|
}
|
|
105
|
-
// Detached `up` can return 0 just before a bad command exits.
|
|
106
|
-
// running so a broken custom Dockerfile/CMD cannot look deployed. Compose restart loops are excluded.
|
|
103
|
+
// Detached `up` can return 0 just before a bad command exits.
|
|
107
104
|
const running = await docker([...compose, "ps", "--status", "running", "--services"], {
|
|
108
105
|
capture: true,
|
|
109
106
|
env,
|
|
@@ -116,7 +113,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
116
113
|
return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
117
114
|
}
|
|
118
115
|
// A user-owned topology may deliberately remove the host port and expose only through its own ingress.
|
|
119
|
-
// In that case Compose `port` is absent/non-zero: service-running is the available readiness floor.
|
|
120
116
|
const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true, env });
|
|
121
117
|
const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
|
|
122
118
|
if (!url) {
|
|
@@ -124,7 +120,17 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
124
120
|
}
|
|
125
121
|
else {
|
|
126
122
|
const healthUrl = `${url}/health`;
|
|
127
|
-
|
|
123
|
+
// Throttled, and an unreadable answer reads as "still starting": the health poll runs twice a second, and a
|
|
124
|
+
// `compose ps` per poll would cost more than the wait it shortens.
|
|
125
|
+
let lastCheck = Date.now();
|
|
126
|
+
const stillStarting = async () => {
|
|
127
|
+
if (Date.now() - lastCheck < 5_000)
|
|
128
|
+
return true;
|
|
129
|
+
lastCheck = Date.now();
|
|
130
|
+
const ps = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true, env });
|
|
131
|
+
return ps.code !== 0 || ps.stdout.split(/\s+/).includes("agent");
|
|
132
|
+
};
|
|
133
|
+
if (!(await healthProbe(healthUrl, stillStarting))) {
|
|
128
134
|
return gate(`agent did not become healthy at ${healthUrl} — inspect \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
129
135
|
}
|
|
130
136
|
}
|
|
@@ -135,19 +141,15 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
135
141
|
if (!tunnel) {
|
|
136
142
|
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
137
143
|
}
|
|
138
|
-
// The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here
|
|
139
|
-
// cloudflared runs in a container, so the author cannot see the logs that would explain the
|
|
140
|
-
// registration failures about to follow. Silence sends them to debug the platform for what is a
|
|
141
|
-
// local tunnel that never came up.
|
|
144
|
+
// The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here.
|
|
142
145
|
if (!tunnel.connected) {
|
|
143
146
|
log(`warn: the tunnel service never reported an edge connection for ${tunnel.url} — announcing it anyway. ` +
|
|
144
147
|
`Nothing reaches a tunnel that has not connected, so a webhook registration that cannot resolve the ` +
|
|
145
148
|
`host is this, not the platform. Inspect \`docker compose -f ${plan.composeFile} logs tunnel\`.`);
|
|
146
149
|
}
|
|
147
150
|
const tunnelUrl = tunnel.url;
|
|
148
|
-
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer
|
|
149
|
-
//
|
|
150
|
-
// the one target whose `--run` could exit 0 with a webhook that never registered.
|
|
151
|
+
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer that owns the
|
|
152
|
+
// outcome, so it is the layer that can gate on one.
|
|
151
153
|
const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
|
|
152
154
|
for (const { kind, outcome } of await plan.announce(tunnelUrl))
|
|
153
155
|
reg.track(kind, outcome);
|