@fastagent-sh/fastagent 0.29.0 → 0.30.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 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/kernel.js +3 -0
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -1,67 +1,27 @@
|
|
|
1
1
|
import { webhookPaths } from "../channel-ingress.js";
|
|
2
|
+
import { manualWebhookStep, webhookKinds } from "../../channels/webhooks.js";
|
|
2
3
|
import { containerArtifacts } from "../container.js";
|
|
3
4
|
import { deploymentLoginCommand } from "../box-shell.js";
|
|
4
5
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
6
|
+
import { stripEdges, stripEnd } from "../../strip-edges.js";
|
|
5
7
|
const MOUNT = "/data";
|
|
6
8
|
export const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
7
9
|
export const DOCKER_VALUE_FILE = `${SECRETS_DIRNAME}/production/.env`;
|
|
8
10
|
const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
|
|
9
11
|
export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
10
|
-
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
11
|
-
export const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
|
|
12
12
|
/** Whether a Compose file still opts into generated-drift warnings (drop the marker after taking ownership). */
|
|
13
13
|
export function isGeneratedCompose(content) {
|
|
14
14
|
return content.startsWith(GENERATED_COMPOSE_MARKER);
|
|
15
15
|
}
|
|
16
|
-
/** Whether an authoritative Compose file already carries the generated tunnel service contract. */
|
|
17
|
-
export function composeHasTunnelService(content) {
|
|
18
|
-
return /^[ \t]+tunnel:\s*(?:#.*)?$/m.test(content);
|
|
19
|
-
}
|
|
20
16
|
/** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
|
|
21
17
|
export function toDockerProjectName(directoryName) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
25
|
-
.replace(/^[-_]+|[-_]+$/g, "")
|
|
26
|
-
.slice(0, 54)
|
|
27
|
-
.replace(/[-_]+$/g, "");
|
|
18
|
+
const separator = (char) => char === "-" || char === "_";
|
|
19
|
+
const slug = stripEnd(stripEdges(directoryName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-"), separator).slice(0, 54), separator);
|
|
28
20
|
return `fastagent-${slug || "agent"}`;
|
|
29
21
|
}
|
|
30
|
-
/** `${NAME:-}` without making JavaScript treat it as interpolation. */
|
|
31
|
-
function composeInterpolation(name) {
|
|
32
|
-
return `\${${name}:-}`;
|
|
33
|
-
}
|
|
34
22
|
function composeYaml(input) {
|
|
35
|
-
// Compose sits beside the Dockerfile, in the agent directory, which is the build context.
|
|
36
|
-
const tunnelService = input.tunnel
|
|
37
|
-
? `
|
|
38
|
-
# Cloudflare Quick Tunnel: ephemeral URL, generated only with \`deploy docker --tunnel\`.
|
|
39
|
-
# No restart policy on purpose: a new process gets a new URL and needs webhook re-registration.
|
|
40
|
-
tunnel:
|
|
41
|
-
image: ${CLOUDFLARED_IMAGE}
|
|
42
|
-
command:
|
|
43
|
-
- tunnel
|
|
44
|
-
- --no-autoupdate
|
|
45
|
-
- --url
|
|
46
|
-
- http://agent:${input.port}
|
|
47
|
-
depends_on:
|
|
48
|
-
- agent
|
|
49
|
-
environment:
|
|
50
|
-
# Docker Desktop may inject HTTP(S)_PROXY into containers. The origin is Docker-internal and must
|
|
51
|
-
# bypass it; preserve any operator NO_PROXY entries after the required service/loopback names.
|
|
52
|
-
NO_PROXY: "agent,localhost,127.0.0.1,${composeInterpolation("NO_PROXY")}"
|
|
53
|
-
no_proxy: "agent,localhost,127.0.0.1,${composeInterpolation("no_proxy")}"
|
|
54
|
-
# Quieter than the agent, but bounded for the same reason: nothing else on this host bounds it.
|
|
55
|
-
logging:
|
|
56
|
-
driver: json-file
|
|
57
|
-
options:
|
|
58
|
-
max-size: "10m"
|
|
59
|
-
max-file: "3"
|
|
60
|
-
restart: "no"
|
|
61
|
-
`
|
|
62
|
-
: "";
|
|
63
23
|
return `${GENERATED_COMPOSE_MARKER}. Edit freely — it is kept unless you pass --force.
|
|
64
|
-
#
|
|
24
|
+
# Public ingress remains yours: add your own tunnel/proxy/DNS/TLS, and name its URL as http.publicUrl.
|
|
65
25
|
name: ${input.projectName}
|
|
66
26
|
|
|
67
27
|
services:
|
|
@@ -98,11 +58,33 @@ services:
|
|
|
98
58
|
max-size: "10m"
|
|
99
59
|
max-file: "3"
|
|
100
60
|
restart: unless-stopped
|
|
101
|
-
|
|
61
|
+
|
|
102
62
|
volumes:
|
|
103
63
|
state:
|
|
104
64
|
`;
|
|
105
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* What `--run` says of the webhook channels once Compose is up. Docker has no URL of its own: with `http.publicUrl`
|
|
68
|
+
* named in the delivered configuration the agent points its own channels there (its `/health` says how each went) and
|
|
69
|
+
* only Slack's Request URL is the operator's; without one, nothing can reach those paths yet.
|
|
70
|
+
*/
|
|
71
|
+
export function ingressNotes(channels, publicUrl, healthUrl) {
|
|
72
|
+
const paths = webhookPaths(channels);
|
|
73
|
+
if (paths.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
if (publicUrl === undefined) {
|
|
76
|
+
return [
|
|
77
|
+
`${paths.join(", ")} receive nothing until a public URL reaches this container — configure your own ` +
|
|
78
|
+
`tunnel/proxy, point those webhook path(s) (or your remapped channel routes) at it and name its URL as ` +
|
|
79
|
+
`http.publicUrl in the configuration --config delivers, or set the channel's ingress to "connection" ` +
|
|
80
|
+
`where it has one`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
`the agent points its webhook channels at ${publicUrl} on every start; ${healthUrl ?? "its /health"} says how each went`,
|
|
85
|
+
...(webhookKinds(channels).includes("slack") ? [manualWebhookStep("slack", publicUrl)] : []),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
106
88
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
107
89
|
export function planDockerDeploy(input) {
|
|
108
90
|
const composePath = DOCKER_COMPOSE_FILE;
|
|
@@ -121,17 +103,14 @@ export function planDockerDeploy(input) {
|
|
|
121
103
|
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}`));
|
|
122
104
|
}
|
|
123
105
|
runbook.push(`# Run from the agent directory. The build bakes it as the definition and installs its`, `# package.json dependencies.`);
|
|
124
|
-
runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT}
|
|
106
|
+
runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT} is the agent's working directory, and retains its state and credentials.`, `# --pull: the base image is a floating tag, and a copy cached here may be older than the runtime floor.`, `${compose} build --pull`, `${compose} up -d`, `curl --fail http://127.0.0.1:${input.port}/health`, ...(input.boxLogin
|
|
125
107
|
? [
|
|
126
108
|
`# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself:`,
|
|
127
109
|
`${deploymentLoginCommand("docker", input.boxLogin)}`,
|
|
128
110
|
]
|
|
129
111
|
: []), ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: deletes credentials and all agent state`);
|
|
130
|
-
if (
|
|
131
|
-
runbook.push(``, `#
|
|
132
|
-
}
|
|
133
|
-
else if (paths.length > 0) {
|
|
134
|
-
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.`);
|
|
112
|
+
if (paths.length > 0) {
|
|
113
|
+
runbook.push(``, `# Public ingress is operator-owned: add your tunnel/proxy/DNS/TLS, then route the detected default`, `# endpoint(s) to the agent: ${paths.map((path) => `https://<your-domain>${path}`).join(", ")}.`, `# If your channels/*.ts glue remaps a route, use that path instead. Name that URL as http.publicUrl in the`, `# configuration \`--config\` delivers, and the agent points Telegram and Feishu/Lark at it on every start`, `# (Slack's Request URL stays yours to set).`);
|
|
135
114
|
}
|
|
136
115
|
runbook.push(`# Keep one agent replica: the local state volume is single-process; multiple replicas split/corrupt state.`);
|
|
137
116
|
return { artifacts, composePath, runbook };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
2
|
-
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
3
|
-
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
4
2
|
import { type CliRunner } from "../runner.ts";
|
|
5
3
|
export interface DockerRunPlan {
|
|
6
4
|
/** Compose file relative to the runner cwd (the agent directory). */
|
|
@@ -8,20 +6,10 @@ export interface DockerRunPlan {
|
|
|
8
6
|
/** Container port from config; used to ask Compose for the effective published host port. */
|
|
9
7
|
port: number;
|
|
10
8
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Make sure the value file Compose reads by `env_file:` holds a value for every name the deployment needs, asking for
|
|
10
|
+
* the ones it lacks: resolves a gate line, or undefined. Nothing travels through this process.
|
|
13
11
|
*/
|
|
14
|
-
|
|
15
|
-
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
16
|
-
missingSecrets: string[];
|
|
17
|
-
/** That value file, agent-dir-relative, so the gate names the file this deploy actually read. */
|
|
18
|
-
valueFile: string;
|
|
19
|
-
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
|
|
20
|
-
announce: DockerAnnounce;
|
|
21
|
-
/** Every declared channel and its ingress: which of them a failed login leaves unregistered. */
|
|
22
|
-
channels: readonly DeclaredChannel[];
|
|
23
|
-
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
24
|
-
requireTunnel: boolean;
|
|
12
|
+
provideSecrets: () => Promise<string | undefined>;
|
|
25
13
|
/**
|
|
26
14
|
* Log the box in (`fastagent login --deployment`) once it is up and before any webhook is pointed at it: a channel pointed at
|
|
27
15
|
* a box with no model credential answers every message with a failure. Resolves a gate line, or undefined.
|
|
@@ -31,23 +19,13 @@ export interface DockerRunPlan {
|
|
|
31
19
|
export type DockerRunOutcome = {
|
|
32
20
|
ok: true;
|
|
33
21
|
url?: string;
|
|
34
|
-
tunnelUrl?: string;
|
|
35
22
|
}
|
|
36
|
-
/**
|
|
37
|
-
* `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know where it is and what
|
|
38
|
-
* to re-run.
|
|
39
|
-
*/
|
|
23
|
+
/** `url` travels with a gate too: Compose is up, so the operator still needs to know where. */
|
|
40
24
|
| {
|
|
41
25
|
ok: false;
|
|
42
26
|
gate: string;
|
|
43
27
|
url?: string;
|
|
44
|
-
tunnelUrl?: string;
|
|
45
28
|
};
|
|
46
|
-
/** Register the deployment's webhooks against its public URL, reporting what each registrar answered. */
|
|
47
|
-
type DockerAnnounce = (baseUrl: string) => Promise<{
|
|
48
|
-
kind: string;
|
|
49
|
-
outcome: RegistrationOutcome;
|
|
50
|
-
}[]>;
|
|
51
29
|
/**
|
|
52
30
|
* `stillStarting` answers whether the agent container is still up; a probe that ignores it simply waits out its whole
|
|
53
31
|
* budget.
|
|
@@ -58,12 +36,6 @@ export type DockerHealthProbe = (healthUrl: string, stillStarting: () => Promise
|
|
|
58
36
|
* publish free", asked before `up`, because afterwards a `/health` that answers cannot say whose it is.
|
|
59
37
|
*/
|
|
60
38
|
export type HostPortProbe = (host: string, port: number) => Promise<boolean>;
|
|
61
|
-
/** A published Quick Tunnel URL, and whether its tunnel ever reported an edge connection. */
|
|
62
|
-
export interface ComposeTunnel {
|
|
63
|
-
url: string;
|
|
64
|
-
connected: boolean;
|
|
65
|
-
}
|
|
66
|
-
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string) => Promise<ComposeTunnel | undefined>;
|
|
67
39
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
68
40
|
export declare function localUrlFromComposePort(stdout: string): string | undefined;
|
|
69
41
|
/**
|
|
@@ -75,14 +47,4 @@ export declare function agentPublishedPorts(stdout: string): {
|
|
|
75
47
|
host: string;
|
|
76
48
|
port: number;
|
|
77
49
|
}[];
|
|
78
|
-
|
|
79
|
-
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
80
|
-
* connection.
|
|
81
|
-
*/
|
|
82
|
-
export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, options?: {
|
|
83
|
-
attempts?: number;
|
|
84
|
-
intervalMs?: number;
|
|
85
|
-
sleep?: (ms: number) => Promise<void>;
|
|
86
|
-
}): Promise<ComposeTunnel | undefined>;
|
|
87
|
-
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe, hostPortBusy?: HostPortProbe): Promise<DockerRunOutcome>;
|
|
88
|
-
export {};
|
|
50
|
+
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, hostPortBusy?: HostPortProbe): Promise<DockerRunOutcome>;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
2
2
|
import { connect } from "node:net";
|
|
3
3
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
4
|
-
import { loginGate } from "../channel-ingress.js";
|
|
5
|
-
import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
|
|
6
|
-
import { registrationGate } from "../registration-gate.js";
|
|
7
4
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
8
5
|
import { readOutput } from "../runner.js";
|
|
9
|
-
import { missingValuesGate } from "../secrets.js";
|
|
10
6
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
11
7
|
export function localUrlFromComposePort(stdout) {
|
|
12
8
|
const line = stdout
|
|
@@ -52,29 +48,6 @@ export function agentPublishedPorts(stdout) {
|
|
|
52
48
|
});
|
|
53
49
|
}
|
|
54
50
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
-
/**
|
|
56
|
-
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
57
|
-
* connection.
|
|
58
|
-
*/
|
|
59
|
-
export async function waitForComposeTunnelUrl(docker, composeFile, options = {}) {
|
|
60
|
-
const compose = ["compose", "-f", composeFile];
|
|
61
|
-
const attempts = options.attempts ?? 60;
|
|
62
|
-
let assigned;
|
|
63
|
-
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
64
|
-
const logs = await docker([...compose, "logs", "--no-color", "tunnel"], { capture: true });
|
|
65
|
-
if (logs.code === 0) {
|
|
66
|
-
assigned ??= parseTunnelUrl(logs.stdout);
|
|
67
|
-
if (assigned && hasTunnelConnection(logs.stdout)) {
|
|
68
|
-
await (options.sleep ?? sleep)(TUNNEL_DNS_LAG_MS);
|
|
69
|
-
return { url: assigned, connected: true };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (attempt + 1 < attempts)
|
|
73
|
-
await (options.sleep ?? sleep)(options.intervalMs ?? 500);
|
|
74
|
-
}
|
|
75
|
-
return assigned ? { url: assigned, connected: false } : undefined;
|
|
76
|
-
}
|
|
77
|
-
const defaultTunnelUrlProbe = (docker, composeFile) => waitForComposeTunnelUrl(docker, composeFile);
|
|
78
51
|
/**
|
|
79
52
|
* `/health` asked from inside the agent container, for a topology that publishes no host port. The image always has
|
|
80
53
|
* node or bun (the generated Dockerfile's base), and both have `fetch`; nothing else is assumed installed.
|
|
@@ -92,7 +65,7 @@ async function containerHealthy(docker, compose, port, stillStarting) {
|
|
|
92
65
|
}
|
|
93
66
|
return false;
|
|
94
67
|
}
|
|
95
|
-
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe,
|
|
68
|
+
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, hostPortBusy = defaultHostPortBusy) {
|
|
96
69
|
const gate = (message) => ({ ok: false, gate: message });
|
|
97
70
|
const compose = ["compose", "-f", plan.composeFile];
|
|
98
71
|
// CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
|
|
@@ -103,17 +76,10 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
103
76
|
if (version.code !== 0) {
|
|
104
77
|
return gate("Docker Compose plugin is unavailable — install/enable `docker compose`, then re-run");
|
|
105
78
|
}
|
|
106
|
-
// The values
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
return gate(
|
|
110
|
-
// Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
|
|
111
|
-
// hands Compose nothing. Saying "passing N secrets to Compose" would be false, and doubly so on a hand-owned
|
|
112
|
-
// Compose file that has no `env_file` entry at all.
|
|
113
|
-
const secretNames = Object.keys(plan.secrets);
|
|
114
|
-
if (secretNames.length > 0) {
|
|
115
|
-
log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
|
|
116
|
-
}
|
|
79
|
+
// The values come before the first side effect (build/create).
|
|
80
|
+
const secretsGate = await plan.provideSecrets();
|
|
81
|
+
if (secretsGate !== undefined)
|
|
82
|
+
return gate(secretsGate);
|
|
117
83
|
if ((await docker(["info"], { capture: true })).code !== 0) {
|
|
118
84
|
return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
|
|
119
85
|
}
|
|
@@ -127,11 +93,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
127
93
|
if (!services.includes("agent")) {
|
|
128
94
|
return gate(`Compose file must keep the "agent" service for \`fastagent deploy docker --run\``);
|
|
129
95
|
}
|
|
130
|
-
const hasTunnel = services.includes("tunnel");
|
|
131
|
-
if (plan.requireTunnel && !hasTunnel) {
|
|
132
|
-
return gate(`--tunnel was requested but the kept ${plan.composeFile} has no "tunnel" service — ` +
|
|
133
|
-
`edit it, delete it and regenerate, or pass --force`);
|
|
134
|
-
}
|
|
135
96
|
// A port something else already serves would answer the `/health` this run waits for, and the run would report that
|
|
136
97
|
// process's answer as the agent's. A container this project already runs holds its own port, so only a first start
|
|
137
98
|
// (or a start after `down`) asks. Before the first side effect: a refusal leaves nothing to undo.
|
|
@@ -146,19 +107,12 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
146
107
|
if (await hostPortBusy(host, port)) {
|
|
147
108
|
return gate(`${host}:${port} is already in use by another process, so the agent's published port is not free and a ` +
|
|
148
109
|
`health check there would answer for that process, not for the agent — stop it, or move the agent: ` +
|
|
149
|
-
`edit the published port in ${plan.composeFile}, or change http.port in fastagent.config.ts and re-run ` +
|
|
110
|
+
`edit the published port in ${plan.composeFile}, or change defaults.http.port in fastagent.config.ts and re-run ` +
|
|
150
111
|
`with --force (a generated Compose file is kept otherwise)`);
|
|
151
112
|
}
|
|
152
113
|
}
|
|
153
114
|
}
|
|
154
115
|
}
|
|
155
|
-
// Quick Tunnel logs are the control-plane output (the assigned URL).
|
|
156
|
-
if (hasTunnel) {
|
|
157
|
-
log("recreating the ephemeral tunnel service…");
|
|
158
|
-
if ((await docker([...compose, "rm", "-s", "-f", "tunnel"])).code !== 0) {
|
|
159
|
-
return gate(`could not recreate the tunnel service — inspect \`docker compose -f ${plan.composeFile} ps\``);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
116
|
log(`building and reconciling ${plan.composeFile}…`);
|
|
163
117
|
// `--pull`: the base image is a floating tag, and a copy cached here may be older than the runtime floor.
|
|
164
118
|
if ((await docker([...compose, "build", "--pull"])).code !== 0) {
|
|
@@ -173,9 +127,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
173
127
|
if (running.code !== 0 || !runningServices.includes("agent")) {
|
|
174
128
|
return gate(`the Compose service "agent" is not running — inspect with \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
175
129
|
}
|
|
176
|
-
if (hasTunnel && !runningServices.includes("tunnel")) {
|
|
177
|
-
return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
178
|
-
}
|
|
179
130
|
// A user-owned topology may deliberately remove the host port and expose only through its own ingress.
|
|
180
131
|
const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true });
|
|
181
132
|
const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
|
|
@@ -208,33 +159,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
208
159
|
}
|
|
209
160
|
// `url` travels with this gate: Compose is up, and the operator needs to know where.
|
|
210
161
|
const notLoggedIn = await plan.boxLogin?.run();
|
|
211
|
-
if (notLoggedIn)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const channels = hasTunnel ? plan.channels : [];
|
|
215
|
-
return { ok: false, gate: loginGate({ notLoggedIn, channels, log, afterLogin }), url };
|
|
216
|
-
}
|
|
217
|
-
if (!hasTunnel)
|
|
218
|
-
return { ok: true, url };
|
|
219
|
-
log("waiting for the Compose tunnel service to publish its Quick Tunnel URL…");
|
|
220
|
-
const tunnel = await tunnelUrlProbe(docker, plan.composeFile);
|
|
221
|
-
if (!tunnel) {
|
|
222
|
-
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
223
|
-
}
|
|
224
|
-
// The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here.
|
|
225
|
-
if (!tunnel.connected) {
|
|
226
|
-
log(`warn: the tunnel service never reported an edge connection for ${tunnel.url} — announcing it anyway. ` +
|
|
227
|
-
`Nothing reaches a tunnel that has not connected, so a webhook registration that cannot resolve the ` +
|
|
228
|
-
`host is this, not the platform. Inspect \`docker compose -f ${plan.composeFile} logs tunnel\`.`);
|
|
229
|
-
}
|
|
230
|
-
const tunnelUrl = tunnel.url;
|
|
231
|
-
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer that owns the
|
|
232
|
-
// outcome, so it is the layer that can gate on one.
|
|
233
|
-
const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
|
|
234
|
-
for (const { kind, outcome } of await plan.announce(tunnelUrl))
|
|
235
|
-
reg.track(kind, outcome);
|
|
236
|
-
const blocked = reg.gate();
|
|
237
|
-
if (blocked)
|
|
238
|
-
return { ok: false, gate: blocked, url, tunnelUrl };
|
|
239
|
-
return { ok: true, url, tunnelUrl };
|
|
162
|
+
if (notLoggedIn)
|
|
163
|
+
return { ok: false, gate: notLoggedIn, url };
|
|
164
|
+
return { ok: true, url };
|
|
240
165
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A value file as a deployment's store: Docker's (Compose reads it by `env_file:`), and what `start` reads. */
|
|
2
|
+
import { loadEnvValues, writeValues } from "../value-file.js";
|
|
3
|
+
export function fileStore(agentDir, file) {
|
|
4
|
+
return {
|
|
5
|
+
async exists() {
|
|
6
|
+
return true;
|
|
7
|
+
},
|
|
8
|
+
async ensure() { },
|
|
9
|
+
async names() {
|
|
10
|
+
return new Set([...loadEnvValues(file)].filter(([, value]) => value.trim() !== "").map(([name]) => name));
|
|
11
|
+
},
|
|
12
|
+
async write(values) {
|
|
13
|
+
const names = Object.keys(values);
|
|
14
|
+
await writeValues(agentDir, "deployment", names.map((name) => ({ name, value: values[name] })), names, file);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -18,12 +18,13 @@ export interface FlyPlanInput extends ContainerInput {
|
|
|
18
18
|
*/
|
|
19
19
|
channels: readonly DeclaredChannel[];
|
|
20
20
|
/**
|
|
21
|
-
* The runbook's variable list (`deploymentSecrets`):
|
|
22
|
-
* carries.
|
|
21
|
+
* The runbook's variable list (`deploymentSecrets`): the declared names, each hinted by the file that declared it.
|
|
23
22
|
*/
|
|
24
23
|
secrets?: readonly DeploymentSecret[];
|
|
25
24
|
/** `schedules/` declares a schedule — one of the things that forces a machine up (deploy/residency.ts). */
|
|
26
25
|
hasCron: boolean;
|
|
26
|
+
/** The `http.publicUrl` the image delivers, where the platforms reach the box; default the app's own URL. */
|
|
27
|
+
publicUrl?: string;
|
|
27
28
|
}
|
|
28
29
|
export interface FlyPlan {
|
|
29
30
|
/** fly.toml / Dockerfile / .dockerignore — written by the CLI (skipped if present unless --force). */
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { webhookKinds } from "../../channels/webhooks.js";
|
|
1
2
|
import { webhookRunbook } from "../channel-ingress.js";
|
|
2
3
|
import { containerArtifacts, imageHasGit } from "../container.js";
|
|
3
4
|
import { deploymentLoginCommand } from "../box-shell.js";
|
|
4
5
|
import { WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
|
|
6
|
+
import { stripEdges } from "../../strip-edges.js";
|
|
5
7
|
function flyToml(appName, port, residency) {
|
|
6
8
|
// WHY is residency.ts's to decide; the SETTING and its wording are Fly's. Anything else is an edit to the
|
|
7
9
|
// generated file, which is the artifact's job.
|
|
@@ -65,7 +67,7 @@ export function planFlyDeploy(input) {
|
|
|
65
67
|
`# Fly app names are GLOBALLY unique: if this fails as taken, set a unique "app" in fly.toml and`,
|
|
66
68
|
`# re-run \`fastagent deploy fly\` — the runbook follows fly.toml's app name.`,
|
|
67
69
|
`fly apps create ${appName}`,
|
|
68
|
-
`# The volume (
|
|
70
|
+
`# The volume (the agent's working directory, state, credentials — kept across stop/suspend/redeploy) is created by the deploy`,
|
|
69
71
|
`# below, in primary_region and sized by [mounts].initial_size. Creating it yourself first pins it to a host`,
|
|
70
72
|
`# picked without the machine, which can fail the deploy with "insufficient resources … existing volume".`,
|
|
71
73
|
``,
|
|
@@ -77,18 +79,30 @@ export function planFlyDeploy(input) {
|
|
|
77
79
|
`fly ips allocate-v6 --app ${appName}`,
|
|
78
80
|
];
|
|
79
81
|
if (secrets.length > 0) {
|
|
80
|
-
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(" ")}`);
|
|
82
|
+
runbook.push(``, `# Secrets the app needs a value for. \`fastagent secrets push fly\` sets the ones a value file holds and`, `# \`fastagent secrets set fly <NAME>\` one typed without echo; or with flyctl (replace each <value>):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`), `fly secrets set --app ${appName} ${secrets.map((s) => `${s.name}=<value>`).join(" ")}`);
|
|
81
83
|
}
|
|
82
84
|
runbook.push(``, `# The build context is the agent directory, baked as the definition.`, `# Before a new definition release, run \`fastagent deploy fly\` to refresh the release manifest.`, `# Run this from the agent directory:`, deployCmd);
|
|
83
|
-
runbook.push(``, `# The volume keeps .state and .secrets across restarts and deploys
|
|
85
|
+
runbook.push(``, `# The volume is the agent's working directory: it keeps its work, .state and .secrets across restarts and deploys.`, imageHasGit(input)
|
|
84
86
|
? `# Git is installed (the definition ships its .git, github content is cloned here, or context.json declares it in aptPackages).`
|
|
85
87
|
: `# To give the agent git, declare it: fastagent env add apt:git`);
|
|
86
88
|
// The credential is created on the box, never carried: the box is then the only holder of its grant.
|
|
87
89
|
if (input.boxLogin) {
|
|
88
90
|
runbook.push(``, `# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself (from the agent directory):`, `${deploymentLoginCommand("fly", input.boxLogin)}`);
|
|
89
91
|
}
|
|
90
|
-
// The
|
|
91
|
-
const
|
|
92
|
+
// The box points the channels its own credentials can at its `http.publicUrl`; only Slack's is a person's step.
|
|
93
|
+
const appUrl = `https://${appName}.fly.dev`;
|
|
94
|
+
const publicUrl = input.publicUrl ?? appUrl;
|
|
95
|
+
const own = webhookKinds(channels).filter((kind) => kind !== "slack");
|
|
96
|
+
const steps = [
|
|
97
|
+
...(own.length > 0
|
|
98
|
+
? [
|
|
99
|
+
`# The box points ${own.join(", ")} at ${publicUrl} itself, on every start; its /health says how each`,
|
|
100
|
+
`# one went (a step only a person can take, Lark's console where its config API answers 404, is in its log):`,
|
|
101
|
+
`curl -fsS ${appUrl}/health`,
|
|
102
|
+
]
|
|
103
|
+
: []),
|
|
104
|
+
...webhookRunbook(publicUrl, channels.filter((channel) => channel.name === "slack")),
|
|
105
|
+
];
|
|
92
106
|
const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
|
|
93
107
|
if (post.length > 0)
|
|
94
108
|
runbook.push(``, ...post);
|
|
@@ -107,9 +121,6 @@ export function parseFlyMinMachines(toml) {
|
|
|
107
121
|
}
|
|
108
122
|
/** Sanitize a directory basename into a Fly app name: lowercase, [a-z0-9-], must start with a letter. */
|
|
109
123
|
export function toFlyAppName(basename) {
|
|
110
|
-
const slug = basename
|
|
111
|
-
.toLowerCase()
|
|
112
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
113
|
-
.replace(/^-+|-+$/g, "");
|
|
124
|
+
const slug = stripEdges(basename.toLowerCase().replace(/[^a-z0-9]+/g, "-"), (char) => char === "-");
|
|
114
125
|
return /^[a-z]/.test(slug) ? slug : `app-${slug || "agent"}`;
|
|
115
126
|
}
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/** `fastagent deploy fly --run` — drive flyctl to completion. */
|
|
2
2
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
|
-
import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
3
|
+
import { type BoxWebhooksProbe, type UrlRecorder, type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
4
4
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
5
5
|
import { type CliRunner } from "../runner.ts";
|
|
6
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
6
7
|
export interface FlyRunPlan {
|
|
8
|
+
/** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
|
|
9
|
+
recordUrl?: UrlRecorder;
|
|
10
|
+
/** The `http.publicUrl` the image delivers, where the platforms reach the box; default the app's own URL. */
|
|
11
|
+
publicUrl?: string;
|
|
7
12
|
appName: string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Make sure the app holds a value for every name the deployment needs, through whoever can supply one
|
|
15
|
+
* (secret-store.ts): resolves a gate line, or undefined. It runs once the app exists, since only then can Fly say
|
|
16
|
+
* which names it has.
|
|
17
|
+
*/
|
|
18
|
+
provideSecrets: (store: SecretStore) => Promise<string | undefined>;
|
|
14
19
|
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
15
20
|
channels: readonly DeclaredChannel[];
|
|
16
21
|
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the agent directory). */
|
|
@@ -37,4 +42,9 @@ export type FlyRunOutcome = {
|
|
|
37
42
|
ok: false;
|
|
38
43
|
gate: string;
|
|
39
44
|
};
|
|
40
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void,
|
|
45
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void,
|
|
46
|
+
/** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
|
|
47
|
+
registrars: Pick<Registrars, "leave">, healthProbe?: PublicHealthProbe, boxWebhooks?: {
|
|
48
|
+
probe: BoxWebhooksProbe;
|
|
49
|
+
intervalMs?: number;
|
|
50
|
+
}): Promise<FlyRunOutcome>;
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { loginGate, publicHealthGate,
|
|
1
|
+
import { confirmWebhooks, loginGate, publicHealthGate, } from "../channel-ingress.js";
|
|
2
2
|
import { readOutput } from "../runner.js";
|
|
3
|
-
import { hostSignedOutGate
|
|
3
|
+
import { hostSignedOutGate } from "../secrets.js";
|
|
4
|
+
import { ensureFlyApp, flyStore } from "./store.js";
|
|
4
5
|
/**
|
|
5
6
|
* The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate command, because
|
|
6
7
|
* that is the granularity the answer is acted on at.
|
|
@@ -17,23 +18,10 @@ function ingressAddresses(stdout) {
|
|
|
17
18
|
const has = (types) => entries.some((entry) => typeof entry?.Address === "string" && entry.Address !== "" && types.includes(entry.Type));
|
|
18
19
|
return { v4: has(INGRESS_TYPES.v4), v6: has(INGRESS_TYPES.v6) };
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*/
|
|
24
|
-
function listHasName(stdout, name) {
|
|
25
|
-
const entries = JSON.parse(stdout);
|
|
26
|
-
if (!Array.isArray(entries))
|
|
27
|
-
throw new Error(`expected a JSON array, got ${typeof entries}`);
|
|
28
|
-
return entries.some((o) => o.Name === name);
|
|
29
|
-
}
|
|
30
|
-
export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
21
|
+
export async function deployFlyRun(plan, fly, log,
|
|
22
|
+
/** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
|
|
23
|
+
registrars, healthProbe, boxWebhooks) {
|
|
31
24
|
const gate = (g) => ({ ok: false, gate: g });
|
|
32
|
-
// 1. Gate missing required secret VALUES before any side effect (no half-created infra), and before asking a
|
|
33
|
-
// person to sign in for a deploy that would stop here anyway.
|
|
34
|
-
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
35
|
-
if (missingValues)
|
|
36
|
-
return gate(missingValues);
|
|
37
25
|
// 2.
|
|
38
26
|
if ((await fly(["auth", "whoami"], { capture: true })).code !== 0) {
|
|
39
27
|
const failed = plan.hostLogin
|
|
@@ -43,19 +31,14 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
43
31
|
return gate(failed);
|
|
44
32
|
}
|
|
45
33
|
// 3.
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return gate(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if ((await fly(["apps", "create", plan.appName])).code !== 0) {
|
|
55
|
-
return gate(`\`fly apps create ${plan.appName}\` failed — Fly app names are globally unique and it may be taken. ` +
|
|
56
|
-
`Set a unique \`app\` in fly.toml and re-run.`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
34
|
+
const appGate = await ensureFlyApp(fly, plan.appName, log);
|
|
35
|
+
if (appGate !== undefined)
|
|
36
|
+
return gate(appGate);
|
|
37
|
+
// 3b. Secrets, before anything else is set up for an app that cannot run without them: the names the app holds, the
|
|
38
|
+
// gaps filled (staged: the deploy below applies them).
|
|
39
|
+
const secretsGate = await plan.provideSecrets(flyStore(fly, plan.appName, log, { stage: true }));
|
|
40
|
+
if (secretsGate !== undefined)
|
|
41
|
+
return gate(secretsGate);
|
|
59
42
|
// 4. NO volume step: `fly deploy` creates it from [mounts] on a first deploy, and only it can tell the scheduler
|
|
60
43
|
// which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
|
|
61
44
|
// blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
|
|
@@ -78,15 +61,6 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
78
61
|
return gate(`\`fly ${allocate.join(" ")}\` failed — see the flyctl output above`);
|
|
79
62
|
}
|
|
80
63
|
}
|
|
81
|
-
// 6. Secrets — staged (no deploy yet; we deploy with fly.toml next). Values over stdin, not argv.
|
|
82
|
-
const keys = Object.keys(plan.secrets);
|
|
83
|
-
if (keys.length > 0) {
|
|
84
|
-
log(`setting ${keys.length} secret(s): ${keys.join(", ")}`);
|
|
85
|
-
const input = `${keys.map((k) => `${k}=${plan.secrets[k]}`).join("\n")}\n`;
|
|
86
|
-
if ((await fly(["secrets", "import", "--stage", "-a", plan.appName], { input })).code !== 0) {
|
|
87
|
-
return gate("`fly secrets import` failed — see the flyctl output above");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
64
|
// 7.
|
|
91
65
|
log("deploying (remote build)…");
|
|
92
66
|
const deployArgs = [
|
|
@@ -128,13 +102,21 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
128
102
|
afterLogin: "re-run `fastagent deploy fly --run` (it keeps the login)",
|
|
129
103
|
}));
|
|
130
104
|
}
|
|
131
|
-
// 9.
|
|
132
|
-
const registrationGateMsg = await
|
|
105
|
+
// 9. The box points its own channels at the `http.publicUrl` this deploy delivered; this reads what it made of them.
|
|
106
|
+
const registrationGateMsg = await confirmWebhooks({
|
|
133
107
|
baseUrl,
|
|
108
|
+
publicUrl: plan.publicUrl ?? baseUrl,
|
|
134
109
|
channels: plan.channels,
|
|
135
110
|
registrars,
|
|
136
111
|
log,
|
|
112
|
+
...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
|
|
137
113
|
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
114
|
+
...(boxWebhooks === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: {
|
|
117
|
+
probe: boxWebhooks.probe,
|
|
118
|
+
...(boxWebhooks.intervalMs === undefined ? {} : { intervalMs: boxWebhooks.intervalMs }),
|
|
119
|
+
}),
|
|
138
120
|
});
|
|
139
121
|
if (registrationGateMsg)
|
|
140
122
|
return gate(registrationGateMsg);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Fly's secrets as a deployment's store: `fly secrets list` for the names, `fly secrets set NAME=-` to set each. */
|
|
2
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
3
|
+
import { type CliRunner } from "../runner.ts";
|
|
4
|
+
/** Create the app when `fly apps list` does not show it. Resolves the gate line when it cannot be made. */
|
|
5
|
+
export declare function ensureFlyApp(fly: CliRunner, app: string, log: (message: string) => void): Promise<string | undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* `stage`: set the secrets without deploying them, for a deploy that follows (`fly deploy` applies staged ones); off, a
|
|
8
|
+
* running app restarts on them at once.
|
|
9
|
+
*/
|
|
10
|
+
export declare function flyStore(fly: CliRunner, app: string, log: (message: string) => void, options: {
|
|
11
|
+
stage: boolean;
|
|
12
|
+
}): SecretStore;
|