@fastagent-sh/fastagent 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 -40
- 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 +233 -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 +166 -229
- 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
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. Pure: facts
|
|
3
|
-
* in, artifact contents + an ordered runbook out; the CLI (deploy side effects live there) writes the
|
|
4
|
-
* files and prints the runbook.
|
|
5
|
-
*
|
|
6
|
-
* fastagent owns the two ends only it can know — generate definition-aware artifacts (state root →
|
|
7
|
-
* volume, autostop tuned to the turn model, the exact secret list) and the post-deploy webhook step —
|
|
8
|
-
* and GUIDES the middle (flyctl app/volume/secrets/deploy) as a precise, values-resolved runbook. By
|
|
9
|
-
* default a coding agent (or you) runs flyctl from that runbook; `deploy fly --run` drives it from the
|
|
10
|
-
* CLI instead. This module stays pure either way — it produces the plan, never runs flyctl. The runbook
|
|
11
|
-
* is a FIRST-deploy sequence: `apps`/`volumes create` are one-time (marked so — re-running would make a
|
|
12
|
-
* second volume, the state split it warns against); a redeploy is `fly deploy` alone.
|
|
13
|
-
*
|
|
14
|
-
* autostop = "suspend": the machine snapshots and suspends when idle (Fly Proxy sees inbound load 0),
|
|
15
|
-
* resumes on the next webhook in ~hundreds of ms. A long turn interrupted by an idle-suspend whose
|
|
16
|
-
* snapshot is discarded replays on the next start (the Telegram L1 turn store) — at-least-once, the
|
|
17
|
-
* documented floor. State on the /data volume survives stop/suspend on the same machine.
|
|
18
|
-
*/
|
|
1
|
+
/** `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. */
|
|
19
2
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
20
3
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
4
|
+
import { type DeploymentSecret } from "../secrets.ts";
|
|
21
5
|
export interface FlyPlanInput extends ContainerInput {
|
|
22
6
|
/** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
|
|
23
7
|
appName: string;
|
|
24
8
|
/** The port the app listens on (config.http.port ?? 8787); fly.toml routes to it. */
|
|
25
9
|
port: number;
|
|
10
|
+
/** What satisfies model auth locally ({@link probeAuthSource}). */
|
|
11
|
+
modelAuth: string | undefined;
|
|
26
12
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
13
|
+
* Every declared channel and its ingress — the source of the webhook steps, and whether a machine
|
|
14
|
+
* must stay up for an outbound connection.
|
|
29
15
|
*/
|
|
30
|
-
modelAuth: string | undefined;
|
|
31
|
-
/** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
|
|
32
|
-
* whether a machine must stay up for an outbound connection. */
|
|
33
16
|
channels: readonly DeclaredChannel[];
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Time triggers present (schedules/ or selfSchedule) — forces one machine up: cron/wake has no
|
|
42
|
-
* external wake-up, so a scaled-to-zero box would sleep through them. */
|
|
43
|
-
hasTimeTriggers: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The runbook's variable list (`deploymentSecrets`): what must have a value, then everything else the value file
|
|
19
|
+
* carries.
|
|
20
|
+
*/
|
|
21
|
+
secrets?: readonly DeploymentSecret[];
|
|
22
|
+
/** `routines/` declares a cron — one of the things that forces a machine up (deploy/residency.ts). */
|
|
23
|
+
hasCron: boolean;
|
|
44
24
|
}
|
|
45
25
|
export interface FlyPlan {
|
|
46
26
|
/** fly.toml / Dockerfile / .dockerignore — written by the CLI (skipped if present unless --force). */
|
|
@@ -48,21 +28,12 @@ export interface FlyPlan {
|
|
|
48
28
|
/** The ordered, values-resolved deploy runbook — printed to stdout for the coding agent to execute. */
|
|
49
29
|
runbook: string[];
|
|
50
30
|
}
|
|
51
|
-
/** Did fastagent generate this `fly.toml`? */
|
|
52
31
|
export declare function isGeneratedFlyToml(content: string): boolean;
|
|
53
32
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
54
33
|
export declare function planFlyDeploy(input: FlyPlanInput): FlyPlan;
|
|
55
|
-
/**
|
|
56
|
-
* The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
|
|
57
|
-
* single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
|
|
58
|
-
* double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
|
|
59
|
-
*/
|
|
34
|
+
/** The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent. */
|
|
60
35
|
export declare function parseFlyAppName(toml: string): string | undefined;
|
|
61
|
-
/** The `
|
|
62
|
-
* volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
|
|
63
|
-
export declare function parseFlyRegion(toml: string): string | undefined;
|
|
64
|
-
/** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
|
|
65
|
-
* file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
|
|
36
|
+
/** The `min_machines_running` from a fly.toml, or undefined. */
|
|
66
37
|
export declare function parseFlyMinMachines(toml: string): number | undefined;
|
|
67
38
|
/** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
|
|
68
39
|
export declare function toFlyAppName(basename: string): string;
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
import { webhookRunbook } from "../channel-ingress.js";
|
|
2
2
|
import { containerArtifacts } from "../container.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
: ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
|
|
3
|
+
import { CRON_CAN_BE_EXTERNAL, WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
|
|
4
|
+
import { isEnvKey } from "../secrets.js";
|
|
5
|
+
function flyToml(appName, port, residency) {
|
|
6
|
+
// WHY is residency.ts's to decide; the SETTING and its wording are Fly's. Anything else is an edit to the
|
|
7
|
+
// generated file, which is the artifact's job.
|
|
8
|
+
const min = residency
|
|
9
|
+
? ` min_machines_running = 1 # ${residency.why}`
|
|
10
|
+
: ` min_machines_running = 0 # scale to zero — ${WAKEUPS_WHEN_ASLEEP}`;
|
|
11
|
+
// The one reason with a way out gets it stated NEXT TO the line it is about, which is where an operator reading
|
|
12
|
+
// `min_machines_running = 1` and wondering what it costs them is looking.
|
|
13
|
+
const alternative = residency?.reason === CRON_CAN_BE_EXTERNAL
|
|
14
|
+
? ` # …or keep the time somewhere else and set this to 0: a scheduler you own (Fly Cron Manager,\n` +
|
|
15
|
+
` # supercronic, GitHub Actions) calls \`POST /run\`, which runs one declared unit of work by name.\n` +
|
|
16
|
+
` # It is an API, not a clock: read its contract first — docs/api-reference.md#post-run.\n` +
|
|
17
|
+
` # At 0, ${WAKEUPS_WHEN_ASLEEP}.\n`
|
|
18
|
+
: "";
|
|
19
|
+
// Suspend, not stop: a resume is fast enough that a webhook does not time out. Edit the line to change it.
|
|
20
|
+
const stopLine = ` auto_stop_machines = "suspend" # suspend on idle (fast resume on the next webhook)`;
|
|
22
21
|
return `${GENERATED_FLY_TOML_MARKER}. Edit freely — it is not regenerated unless you pass --force.
|
|
23
22
|
app = "${appName}"
|
|
24
23
|
primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
@@ -36,44 +35,36 @@ primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
|
36
35
|
${stopLine}
|
|
37
36
|
auto_start_machines = true
|
|
38
37
|
${min}
|
|
39
|
-
|
|
38
|
+
${alternative}
|
|
40
39
|
[mounts]
|
|
41
40
|
source = "data"
|
|
42
|
-
destination = "/data" #
|
|
41
|
+
destination = "/data" # workspace, state and credentials survive stop/suspend/redeploy
|
|
42
|
+
initial_size = "1gb" # the volume is created by the FIRST deploy — it is pinned to one host, and only
|
|
43
|
+
# deploy tells the scheduler which machine (size + image) must fit there too
|
|
43
44
|
|
|
44
45
|
[[vm]]
|
|
45
46
|
size = "shared-cpu-1x"
|
|
46
47
|
memory = "512mb" # suspend is not recommended above 2 GB
|
|
47
48
|
`;
|
|
48
49
|
}
|
|
49
|
-
/** First line of a generated `fly.toml`, and the predicate that reads it back.
|
|
50
|
-
* whether `--force` may reset the file (app/region/vm state a user tuned by hand is theirs). */
|
|
50
|
+
/** First line of a generated `fly.toml`, and the predicate that reads it back. */
|
|
51
51
|
const GENERATED_FLY_TOML_MARKER = "# Generated by `fastagent deploy fly`";
|
|
52
|
-
/** Did fastagent generate this `fly.toml`? */
|
|
53
52
|
export function isGeneratedFlyToml(content) {
|
|
54
53
|
return content.startsWith(GENERATED_FLY_TOML_MARKER);
|
|
55
54
|
}
|
|
56
55
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
57
56
|
export function planFlyDeploy(input) {
|
|
58
57
|
const { appName, port, modelAuth, channels } = input;
|
|
59
|
-
// Artifacts sit under the agent prefix, so
|
|
60
|
-
// files; the runbook passes explicit -c/--dockerfile flags either way (unambiguous across flyctl
|
|
61
|
-
// versions — no reliance on config-relative path resolution).
|
|
58
|
+
// Artifacts sit under the agent prefix, so they never touch the workspace's own deploy files.
|
|
62
59
|
const flyTomlPath = `${input.agentPrefix}fly.toml`;
|
|
63
60
|
const artifacts = [
|
|
64
61
|
{
|
|
65
62
|
path: flyTomlPath,
|
|
66
|
-
content: flyToml(appName, port,
|
|
63
|
+
content: flyToml(appName, port, residencyFor({ channels, hasCron: input.hasCron })),
|
|
67
64
|
},
|
|
68
65
|
...containerArtifacts(input),
|
|
69
66
|
];
|
|
70
|
-
|
|
71
|
-
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
72
|
-
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
73
|
-
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
74
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
75
|
-
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
76
|
-
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
67
|
+
const secrets = input.secrets ?? [];
|
|
77
68
|
const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
|
|
78
69
|
const runbook = [
|
|
79
70
|
`# Deploy "${appName}" to Fly.io. ${flyTomlPath} / Dockerfile(.dockerignore) are generated above.`,
|
|
@@ -83,10 +74,9 @@ export function planFlyDeploy(input) {
|
|
|
83
74
|
`# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
|
|
84
75
|
`# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
|
|
85
76
|
`fly apps create ${appName}`,
|
|
86
|
-
`# volume
|
|
87
|
-
`#
|
|
88
|
-
`#
|
|
89
|
-
`fly volumes create data --app ${appName} --region <region> --size 1`,
|
|
77
|
+
`# The volume (workspace, state, credentials — kept across stop/suspend/redeploy) is created by the deploy`,
|
|
78
|
+
`# below, in primary_region and sized by [mounts].initial_size. Creating it yourself first pins it to a host`,
|
|
79
|
+
`# picked without the machine, which can fail the deploy with "insufficient resources … existing volume".`,
|
|
90
80
|
``,
|
|
91
81
|
`# An address to be reached ON: [http_service] declares a service, it does not allocate an IP.`,
|
|
92
82
|
`# \`fly deploy\` allocates one on a FIRST deploy only, and merely WARNS when that fails — which`,
|
|
@@ -95,55 +85,33 @@ export function planFlyDeploy(input) {
|
|
|
95
85
|
`fly ips allocate-v4 --shared --app ${appName}`,
|
|
96
86
|
`fly ips allocate-v6 --app ${appName}`,
|
|
97
87
|
];
|
|
98
|
-
if (
|
|
99
|
-
runbook.push(``, `#
|
|
100
|
-
}
|
|
101
|
-
if (optionalSecrets.length > 0) {
|
|
102
|
-
runbook.push(``, `# Optional secrets — set only when the matching feature is configured:`, ...optionalSecrets.map((s) => `# ${s.name}: ${s.hint}`), `# fly secrets set --app ${appName} ${optionalSecrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
103
|
-
}
|
|
104
|
-
runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, ...(input.agentPrefix
|
|
105
|
-
? [`# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`]
|
|
106
|
-
: []), `# Run this from ${input.agentPrefix ? "the workspace root" : "this directory"}:`, deployCmd);
|
|
107
|
-
if (input.shipsGit) {
|
|
108
|
-
runbook.push(``, `# The image is a WYSIWYG snapshot of this directory. Freshness/durability run through git, driven`, `# by the agent itself: .git ships in the image (see .dockerignore) and git is baked in, so the agent`, `# can pull to freshen content and commit/push its work back (creds ride config.deploy.secrets; the`, `# POLICY — push vs PR, identity — lives in persona.md). CAVEAT: whether .git survives the upload is`, `# host-CLI-dependent — verify \`git status\` on the box; if missing, have the agent clone instead.`, `# Un-pushed changes on the box never survive a redeploy; durability lives in git.`);
|
|
88
|
+
if (secrets.length > 0) {
|
|
89
|
+
runbook.push(``, `# Secrets (replace each <value>):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
109
90
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
// `AuthResult.source` non-env labels), so the wording doesn't prejudge whether it's migratable.
|
|
91
|
+
runbook.push(``, `# The build context is the WORKSPACE ROOT (the whole directory is baked as the agent's cwd).`, `# The config/Dockerfile live under ${input.agentPrefix} so they never collide with the workspace's own.`, `# Before a new definition release, run \`fastagent deploy fly\` to refresh the release manifest.`, `# Run this from the workspace root:`, deployCmd);
|
|
92
|
+
runbook.push(``, `# The volume keeps /data/base (including uncommitted work), .state and .secrets across restarts and deploys.`, `# Each generated release replaces only /data/base/${input.agentPrefix}; other workspace files are initialized once.`, input.shipsGit
|
|
93
|
+
? `# Git is available for optional collaboration. Verify uploaded history with \`git status\`; clone if the host stripped it.`
|
|
94
|
+
: `# To use Git for collaboration, add deploy: { apt: ["git"] }. Storage durability does not require Git.`);
|
|
95
|
+
// Model-auth guidance: an env key becomes a secret above.
|
|
116
96
|
if (!isEnvKey(modelAuth)) {
|
|
117
97
|
runbook.push(``, modelAuth === undefined
|
|
118
|
-
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here;
|
|
98
|
+
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; set FASTAGENT_AUTH_PATH (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
119
99
|
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
|
|
120
100
|
}
|
|
121
|
-
// The fastagent-only post-step: point each channel at the live URL.
|
|
122
|
-
// is the shared channel-ingress kernel's answer; Fly's contribution is that its URL is deterministic.
|
|
101
|
+
// The fastagent-only post-step: point each channel at the live URL.
|
|
123
102
|
const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
|
|
124
103
|
const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
|
|
125
104
|
if (post.length > 0)
|
|
126
105
|
runbook.push(``, ...post);
|
|
127
|
-
// Single-machine tier: state lives on ONE volume tied to ONE machine.
|
|
128
|
-
// splits state (each gets its own volume) — that needs a shared/external backend, not this recipe.
|
|
106
|
+
// Single-machine tier: state lives on ONE volume tied to ONE machine.
|
|
129
107
|
runbook.push(``, `# Keep this a SINGLE machine: the /data volume (and all state on it) is tied to one machine.`, `# Multiple machines would each get their own volume and split sessions/turns — don't scale past 1.`);
|
|
130
108
|
return { artifacts, runbook };
|
|
131
109
|
}
|
|
132
|
-
/**
|
|
133
|
-
* The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent — the KEEP-mode
|
|
134
|
-
* single source (a user who renamed the app is not overridden by the basename guess). Accepts TOML's
|
|
135
|
-
* double OR single quotes; anything else (or no `app`) is undefined → the caller falls back to basename.
|
|
136
|
-
*/
|
|
110
|
+
/** The `app` name from an existing fly.toml's `app = "…"` line, or undefined if absent. */
|
|
137
111
|
export function parseFlyAppName(toml) {
|
|
138
112
|
return toml.match(/^\s*app\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
139
113
|
}
|
|
140
|
-
/** The `
|
|
141
|
-
* volume lands in the machine's region (fly.toml is the single source; see {@link parseFlyAppName}). */
|
|
142
|
-
export function parseFlyRegion(toml) {
|
|
143
|
-
return toml.match(/^\s*primary_region\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
144
|
-
}
|
|
145
|
-
/** The `min_machines_running` from a fly.toml, or undefined — the KEEP-mode check reads it so a kept
|
|
146
|
-
* file that still scales to zero can be warned about when time triggers (schedules/wake) exist. */
|
|
114
|
+
/** The `min_machines_running` from a fly.toml, or undefined. */
|
|
147
115
|
export function parseFlyMinMachines(toml) {
|
|
148
116
|
const m = toml.match(/^\s*min_machines_running\s*=\s*(\d+)/m)?.[1];
|
|
149
117
|
return m === undefined ? undefined : Number(m);
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -1,39 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
|
|
4
|
-
* coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
|
|
5
|
-
* local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
|
|
6
|
-
* (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
|
|
7
|
-
* exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
|
|
8
|
-
* gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
|
|
9
|
-
* `add <channel>` prints).
|
|
10
|
-
*
|
|
11
|
-
* flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
|
|
12
|
-
* records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
|
|
13
|
-
* encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
|
|
14
|
-
*
|
|
15
|
-
* Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
|
|
16
|
-
* builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
|
|
17
|
-
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
|
-
* never land in argv/process listings.
|
|
19
|
-
*/
|
|
20
|
-
import { type Registrars } from "../channel-ingress.ts";
|
|
1
|
+
/** `fastagent deploy fly --run` — drive flyctl to completion. */
|
|
2
|
+
import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
21
3
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
22
4
|
import type { CliRunner } from "../runner.ts";
|
|
23
5
|
export interface FlyRunPlan {
|
|
24
6
|
appName: string;
|
|
25
|
-
|
|
26
|
-
/** `KEY=value` secrets to set on Fly: model key (env auth) or `FASTAGENT_AUTH_SEED` (file auth) +
|
|
27
|
-
* channel secrets. Set via stdin, never argv. */
|
|
7
|
+
/** `KEY=value` secrets to set on Fly: the value file's variables + `FASTAGENT_AUTH_SEED` for a file credential. */
|
|
28
8
|
secrets: Record<string, string>;
|
|
29
|
-
/**
|
|
9
|
+
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
30
10
|
missingSecrets: string[];
|
|
11
|
+
/** That value file, workspace-relative, so the gate names the file this deploy actually read. */
|
|
12
|
+
valueFile: string;
|
|
31
13
|
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
32
14
|
channels: readonly DeclaredChannel[];
|
|
33
15
|
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
|
|
34
16
|
flyConfig: string;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
17
|
+
/**
|
|
18
|
+
* Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context — flyctl would otherwise
|
|
19
|
+
* resolve it relative to the config's own directory).
|
|
20
|
+
*/
|
|
37
21
|
dockerfile: string;
|
|
38
22
|
}
|
|
39
23
|
/** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
|
|
@@ -44,36 +28,17 @@ export type FlyRunOutcome = {
|
|
|
44
28
|
gate: string;
|
|
45
29
|
};
|
|
46
30
|
/**
|
|
47
|
-
* Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an
|
|
48
|
-
*
|
|
49
|
-
* passes the coarse test and still resolves `<app>.fly.dev` to an AAAA record alone, so an IPv4-only
|
|
50
|
-
* webhook sender (Telegram, GitHub) reproduces #425 against it.
|
|
51
|
-
*
|
|
52
|
-
* The list is EVERY assignment the app holds, and a Flycast or egress address carries a non-empty
|
|
53
|
-
* `Address` too — reading one as routable pre-empts flyctl's own first-deploy fallback, which returns
|
|
54
|
-
* early once any assignment exists.
|
|
55
|
-
*
|
|
56
|
-
* Output that is not a JSON array THROWS rather than reading as "no address": the caller turns that
|
|
57
|
-
* into a gate, because a list we cannot read is not a state we can act on.
|
|
31
|
+
* Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an ingress address" is
|
|
32
|
+
* not the question the two allocate commands answer.
|
|
58
33
|
*/
|
|
59
34
|
export declare function ingressAddresses(stdout: string): {
|
|
60
35
|
v4: boolean;
|
|
61
36
|
v6: boolean;
|
|
62
37
|
};
|
|
63
38
|
/**
|
|
64
|
-
* Whether
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* THROWS on output that is not a JSON array, for the same reason {@link ingressAddresses} does —
|
|
69
|
-
* `false` here means "the host does not have it", and answering that for a list nobody could read
|
|
70
|
-
* creates a SECOND volume, or in teardown destroys nothing at all.
|
|
39
|
+
* Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field; the live probe
|
|
40
|
+
* (test/live/fly.live.test.ts) is what pins that against real output, since an offline fixture can only repeat what
|
|
41
|
+
* we already believe.
|
|
71
42
|
*/
|
|
72
43
|
export declare function listHasName(stdout: string, name: string): boolean;
|
|
73
|
-
|
|
74
|
-
* Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
|
|
75
|
-
* post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
|
|
76
|
-
* host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
77
|
-
* fail-visible.
|
|
78
|
-
*/
|
|
79
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<FlyRunOutcome>;
|
|
44
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars, healthProbe?: PublicHealthProbe): Promise<FlyRunOutcome>;
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -1,40 +1,14 @@
|
|
|
1
|
+
/** `fastagent deploy fly --run` — drive flyctl to completion. */
|
|
2
|
+
import { publicHealthGate, registerWebhooks } from "../channel-ingress.js";
|
|
3
|
+
import { missingValuesGate } from "../secrets.js";
|
|
1
4
|
/**
|
|
2
|
-
* `
|
|
3
|
-
*
|
|
4
|
-
* coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
|
|
5
|
-
* local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
|
|
6
|
-
* (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
|
|
7
|
-
* exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
|
|
8
|
-
* gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
|
|
9
|
-
* `add <channel>` prints).
|
|
10
|
-
*
|
|
11
|
-
* flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
|
|
12
|
-
* records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
|
|
13
|
-
* encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
|
|
14
|
-
*
|
|
15
|
-
* Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
|
|
16
|
-
* builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
|
|
17
|
-
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
|
-
* never land in argv/process listings.
|
|
5
|
+
* The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate command, because
|
|
6
|
+
* that is the granularity the answer is acted on at.
|
|
19
7
|
*/
|
|
20
|
-
import { registerWebhooks } from "../channel-ingress.js";
|
|
21
|
-
/** The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate
|
|
22
|
-
* command, because that is the granularity the answer is acted on at. The rest of flyctl's
|
|
23
|
-
* vocabulary — `private_v6` (Flycast), `egress_v4`, `egress_v6`, `egress_pair` — is internal or
|
|
24
|
-
* outbound. */
|
|
25
8
|
const INGRESS_TYPES = { v4: ["v4", "shared_v4"], v6: ["v6"] };
|
|
26
9
|
/**
|
|
27
|
-
* Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an
|
|
28
|
-
*
|
|
29
|
-
* passes the coarse test and still resolves `<app>.fly.dev` to an AAAA record alone, so an IPv4-only
|
|
30
|
-
* webhook sender (Telegram, GitHub) reproduces #425 against it.
|
|
31
|
-
*
|
|
32
|
-
* The list is EVERY assignment the app holds, and a Flycast or egress address carries a non-empty
|
|
33
|
-
* `Address` too — reading one as routable pre-empts flyctl's own first-deploy fallback, which returns
|
|
34
|
-
* early once any assignment exists.
|
|
35
|
-
*
|
|
36
|
-
* Output that is not a JSON array THROWS rather than reading as "no address": the caller turns that
|
|
37
|
-
* into a gate, because a list we cannot read is not a state we can act on.
|
|
10
|
+
* Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an ingress address" is
|
|
11
|
+
* not the question the two allocate commands answer.
|
|
38
12
|
*/
|
|
39
13
|
export function ingressAddresses(stdout) {
|
|
40
14
|
const entries = JSON.parse(stdout);
|
|
@@ -44,30 +18,22 @@ export function ingressAddresses(stdout) {
|
|
|
44
18
|
return { v4: has(INGRESS_TYPES.v4), v6: has(INGRESS_TYPES.v6) };
|
|
45
19
|
}
|
|
46
20
|
/**
|
|
47
|
-
* Whether
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* THROWS on output that is not a JSON array, for the same reason {@link ingressAddresses} does —
|
|
52
|
-
* `false` here means "the host does not have it", and answering that for a list nobody could read
|
|
53
|
-
* creates a SECOND volume, or in teardown destroys nothing at all.
|
|
21
|
+
* Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field; the live probe
|
|
22
|
+
* (test/live/fly.live.test.ts) is what pins that against real output, since an offline fixture can only repeat what
|
|
23
|
+
* we already believe.
|
|
54
24
|
*/
|
|
55
25
|
export function listHasName(stdout, name) {
|
|
56
26
|
const entries = JSON.parse(stdout);
|
|
57
27
|
if (!Array.isArray(entries))
|
|
58
28
|
throw new Error(`expected a JSON array, got ${typeof entries}`);
|
|
59
|
-
return entries.some((o) => o.Name === name
|
|
29
|
+
return entries.some((o) => o.Name === name);
|
|
60
30
|
}
|
|
61
31
|
/**
|
|
62
|
-
* A read-only `fly … list --json`, reduced to the question the next step asks of it — or the gate for
|
|
63
|
-
*
|
|
64
|
-
* unreadable, or the host gave a verdict. Collapsing the middle one into `false` is what shipped #425
|
|
65
|
-
* and what would have skipped a teardown.
|
|
32
|
+
* A read-only `fly … list --json`, reduced to the question the next step asks of it — or the gate for a list we cannot
|
|
33
|
+
* act on.
|
|
66
34
|
*/
|
|
67
35
|
async function readList(fly, args, read) {
|
|
68
|
-
// The command as RUN, not a restatement of it
|
|
69
|
-
// has to name one that works from their cwd, and `fly ips list --json` without `-a <app>` does not
|
|
70
|
-
// (fly.toml lives under the agent prefix, so flyctl finds no app).
|
|
36
|
+
// The command as RUN, not a restatement of it.
|
|
71
37
|
const cmd = `fly ${args.join(" ")}`;
|
|
72
38
|
const result = await fly(args, { capture: true });
|
|
73
39
|
if (result.code !== 0)
|
|
@@ -76,34 +42,24 @@ async function readList(fly, args, read) {
|
|
|
76
42
|
return { value: read(result.stdout) };
|
|
77
43
|
}
|
|
78
44
|
catch (error) {
|
|
79
|
-
// The one place a parse failure is allowed to stop being an exception
|
|
80
|
-
// turns it into the operator's gate (printed + non-zero exit), never a default answer.
|
|
45
|
+
// The one place a parse failure is allowed to stop being an exception.
|
|
81
46
|
return {
|
|
82
47
|
gate: `\`${cmd}\` was unreadable (${error instanceof Error ? error.message : String(error)}) — ` +
|
|
83
48
|
`run it yourself and check the flyctl version; fix and re-run`,
|
|
84
49
|
};
|
|
85
50
|
}
|
|
86
51
|
}
|
|
87
|
-
|
|
88
|
-
* Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
|
|
89
|
-
* post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
|
|
90
|
-
* host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
91
|
-
* fail-visible.
|
|
92
|
-
*/
|
|
93
|
-
export async function deployFlyRun(plan, fly, log, registrars) {
|
|
52
|
+
export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
94
53
|
const gate = (g) => ({ ok: false, gate: g });
|
|
95
|
-
// 1.
|
|
96
|
-
// either an interactive login or FLY_API_TOKEN, so one check covers both.
|
|
54
|
+
// 1.
|
|
97
55
|
if ((await fly(["auth", "whoami"], { capture: true })).code !== 0) {
|
|
98
56
|
return gate("not logged in to Fly — run `fly auth login` (opens a browser), or set FLY_API_TOKEN, then re-run");
|
|
99
57
|
}
|
|
100
58
|
// 2. Gate missing required secret VALUES before any side effect (no half-created infra).
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// 3.
|
|
105
|
-
// own gate ({@link readList}): inferring "absent" from a query nobody could read would then
|
|
106
|
-
// misreport the create as a name clash.
|
|
59
|
+
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
60
|
+
if (missingValues)
|
|
61
|
+
return gate(missingValues);
|
|
62
|
+
// 3.
|
|
107
63
|
const appExists = await readList(fly, ["apps", "list", "--json"], (out) => listHasName(out, plan.appName));
|
|
108
64
|
if ("gate" in appExists)
|
|
109
65
|
return gate(appExists.gate);
|
|
@@ -117,34 +73,15 @@ export async function deployFlyRun(plan, fly, log, registrars) {
|
|
|
117
73
|
`Set a unique \`app\` in fly.toml and re-run.`);
|
|
118
74
|
}
|
|
119
75
|
}
|
|
120
|
-
// 4.
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return gate(volumeExists.gate);
|
|
125
|
-
if (volumeExists.value) {
|
|
126
|
-
log(`volume data exists — skipping create`);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
log(`creating volume data in ${plan.region}…`);
|
|
130
|
-
if ((await fly(["volumes", "create", "data", "-a", plan.appName, "--region", plan.region, "--size", "1", "--yes"]))
|
|
131
|
-
.code !== 0) {
|
|
132
|
-
return gate("`fly volumes create` failed — see the flyctl output above");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// 5. Ingress address — `[http_service]` in fly.toml DECLARES a service; it does not create an address
|
|
136
|
-
// to reach it on. `fly launch` allocates one as part of its flow, and this driver does not use it,
|
|
137
|
-
// so without this step the deploy succeeds, the machine serves, and `https://<app>.fly.dev` has no
|
|
138
|
-
// DNS record at all — reported as a healthy deploy, and handed to the webhook registrars as a URL
|
|
139
|
-
// they then fail to reach.
|
|
76
|
+
// 4. NO volume step: `fly deploy` creates it from [mounts] on a first deploy, and only it can tell the scheduler
|
|
77
|
+
// which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
|
|
78
|
+
// blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
|
|
79
|
+
// 5.
|
|
140
80
|
const addresses = await readList(fly, ["ips", "list", "-a", plan.appName, "--json"], ingressAddresses);
|
|
141
81
|
if ("gate" in addresses)
|
|
142
82
|
return gate(addresses.gate);
|
|
143
|
-
// Check-then-act PER FAMILY: an app that already holds one must still be given the other, or the
|
|
144
|
-
//
|
|
145
|
-
// asks for sees the v4 it just made, skips, and reports success on an app with no v6 (and, the way
|
|
146
|
-
// that matters, no v4). v4 SHARED and v6 are both free; a dedicated v4 is billed, so it stays an
|
|
147
|
-
// operator decision — `fly ips allocate-v4 -a <app>` after the fact, which this step reads as v4.
|
|
83
|
+
// Check-then-act PER FAMILY: an app that already holds one must still be given the other, or the gate between the
|
|
84
|
+
// two allocations below heals into a permanent half-state.
|
|
148
85
|
for (const [family, allocate] of [
|
|
149
86
|
["v4", ["ips", "allocate-v4", "--shared", "-a", plan.appName]],
|
|
150
87
|
["v6", ["ips", "allocate-v6", "-a", plan.appName]],
|
|
@@ -167,8 +104,7 @@ export async function deployFlyRun(plan, fly, log, registrars) {
|
|
|
167
104
|
return gate("`fly secrets import` failed — see the flyctl output above");
|
|
168
105
|
}
|
|
169
106
|
}
|
|
170
|
-
// 7.
|
|
171
|
-
// explicitly (the workspace root is the context; the Dockerfile lives under fastagent/).
|
|
107
|
+
// 7.
|
|
172
108
|
log("deploying (remote build)…");
|
|
173
109
|
const deployArgs = [
|
|
174
110
|
"deploy",
|
|
@@ -186,10 +122,21 @@ export async function deployFlyRun(plan, fly, log, registrars) {
|
|
|
186
122
|
if ((await fly(deployArgs)).code !== 0) {
|
|
187
123
|
return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
|
|
188
124
|
}
|
|
189
|
-
// 8.
|
|
190
|
-
|
|
125
|
+
// 8. `fly deploy` exits 0 on a machine that then crash-loops, so readiness is asked here and not inferred.
|
|
126
|
+
const baseUrl = `https://${plan.appName}.fly.dev`;
|
|
127
|
+
const healthGate = await publicHealthGate({
|
|
128
|
+
baseUrl,
|
|
129
|
+
channels: plan.channels,
|
|
130
|
+
log,
|
|
131
|
+
inspectHint: `the app itself deployed — inspect \`fly logs -a ${plan.appName}\`, then re-run once it answers ` +
|
|
132
|
+
`(a re-run repeats the remote build)`,
|
|
133
|
+
probe: healthProbe,
|
|
134
|
+
});
|
|
135
|
+
if (healthGate)
|
|
136
|
+
return gate(healthGate);
|
|
137
|
+
// 9.
|
|
191
138
|
const registrationGateMsg = await registerWebhooks({
|
|
192
|
-
baseUrl
|
|
139
|
+
baseUrl,
|
|
193
140
|
channels: plan.channels,
|
|
194
141
|
registrars,
|
|
195
142
|
log,
|
package/dist/deploy/hosts.d.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* The deploy targets, as a value: the CLI's `<host>` choices and the host-only-flag table's
|
|
3
|
+
* exhaustiveness check both read it. Dependency-free, so `cli/program.ts` can import it at load
|
|
4
|
+
* time without pulling a command module.
|
|
5
|
+
*
|
|
6
|
+
* ── ADDING A HOST ───────────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* Three edits: a directory beside this file (copy `fly/`: `plan.ts` pure, `run.ts` driving the host
|
|
9
|
+
* CLI behind the runner seam), its `HostDeploy` in `cli/commands/deploy/<host>.ts` (kept-file
|
|
10
|
+
* semantics, gates, the drive glue — the half that may exit the process), and its name in the array
|
|
11
|
+
* below. `deploy/` itself is a neutral kernel plus one directory per host; nothing host-specific
|
|
12
|
+
* belongs in the kernel, and no host may hold a fact about another one (a host-only flag is a row in
|
|
13
|
+
* `HOST_ONLY_FLAGS` in `cli/commands/deploy.ts`, warning elsewhere; `--tunnel` stays a usage GATE in `runDeploy`
|
|
14
|
+
* because a refusal is not a row).
|
|
15
|
+
*
|
|
16
|
+
* BEFORE writing one, read that host's docs for what it does NOT do implicitly — above all, whether
|
|
17
|
+
* a created resource is REACHABLE without a further step. On Fly, `[http_service]` declares a service and allocates
|
|
18
|
+
* no address, and v4 and v6 are separate commands, so an app with only v6 is unreachable from an IPv4-only webhook
|
|
19
|
+
* sender. The host's own docs and CLI source usually say so; read them before a real deploy has to.
|
|
20
|
+
*
|
|
21
|
+
* And when a driver PARSES what a host CLI printed: the judgement must match the GRANULARITY of the
|
|
22
|
+
* action it drives (one question per command), and "we could not read the host" is a third answer,
|
|
23
|
+
* never "absent".
|
|
24
|
+
*/
|
|
4
25
|
export declare const DEPLOY_HOSTS: readonly ["docker", "fly", "railway", "agentcore"];
|
|
5
26
|
export type DeployHost = (typeof DEPLOY_HOSTS)[number];
|