@fastagent-sh/fastagent 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -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 +3 -1
- package/dist/channels/feishu/feishu-api.js +4 -0
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.d.ts +2 -0
- package/dist/channels/feishu/register-app.js +4 -6
- 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/wait-health.js +1 -1
- 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 -310
- 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 -67
- 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 +63 -24
- 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.d.ts +12 -0
- package/dist/cli/commands/mcp.js +164 -0
- 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/program.js +339 -108
- 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.js +50 -6
- 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 +26 -2
- 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 -11
- 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 +80 -39
- 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 +18 -43
- package/dist/deploy/docker/run.js +57 -79
- 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 -22
- package/dist/deploy/fly/run.js +25 -44
- 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 -7
- 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.d.ts +3 -0
- package/dist/harnesses/pi-durable/mcp.js +32 -0
- package/dist/harnesses/pi-durable/open.d.ts +36 -38
- package/dist/harnesses/pi-durable/open.js +97 -150
- 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 +95 -15
- package/dist/harnesses/pi-durable/serve.js +151 -59
- 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/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +8 -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/locked-file.d.ts +2 -2
- package/dist/locked-file.js +8 -3
- package/dist/mcp/auth.d.ts +22 -0
- package/dist/mcp/auth.js +180 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +179 -0
- package/dist/mcp/config.d.ts +86 -0
- package/dist/mcp/config.js +225 -0
- package/dist/mcp/grants.d.ts +4 -0
- package/dist/mcp/grants.js +132 -0
- package/dist/mcp/host.d.ts +19 -0
- package/dist/mcp/host.js +128 -0
- package/dist/mcp/network.d.ts +12 -0
- package/dist/mcp/network.js +150 -0
- package/dist/mcp/sign-in.d.ts +2 -0
- package/dist/mcp/sign-in.js +204 -0
- package/dist/mcp-api.d.ts +107 -0
- package/dist/mcp-api.js +10 -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/setup-page.d.ts +51 -0
- package/dist/setup-page.js +149 -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 +12 -6
- package/dist/value-file.js +36 -25
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +8 -6
- package/dist/channels/lark/onboard.d.ts +0 -36
- package/dist/channels/lark/onboard.js +0 -56
- 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 -201
- package/dist/cli/add-feishu.d.ts +0 -50
- package/dist/cli/add-feishu.js +0 -367
- 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,28 +1,15 @@
|
|
|
1
|
-
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
2
1
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
|
-
import type
|
|
4
|
-
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
5
|
-
import type { CliRunner } from "../runner.ts";
|
|
2
|
+
import { type CliRunner } from "../runner.ts";
|
|
6
3
|
export interface DockerRunPlan {
|
|
7
4
|
/** Compose file relative to the runner cwd (the agent directory). */
|
|
8
5
|
composeFile: string;
|
|
9
6
|
/** Container port from config; used to ask Compose for the effective published host port. */
|
|
10
7
|
port: number;
|
|
11
8
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
11
|
*/
|
|
15
|
-
|
|
16
|
-
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
17
|
-
missingSecrets: string[];
|
|
18
|
-
/** That value file, agent-dir-relative, so the gate names the file this deploy actually read. */
|
|
19
|
-
valueFile: string;
|
|
20
|
-
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
|
|
21
|
-
announce: DockerAnnounce;
|
|
22
|
-
/** Every declared channel and its ingress: which of them a failed login leaves unregistered. */
|
|
23
|
-
channels: readonly DeclaredChannel[];
|
|
24
|
-
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
25
|
-
requireTunnel: boolean;
|
|
12
|
+
provideSecrets: () => Promise<string | undefined>;
|
|
26
13
|
/**
|
|
27
14
|
* Log the box in (`fastagent login --deployment`) once it is up and before any webhook is pointed at it: a channel pointed at
|
|
28
15
|
* a box with no model credential answers every message with a failure. Resolves a gate line, or undefined.
|
|
@@ -32,44 +19,32 @@ export interface DockerRunPlan {
|
|
|
32
19
|
export type DockerRunOutcome = {
|
|
33
20
|
ok: true;
|
|
34
21
|
url?: string;
|
|
35
|
-
tunnelUrl?: string;
|
|
36
22
|
}
|
|
37
|
-
/**
|
|
38
|
-
* `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know where it is and what
|
|
39
|
-
* to re-run.
|
|
40
|
-
*/
|
|
23
|
+
/** `url` travels with a gate too: Compose is up, so the operator still needs to know where. */
|
|
41
24
|
| {
|
|
42
25
|
ok: false;
|
|
43
26
|
gate: string;
|
|
44
27
|
url?: string;
|
|
45
|
-
tunnelUrl?: string;
|
|
46
28
|
};
|
|
47
|
-
/** Register the deployment's webhooks against its public URL, reporting what each registrar answered. */
|
|
48
|
-
type DockerAnnounce = (baseUrl: string) => Promise<{
|
|
49
|
-
kind: string;
|
|
50
|
-
outcome: RegistrationOutcome;
|
|
51
|
-
}[]>;
|
|
52
29
|
/**
|
|
53
30
|
* `stillStarting` answers whether the agent container is still up; a probe that ignores it simply waits out its whole
|
|
54
31
|
* budget.
|
|
55
32
|
*/
|
|
56
33
|
export type DockerHealthProbe = (healthUrl: string, stillStarting: () => Promise<boolean>) => Promise<boolean>;
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string) => Promise<ComposeTunnel | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* Whether something already accepts connections at `host:port`. The answer to "is the port the agent is about to
|
|
36
|
+
* publish free", asked before `up`, because afterwards a `/health` that answers cannot say whose it is.
|
|
37
|
+
*/
|
|
38
|
+
export type HostPortProbe = (host: string, port: number) => Promise<boolean>;
|
|
63
39
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
64
40
|
export declare function localUrlFromComposePort(stdout: string): string | undefined;
|
|
65
41
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
42
|
+
* The host ports the `agent` service publishes, from `docker compose config --format json` (Compose has already
|
|
43
|
+
* normalised every short or long syntax into `ports[]`). A port with no fixed host side (`published` unset: Docker picks
|
|
44
|
+
* one) cannot collide and is left out. A wildcard bind is asked on loopback, which is where the URL we report points.
|
|
68
45
|
*/
|
|
69
|
-
export declare function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
|
|
75
|
-
export {};
|
|
46
|
+
export declare function agentPublishedPorts(stdout: string): {
|
|
47
|
+
host: string;
|
|
48
|
+
port: number;
|
|
49
|
+
}[];
|
|
50
|
+
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, hostPortBusy?: HostPortProbe): Promise<DockerRunOutcome>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
2
|
+
import { connect } from "node:net";
|
|
1
3
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
2
|
-
import { loginGate } from "../channel-ingress.js";
|
|
3
|
-
import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
|
|
4
|
-
import { registrationGate } from "../registration-gate.js";
|
|
5
4
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
6
|
-
import {
|
|
5
|
+
import { readOutput } from "../runner.js";
|
|
7
6
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
8
7
|
export function localUrlFromComposePort(stdout) {
|
|
9
8
|
const line = stdout
|
|
@@ -18,30 +17,37 @@ export function localUrlFromComposePort(stdout) {
|
|
|
18
17
|
* so this budget covers a copy on a slow Docker Desktop disk, not a listen.
|
|
19
18
|
*/
|
|
20
19
|
const defaultHealthProbe = (healthUrl, stillStarting) => waitForHealth(healthUrl, 180_000, 500, stillStarting);
|
|
21
|
-
const
|
|
20
|
+
const defaultHostPortBusy = (host, port) => new Promise((resolve) => {
|
|
21
|
+
const socket = connect({ host, port });
|
|
22
|
+
const answer = (busy) => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve(busy);
|
|
25
|
+
};
|
|
26
|
+
socket.setTimeout(1_000, () => answer(false)); // a filtered port is not one something is serving
|
|
27
|
+
socket.once("connect", () => answer(true));
|
|
28
|
+
socket.once("error", () => answer(false));
|
|
29
|
+
});
|
|
22
30
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
31
|
+
* The host ports the `agent` service publishes, from `docker compose config --format json` (Compose has already
|
|
32
|
+
* normalised every short or long syntax into `ports[]`). A port with no fixed host side (`published` unset: Docker picks
|
|
33
|
+
* one) cannot collide and is left out. A wildcard bind is asked on loopback, which is where the URL we report points.
|
|
25
34
|
*/
|
|
26
|
-
export
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await (options.sleep ?? sleep)(options.intervalMs ?? 500);
|
|
41
|
-
}
|
|
42
|
-
return assigned ? { url: assigned, connected: false } : undefined;
|
|
35
|
+
export function agentPublishedPorts(stdout) {
|
|
36
|
+
const config = JSON.parse(stdout);
|
|
37
|
+
const ports = config?.services?.agent?.ports;
|
|
38
|
+
if (ports === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
if (!Array.isArray(ports))
|
|
41
|
+
throw new Error("services.agent.ports is not a list");
|
|
42
|
+
return ports.flatMap((entry) => {
|
|
43
|
+
const port = Number(entry?.published);
|
|
44
|
+
if (!Number.isInteger(port) || port <= 0 || (entry.protocol ?? "tcp") !== "tcp")
|
|
45
|
+
return [];
|
|
46
|
+
const wildcard = !entry.host_ip || entry.host_ip === "0.0.0.0" || entry.host_ip === "::";
|
|
47
|
+
return [{ host: wildcard ? "127.0.0.1" : entry.host_ip, port }];
|
|
48
|
+
});
|
|
43
49
|
}
|
|
44
|
-
const
|
|
50
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
51
|
/**
|
|
46
52
|
* `/health` asked from inside the agent container, for a topology that publishes no host port. The image always has
|
|
47
53
|
* node or bun (the generated Dockerfile's base), and both have `fetch`; nothing else is assumed installed.
|
|
@@ -59,7 +65,7 @@ async function containerHealthy(docker, compose, port, stillStarting) {
|
|
|
59
65
|
}
|
|
60
66
|
return false;
|
|
61
67
|
}
|
|
62
|
-
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe,
|
|
68
|
+
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, hostPortBusy = defaultHostPortBusy) {
|
|
63
69
|
const gate = (message) => ({ ok: false, gate: message });
|
|
64
70
|
const compose = ["compose", "-f", plan.composeFile];
|
|
65
71
|
// CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
|
|
@@ -70,17 +76,10 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
70
76
|
if (version.code !== 0) {
|
|
71
77
|
return gate("Docker Compose plugin is unavailable — install/enable `docker compose`, then re-run");
|
|
72
78
|
}
|
|
73
|
-
// The values
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
return gate(
|
|
77
|
-
// Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
|
|
78
|
-
// hands Compose nothing. Saying "passing N secrets to Compose" would be false, and doubly so on a hand-owned
|
|
79
|
-
// Compose file that has no `env_file` entry at all.
|
|
80
|
-
const secretNames = Object.keys(plan.secrets);
|
|
81
|
-
if (secretNames.length > 0) {
|
|
82
|
-
log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
|
|
83
|
-
}
|
|
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);
|
|
84
83
|
if ((await docker(["info"], { capture: true })).code !== 0) {
|
|
85
84
|
return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
|
|
86
85
|
}
|
|
@@ -94,16 +93,24 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
94
93
|
if (!services.includes("agent")) {
|
|
95
94
|
return gate(`Compose file must keep the "agent" service for \`fastagent deploy docker --run\``);
|
|
96
95
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
// A port something else already serves would answer the `/health` this run waits for, and the run would report that
|
|
97
|
+
// process's answer as the agent's. A container this project already runs holds its own port, so only a first start
|
|
98
|
+
// (or a start after `down`) asks. Before the first side effect: a refusal leaves nothing to undo.
|
|
99
|
+
const hostPorts = await readOutput(docker, "docker", [...compose, "config", "--format", "json"], agentPublishedPorts);
|
|
100
|
+
if ("gate" in hostPorts)
|
|
101
|
+
return gate(hostPorts.gate);
|
|
102
|
+
if (hostPorts.value.length > 0) {
|
|
103
|
+
const running = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true });
|
|
104
|
+
const ours = running.code === 0 && running.stdout.split(/\s+/).includes("agent");
|
|
105
|
+
if (!ours) {
|
|
106
|
+
for (const { host, port } of hostPorts.value) {
|
|
107
|
+
if (await hostPortBusy(host, port)) {
|
|
108
|
+
return gate(`${host}:${port} is already in use by another process, so the agent's published port is not free and a ` +
|
|
109
|
+
`health check there would answer for that process, not for the agent — stop it, or move the agent: ` +
|
|
110
|
+
`edit the published port in ${plan.composeFile}, or change defaults.http.port in fastagent.config.ts and re-run ` +
|
|
111
|
+
`with --force (a generated Compose file is kept otherwise)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
log(`building and reconciling ${plan.composeFile}…`);
|
|
@@ -120,9 +127,6 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
120
127
|
if (running.code !== 0 || !runningServices.includes("agent")) {
|
|
121
128
|
return gate(`the Compose service "agent" is not running — inspect with \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
122
129
|
}
|
|
123
|
-
if (hasTunnel && !runningServices.includes("tunnel")) {
|
|
124
|
-
return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
125
|
-
}
|
|
126
130
|
// A user-owned topology may deliberately remove the host port and expose only through its own ingress.
|
|
127
131
|
const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true });
|
|
128
132
|
const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
|
|
@@ -155,33 +159,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
155
159
|
}
|
|
156
160
|
// `url` travels with this gate: Compose is up, and the operator needs to know where.
|
|
157
161
|
const notLoggedIn = await plan.boxLogin?.run();
|
|
158
|
-
if (notLoggedIn)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const channels = hasTunnel ? plan.channels : [];
|
|
162
|
-
return { ok: false, gate: loginGate({ notLoggedIn, channels, log, afterLogin }), url };
|
|
163
|
-
}
|
|
164
|
-
if (!hasTunnel)
|
|
165
|
-
return { ok: true, url };
|
|
166
|
-
log("waiting for the Compose tunnel service to publish its Quick Tunnel URL…");
|
|
167
|
-
const tunnel = await tunnelUrlProbe(docker, plan.composeFile);
|
|
168
|
-
if (!tunnel) {
|
|
169
|
-
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
170
|
-
}
|
|
171
|
-
// The same sentence `startCloudflareTunnel` prints for the same state, and needed MORE here.
|
|
172
|
-
if (!tunnel.connected) {
|
|
173
|
-
log(`warn: the tunnel service never reported an edge connection for ${tunnel.url} — announcing it anyway. ` +
|
|
174
|
-
`Nothing reaches a tunnel that has not connected, so a webhook registration that cannot resolve the ` +
|
|
175
|
-
`host is this, not the platform. Inspect \`docker compose -f ${plan.composeFile} logs tunnel\`.`);
|
|
176
|
-
}
|
|
177
|
-
const tunnelUrl = tunnel.url;
|
|
178
|
-
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer that owns the
|
|
179
|
-
// outcome, so it is the layer that can gate on one.
|
|
180
|
-
const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
|
|
181
|
-
for (const { kind, outcome } of await plan.announce(tunnelUrl))
|
|
182
|
-
reg.track(kind, outcome);
|
|
183
|
-
const blocked = reg.gate();
|
|
184
|
-
if (blocked)
|
|
185
|
-
return { ok: false, gate: blocked, url, tunnelUrl };
|
|
186
|
-
return { ok: true, url, tunnelUrl };
|
|
162
|
+
if (notLoggedIn)
|
|
163
|
+
return { ok: false, gate: notLoggedIn, url };
|
|
164
|
+
return { ok: true, url };
|
|
187
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,18 +42,9 @@ export type FlyRunOutcome = {
|
|
|
37
42
|
ok: false;
|
|
38
43
|
gate: string;
|
|
39
44
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
v6: boolean;
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Whether `fly apps list --json` lists an app called `name`. flyctl capitalizes the field; the live probe
|
|
50
|
-
* (test/live/fly.live.test.ts) is what pins that against real output, since an offline fixture can only repeat what
|
|
51
|
-
* we already believe.
|
|
52
|
-
*/
|
|
53
|
-
export declare function listHasName(stdout: string, name: string): boolean;
|
|
54
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars, healthProbe?: PublicHealthProbe): Promise<FlyRunOutcome>;
|
|
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.
|
|
@@ -10,31 +11,17 @@ const INGRESS_TYPES = { v4: ["v4", "shared_v4"], v6: ["v6"] };
|
|
|
10
11
|
* Which PUBLIC ingress families `fly ips list --json` shows — asked per family, because "has an ingress address" is
|
|
11
12
|
* not the question the two allocate commands answer.
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
+
function ingressAddresses(stdout) {
|
|
14
15
|
const entries = JSON.parse(stdout);
|
|
15
16
|
if (!Array.isArray(entries))
|
|
16
17
|
throw new Error(`expected a JSON array, got ${typeof entries}`);
|
|
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
|
-
* we already believe.
|
|
24
|
-
*/
|
|
25
|
-
export function listHasName(stdout, name) {
|
|
26
|
-
const entries = JSON.parse(stdout);
|
|
27
|
-
if (!Array.isArray(entries))
|
|
28
|
-
throw new Error(`expected a JSON array, got ${typeof entries}`);
|
|
29
|
-
return entries.some((o) => o.Name === name);
|
|
30
|
-
}
|
|
31
|
-
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) {
|
|
32
24
|
const gate = (g) => ({ ok: false, gate: g });
|
|
33
|
-
// 1. Gate missing required secret VALUES before any side effect (no half-created infra), and before asking a
|
|
34
|
-
// person to sign in for a deploy that would stop here anyway.
|
|
35
|
-
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
36
|
-
if (missingValues)
|
|
37
|
-
return gate(missingValues);
|
|
38
25
|
// 2.
|
|
39
26
|
if ((await fly(["auth", "whoami"], { capture: true })).code !== 0) {
|
|
40
27
|
const failed = plan.hostLogin
|
|
@@ -44,19 +31,14 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
44
31
|
return gate(failed);
|
|
45
32
|
}
|
|
46
33
|
// 3.
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return gate(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if ((await fly(["apps", "create", plan.appName])).code !== 0) {
|
|
56
|
-
return gate(`\`fly apps create ${plan.appName}\` failed — Fly app names are globally unique and it may be taken. ` +
|
|
57
|
-
`Set a unique \`app\` in fly.toml and re-run.`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
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);
|
|
60
42
|
// 4. NO volume step: `fly deploy` creates it from [mounts] on a first deploy, and only it can tell the scheduler
|
|
61
43
|
// which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
|
|
62
44
|
// blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
|
|
@@ -79,15 +61,6 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
79
61
|
return gate(`\`fly ${allocate.join(" ")}\` failed — see the flyctl output above`);
|
|
80
62
|
}
|
|
81
63
|
}
|
|
82
|
-
// 6. Secrets — staged (no deploy yet; we deploy with fly.toml next). Values over stdin, not argv.
|
|
83
|
-
const keys = Object.keys(plan.secrets);
|
|
84
|
-
if (keys.length > 0) {
|
|
85
|
-
log(`setting ${keys.length} secret(s): ${keys.join(", ")}`);
|
|
86
|
-
const input = `${keys.map((k) => `${k}=${plan.secrets[k]}`).join("\n")}\n`;
|
|
87
|
-
if ((await fly(["secrets", "import", "--stage", "-a", plan.appName], { input })).code !== 0) {
|
|
88
|
-
return gate("`fly secrets import` failed — see the flyctl output above");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
64
|
// 7.
|
|
92
65
|
log("deploying (remote build)…");
|
|
93
66
|
const deployArgs = [
|
|
@@ -129,13 +102,21 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
129
102
|
afterLogin: "re-run `fastagent deploy fly --run` (it keeps the login)",
|
|
130
103
|
}));
|
|
131
104
|
}
|
|
132
|
-
// 9.
|
|
133
|
-
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({
|
|
134
107
|
baseUrl,
|
|
108
|
+
publicUrl: plan.publicUrl ?? baseUrl,
|
|
135
109
|
channels: plan.channels,
|
|
136
110
|
registrars,
|
|
137
111
|
log,
|
|
112
|
+
...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
|
|
138
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
|
+
}),
|
|
139
120
|
});
|
|
140
121
|
if (registrationGateMsg)
|
|
141
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;
|