@fastagent-sh/fastagent 0.18.0 → 0.20.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 +25 -7
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +23 -0
- package/dist/atomic-write.js +53 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +55 -0
- package/dist/channels/agentcore-service.js +107 -0
- package/dist/channels/agentcore.d.ts +22 -20
- package/dist/channels/agentcore.js +20 -11
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +55 -7
- package/dist/channels/control.js +312 -67
- package/dist/channels/discover.d.ts +59 -0
- package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.js +6 -9
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +41 -24
- package/dist/channels/feishu/invoke-turn.d.ts +1 -1
- package/dist/channels/feishu/invoke-turn.js +3 -4
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +1 -109
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/{state.js → kit/state.js} +4 -7
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
- package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
- package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
- package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +2 -2
- package/dist/channels/slack/invoke-turn.js +13 -6
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +23 -55
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +20 -20
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +1 -1
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +67 -13
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +3 -3
- package/dist/channels/telegram/telegram.js +60 -72
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +25 -40
- package/dist/cli/commands/fire.js +1 -1
- package/dist/cli/commands/info.js +7 -5
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +80 -121
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +22 -70
- package/dist/cli/serve.js +66 -257
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +53 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +11 -6
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +295 -0
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +5 -5
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +67 -42
- package/dist/engines/pi/create.js +142 -72
- package/dist/engines/pi/definition.d.ts +42 -8
- package/dist/engines/pi/definition.js +114 -11
- package/dist/engines/pi/invoke-session.d.ts +22 -19
- package/dist/engines/pi/invoke-session.js +286 -100
- package/dist/engines/pi/login.js +39 -20
- package/dist/engines/pi/models.d.ts +14 -0
- package/dist/engines/pi/models.js +9 -1
- package/dist/engines/pi/open.d.ts +10 -8
- package/dist/engines/pi/open.js +21 -11
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +27 -0
- package/dist/engines/pi/session-builder.js +87 -118
- package/dist/engines/pi/session-control.d.ts +28 -14
- package/dist/engines/pi/session-control.js +570 -438
- package/dist/engines/pi/session-inheritance.d.ts +30 -0
- package/dist/engines/pi/session-inheritance.js +261 -0
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +15 -3
- package/dist/engines/pi/session-settings.js +22 -1
- package/dist/engines/pi/session-store.d.ts +140 -0
- package/dist/engines/pi/session-store.js +666 -0
- package/dist/engines/pi/tool-context.d.ts +36 -13
- package/dist/engines/pi/tool-context.js +51 -4
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +3 -3
- package/dist/engines/pi/turn-kit.d.ts +32 -6
- package/dist/engines/pi/turn-kit.js +28 -1
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/env.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +37 -2
- package/dist/pi.d.ts +14 -8
- package/dist/pi.js +6 -5
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +2 -2
- package/dist/schedule/wakeups.js +11 -8
- package/dist/service.d.ts +157 -0
- package/dist/service.js +376 -0
- package/dist/session-remote.d.ts +8 -3
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +229 -93
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +14 -5
- package/dist/engines/pi/channel.d.ts +0 -37
- package/dist/engines/pi/harness.d.ts +0 -115
- package/dist/engines/pi/harness.js +0 -154
- package/dist/engines/pi/invoke.d.ts +0 -60
- package/dist/engines/pi/invoke.js +0 -459
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -60
- package/dist/engines/pi/sessions.js +0 -464
- package/dist/host/node.js +0 -52
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { webhookRunbook } from "../channel-ingress.js";
|
|
1
2
|
import { containerArtifacts } from "../container.js";
|
|
2
3
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
4
|
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
|
|
@@ -62,7 +63,7 @@ export function planFlyDeploy(input) {
|
|
|
62
63
|
const artifacts = [
|
|
63
64
|
{
|
|
64
65
|
path: flyTomlPath,
|
|
65
|
-
content: flyToml(appName, port, channels.
|
|
66
|
+
content: flyToml(appName, port, channels.some((channel) => channel.name === "github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, channels.some((channel) => channel.ingress === "long-connection")),
|
|
66
67
|
},
|
|
67
68
|
...containerArtifacts(input),
|
|
68
69
|
];
|
|
@@ -70,7 +71,7 @@ export function planFlyDeploy(input) {
|
|
|
70
71
|
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
71
72
|
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
72
73
|
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
73
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets
|
|
74
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
74
75
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
75
76
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
76
77
|
const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
|
|
@@ -110,26 +111,10 @@ export function planFlyDeploy(input) {
|
|
|
110
111
|
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
111
112
|
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
|
|
112
113
|
}
|
|
113
|
-
// The fastagent-only post-step: point each channel
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const post = [];
|
|
118
|
-
if (channels.includes("telegram")) {
|
|
119
|
-
post.push(`# After deploy — register the Telegram webhook. The path assumes the default route (POST /telegram);`, `# if you remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`, `curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`, ` -d url=https://${appName}.fly.dev/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`);
|
|
120
|
-
}
|
|
121
|
-
if (channels.includes("github")) {
|
|
122
|
-
post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
123
|
-
}
|
|
124
|
-
if (channels.includes("slack")) {
|
|
125
|
-
post.push(`# After deploy — set Slack Event Subscriptions → Request URL. Path assumes POST /slack;`, `# Slack verifies the running endpoint with a challenge:`, `# Request URL = https://${appName}.fly.dev/slack`, `# Ensure OAuth scopes + message.* subscriptions match the groupBehavior in channels/slack.ts.`);
|
|
126
|
-
}
|
|
127
|
-
for (const kind of ["feishu", "lark"]) {
|
|
128
|
-
if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
|
|
129
|
-
continue;
|
|
130
|
-
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
131
|
-
post.push(`# After deploy — set the ${label} event Request URL (developer console → Events & Callbacks).`, `# Path assumes the default route (POST /${kind}); the app must be RUNNING when you save (the console`, `# verifies the URL with a challenge):`, `# Request URL = https://${appName}.fly.dev/${kind}`);
|
|
132
|
-
}
|
|
114
|
+
// The fastagent-only post-step: point each channel at the live URL. WHICH channels and in what words
|
|
115
|
+
// is the shared channel-ingress kernel's answer; Fly's contribution is that its URL is deterministic.
|
|
116
|
+
const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
|
|
117
|
+
const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
|
|
133
118
|
if (post.length > 0)
|
|
134
119
|
runbook.push(``, ...post);
|
|
135
120
|
// Single-machine tier: state lives on ONE volume tied to ONE machine. Scaling to multiple machines
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -17,23 +17,9 @@
|
|
|
17
17
|
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
18
|
* never land in argv/process listings.
|
|
19
19
|
*/
|
|
20
|
-
import type
|
|
21
|
-
import type {
|
|
20
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
21
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
22
22
|
import type { CliRunner } from "../runner.ts";
|
|
23
|
-
/**
|
|
24
|
-
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
25
|
-
* FASTAGENT_AUTH_SEED materialization. ABSENT-ONLY by design: a present file (a refreshed volume copy)
|
|
26
|
-
* is never overwritten by the stale seed, so a box that ran its own OAuth refresh is not rolled back.
|
|
27
|
-
*/
|
|
28
|
-
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
31
|
-
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
32
|
-
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
33
|
-
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
34
|
-
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
35
|
-
*/
|
|
36
|
-
export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
|
|
37
23
|
export interface FlyRunPlan {
|
|
38
24
|
appName: string;
|
|
39
25
|
region: string;
|
|
@@ -42,8 +28,8 @@ export interface FlyRunPlan {
|
|
|
42
28
|
secrets: Record<string, string>;
|
|
43
29
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
44
30
|
missingSecrets: string[];
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
32
|
+
channels: readonly DeclaredChannel[];
|
|
47
33
|
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
|
|
48
34
|
flyConfig: string;
|
|
49
35
|
/** Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context —
|
|
@@ -58,9 +44,9 @@ export type FlyRunOutcome = {
|
|
|
58
44
|
gate: string;
|
|
59
45
|
};
|
|
60
46
|
/**
|
|
61
|
-
* Run the deploy through `fly`. `log` reports progress; the injected
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
47
|
+
* Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
|
|
48
|
+
* post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
|
|
49
|
+
* host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
50
|
+
* fail-visible.
|
|
65
51
|
*/
|
|
66
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void,
|
|
52
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<FlyRunOutcome>;
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
|
-
import { registrationGate } from "../registration-gate.js";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `fastagent deploy fly --run` — drive flyctl to completion. The middle of the deploy (app / volume /
|
|
3
|
+
* secrets / deploy) that the plain runbook hands to the operator; `--run` executes it instead, so a
|
|
4
|
+
* coding agent runs ONE command. Idempotent (app/volume check-then-act; channel secrets come from the
|
|
5
|
+
* local env — NOT minted — so a re-run sets the same values) and resumable: it STOPS at a human gate
|
|
6
|
+
* (not logged in, a missing secret value, a taken app name, a failed webhook registration) with one actionable line and a non-zero
|
|
7
|
+
* exit, so the agent clears the gate and re-runs. A `generate` channel secret absent from `.env` is a
|
|
8
|
+
* gate too (`missingSecrets`), not a silent mint — fill it in `.env` (use the random string that
|
|
9
|
+
* `add <channel>` prints).
|
|
10
|
+
*
|
|
11
|
+
* flyctl is behind the shared {@link CliRunner} seam — production spawns `fly`, tests inject a fake that
|
|
12
|
+
* records the command sequence and scripts outputs. That seam is the benchmark: the agent's journey
|
|
13
|
+
* encoded as an asserted command sequence + gate behavior, validated without a real Fly account.
|
|
14
|
+
*
|
|
15
|
+
* Non-interactive incantations the agent would otherwise get wrong: `--remote-only` (build on Fly's
|
|
16
|
+
* builders — no local Docker in a sandbox), `--yes` (no prompts), `--ha=false` + the mounted volume
|
|
17
|
+
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
|
+
* never land in argv/process listings.
|
|
6
19
|
*/
|
|
7
|
-
|
|
8
|
-
return !seed || fileExists ? undefined : Buffer.from(seed, "base64");
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
12
|
-
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
13
|
-
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
14
|
-
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
15
|
-
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
16
|
-
*/
|
|
17
|
-
export function collectAuthSeed(env) {
|
|
18
|
-
const first = env.FASTAGENT_AUTH_SEED;
|
|
19
|
-
if (!first)
|
|
20
|
-
return undefined;
|
|
21
|
-
let seed = first;
|
|
22
|
-
for (let i = 2;; i++) {
|
|
23
|
-
const part = env[`FASTAGENT_AUTH_SEED_${i}`];
|
|
24
|
-
if (!part)
|
|
25
|
-
break;
|
|
26
|
-
seed += part;
|
|
27
|
-
}
|
|
28
|
-
return seed;
|
|
29
|
-
}
|
|
20
|
+
import { registerWebhooks } from "../channel-ingress.js";
|
|
30
21
|
/** Whether a `fly … list --json` array contains an object named `name` (Fly capitalizes `Name`; accept both). */
|
|
31
22
|
function listHasName(stdout, name) {
|
|
32
23
|
try {
|
|
@@ -39,12 +30,12 @@ function listHasName(stdout, name) {
|
|
|
39
30
|
}
|
|
40
31
|
}
|
|
41
32
|
/**
|
|
42
|
-
* Run the deploy through `fly`. `log` reports progress; the injected
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
33
|
+
* Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
|
|
34
|
+
* post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
|
|
35
|
+
* host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
36
|
+
* fail-visible.
|
|
46
37
|
*/
|
|
47
|
-
export async function deployFlyRun(plan, fly, log,
|
|
38
|
+
export async function deployFlyRun(plan, fly, log, registrars) {
|
|
48
39
|
const gate = (g) => ({ ok: false, gate: g });
|
|
49
40
|
// 1. Auth is the one gate a coding agent can't clear itself (browser OAuth). `whoami` succeeds with
|
|
50
41
|
// either an interactive login or FLY_API_TOKEN, so one check covers both.
|
|
@@ -113,42 +104,15 @@ export async function deployFlyRun(plan, fly, log, registerTelegram, registerFei
|
|
|
113
104
|
if ((await fly(deployArgs)).code !== 0) {
|
|
114
105
|
return gate("`fly deploy` failed — see the flyctl output above; fix and re-run");
|
|
115
106
|
}
|
|
116
|
-
// 7. Post-deploy webhook —
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
log(`github: set the webhook in the repo (Settings → Webhooks) → https://${plan.appName}.fly.dev/webhook`);
|
|
126
|
-
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
127
|
-
}
|
|
128
|
-
if (plan.channels.includes("slack")) {
|
|
129
|
-
const baseUrl = `https://${plan.appName}.fly.dev`;
|
|
130
|
-
if (registerSlack) {
|
|
131
|
-
log("registering slack event URL…");
|
|
132
|
-
reg.track("slack", await registerSlack(baseUrl));
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
log(`slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`);
|
|
136
|
-
reg.track("slack", "manual");
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
for (const kind of ["feishu", "lark"]) {
|
|
140
|
-
if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
|
|
141
|
-
continue;
|
|
142
|
-
if (registerFeishu) {
|
|
143
|
-
log(`registering ${kind} event URL…`);
|
|
144
|
-
reg.track(kind, await registerFeishu(`https://${plan.appName}.fly.dev`, kind));
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → https://${plan.appName}.fly.dev/${kind} (the app must be running when you save)`);
|
|
148
|
-
reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const registrationGateMsg = reg.gate();
|
|
107
|
+
// 7. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
|
|
108
|
+
// kernel's; Fly contributes its deterministic URL and how to retry.
|
|
109
|
+
const registrationGateMsg = await registerWebhooks({
|
|
110
|
+
baseUrl: `https://${plan.appName}.fly.dev`,
|
|
111
|
+
channels: plan.channels,
|
|
112
|
+
registrars,
|
|
113
|
+
log,
|
|
114
|
+
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
115
|
+
});
|
|
152
116
|
if (registrationGateMsg)
|
|
153
117
|
return gate(registrationGateMsg);
|
|
154
118
|
return { ok: true };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FastagentConfig } from "../engines/pi/config.ts";
|
|
2
2
|
import { type ResolvedPlacement } from "../paths.ts";
|
|
3
|
-
import { type
|
|
3
|
+
import { type DeclaredChannel } from "../channels/discover.ts";
|
|
4
4
|
import { type ContainerInput } from "./container.ts";
|
|
5
5
|
/** A stderr line the CLI prints (`[fastagent] warn: …` / `[fastagent] note: …`). Host-neutral advisories. */
|
|
6
6
|
interface DeployMessage {
|
|
@@ -10,11 +10,10 @@ interface DeployMessage {
|
|
|
10
10
|
/** The resolved facts every host plan needs (the container shape, channels, model auth, ports/secrets). */
|
|
11
11
|
interface DeployFacts {
|
|
12
12
|
messages: DeployMessage[];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
longConnectionChannels: string[];
|
|
13
|
+
/** Every declared channel with the ingress its module shape says it has, custom ones included. The
|
|
14
|
+
* ONE channel fact the plans and drivers read: a consumer that needs a subset derives it (see
|
|
15
|
+
* {@link webhookKinds}) rather than receiving a list someone else already filtered. */
|
|
16
|
+
channels: DeclaredChannel[];
|
|
18
17
|
/** Whether the agent has TIME triggers — `schedules/` files or `selfSchedule` (the wake tool). Cron/wake
|
|
19
18
|
* has no external wake-up, so the deployment must keep one machine running: the fly plan forces
|
|
20
19
|
* `min_machines_running=1`, the railway runbook forbids App Sleeping. */
|
package/dist/deploy/preflight.js
CHANGED
|
@@ -14,15 +14,15 @@ import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
|
14
14
|
import ignore from "ignore";
|
|
15
15
|
import { classifyBind } from "../bind.js";
|
|
16
16
|
import { resolveAuthPath } from "../engines/pi/config.js";
|
|
17
|
-
import { resolveSecretsDir, resolveStateRoot } from "../paths.js";
|
|
18
|
-
import { inspectChannels } from "../
|
|
17
|
+
import { resolveSecretsDir, resolveStateRoot, exists } from "../paths.js";
|
|
18
|
+
import { inspectChannels } from "../channels/discover.js";
|
|
19
19
|
import { discoverScheduleFiles } from "../schedule/discover.js";
|
|
20
20
|
import { createPiModelRuntime, modelCredentialCarry, probeAuthSource } from "../engines/pi/models.js";
|
|
21
21
|
import { CHANNEL_KINDS } from "../scaffold/add-channel.js";
|
|
22
|
-
import { exists } from "../paths.js";
|
|
23
22
|
import { detectRuntime, readPackageJson } from "../runtime.js";
|
|
24
23
|
import { fastagentVersion } from "../version.js";
|
|
25
24
|
import { isGeneratedDockerfile, isGeneratedDockerignore } from "./container.js";
|
|
25
|
+
import { CONTROL_TOKEN_ENV } from "../channels/control.js";
|
|
26
26
|
import { isEnvKey } from "./secrets.js";
|
|
27
27
|
/**
|
|
28
28
|
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore`
|
|
@@ -74,17 +74,19 @@ export async function preflightDeploy(input) {
|
|
|
74
74
|
messages.push({ level: "warn", text: modelIssue });
|
|
75
75
|
}
|
|
76
76
|
// The control plane on a deployed box: `start` honors `sessionControl: true`, so `/control/*`
|
|
77
|
-
// (steer
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// (steer, stop, rewrite or delete a session) rides the PUBLIC host URL, protected only by the bearer token. The token
|
|
78
|
+
// travels as a deploy secret (see extraSecrets below) so the caller has it; the reach still warrants
|
|
79
|
+
// a warning — the tunnel path warns loudly and deploy must not be the silent second way to break the
|
|
80
|
+
// loopback trust story.
|
|
81
81
|
if (config.sessionControl === true) {
|
|
82
82
|
messages.push({
|
|
83
83
|
level: "warn",
|
|
84
|
-
text: `sessionControl: true — the deployed box serves /control/* (steer
|
|
85
|
-
`protected only by a
|
|
86
|
-
|
|
87
|
-
`
|
|
84
|
+
text: `sessionControl: true — the deployed box serves /control/* (steer, stop, rewrite or delete a session) at its public URL, ` +
|
|
85
|
+
`protected only by a bearer token. Set ${CONTROL_TOKEN_ENV} (listed with the other secrets) and give the ` +
|
|
86
|
+
`same value to callers: attach --url <public-url> --token …. Unset, the box mints its own per boot — ` +
|
|
87
|
+
`readable only by shelling in (\`docker compose exec\`/\`fly ssh console\`: <stateRoot>/control.json, whose ` +
|
|
88
|
+
`url field is container-loopback) and replaced on every restart. Front the endpoint with real auth for ` +
|
|
89
|
+
`anything wider (design §14)`,
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
// Known channel kinds only — a custom channel's secrets/webhook are unknown to us; note and let the
|
|
@@ -93,20 +95,18 @@ export async function preflightDeploy(input) {
|
|
|
93
95
|
if (inspected.failures.length > 0) {
|
|
94
96
|
throw new Error(`cannot inspect channel modules: ${inspected.failures.map((failure) => `${failure.label}: ${failure.message}`).join("; ")}`);
|
|
95
97
|
}
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const longConnectionChannels = inspected.longConnectionChannels;
|
|
100
|
-
for (const c of discovered) {
|
|
101
|
-
if (channels.includes(c))
|
|
98
|
+
const channels = inspected.channels;
|
|
99
|
+
for (const { name, ingress } of channels) {
|
|
100
|
+
if (CHANNEL_KINDS.includes(name))
|
|
102
101
|
continue;
|
|
103
102
|
messages.push({
|
|
104
103
|
level: "note",
|
|
105
|
-
text:
|
|
106
|
-
? `long-connection channel "${
|
|
107
|
-
: `route channel "${
|
|
104
|
+
text: ingress === "long-connection"
|
|
105
|
+
? `long-connection channel "${name}" is custom — configure its secrets yourself; generated deploy plans keep the process running and skip webhook registration`
|
|
106
|
+
: `route channel "${name}" is custom — configure its secrets and webhook yourself`,
|
|
108
107
|
});
|
|
109
108
|
}
|
|
109
|
+
const longConnectionChannels = channels.filter((c) => c.ingress === "long-connection").map((c) => c.name);
|
|
110
110
|
// Time triggers (static schedules or self-scheduling) need a machine kept running — unlike a webhook,
|
|
111
111
|
// nothing external wakes a scale-to-zero box for a cron instant or a wake-up. The note is CONDITIONAL
|
|
112
112
|
// ("the generated plan…"): in KEEP mode an existing fly.toml is not rewritten — the CLI warns separately
|
|
@@ -405,7 +405,13 @@ export async function preflightDeploy(input) {
|
|
|
405
405
|
}
|
|
406
406
|
// What the agent declared it needs on the box (fastagent.config deploy.secrets) — carried like channel
|
|
407
407
|
// secrets: listed in the runbook, set from the local env under --run, gated if a value is missing.
|
|
408
|
-
const extraSecrets = config.deploy?.secrets ?? [];
|
|
408
|
+
const extraSecrets = [...(config.deploy?.secrets ?? [])];
|
|
409
|
+
// The plane's bearer token is the DEPLOYMENT's secret, not the container's: minted inside the box it
|
|
410
|
+
// is unreadable from outside and replaced on every restart, which is what makes a public /control/*
|
|
411
|
+
// unusable. Carried like any declared secret — listed in the runbook, taken from the local env under
|
|
412
|
+
// --run, gated when absent (never minted: a value minted per deploy rotates under its holder).
|
|
413
|
+
if (config.sessionControl === true)
|
|
414
|
+
extraSecrets.push(CONTROL_TOKEN_ENV);
|
|
409
415
|
// deploy.apt only shapes the GENERATED Dockerfile. Warn ONLY when the kept Dockerfile is HAND-WRITTEN
|
|
410
416
|
// (its apt won't include these) — a fastagent-generated one is handled by writeArtifacts. Don't suggest
|
|
411
417
|
// --force here: it would overwrite the user's hand-written file.
|
|
@@ -423,8 +429,6 @@ export async function preflightDeploy(input) {
|
|
|
423
429
|
ok: true,
|
|
424
430
|
messages,
|
|
425
431
|
channels,
|
|
426
|
-
routeChannels,
|
|
427
|
-
longConnectionChannels,
|
|
428
432
|
hasTimeTriggers,
|
|
429
433
|
modelAuth,
|
|
430
434
|
modelKeyInDefinition,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* the required-secret list. `railway.json`'s `healthcheckPath=/health` also fixes the "routed before the
|
|
22
22
|
* server is listening" boot race Fly's deploy hit — Railway only routes once /health passes.
|
|
23
23
|
*/
|
|
24
|
-
import type {
|
|
24
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
25
25
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
26
26
|
export interface RailwayPlanInput extends ContainerInput {
|
|
27
27
|
/** The service name to create (`railway add --service`). Railway service names are project-scoped, not
|
|
@@ -29,10 +29,9 @@ export interface RailwayPlanInput extends ContainerInput {
|
|
|
29
29
|
serviceName: string;
|
|
30
30
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
31
31
|
modelAuth: string | undefined;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
longConnectionChannels?: string[];
|
|
32
|
+
/** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
|
|
33
|
+
* whether App Sleeping must stay off for an outbound connection. */
|
|
34
|
+
channels: readonly DeclaredChannel[];
|
|
36
35
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
37
36
|
extraSecrets?: string[];
|
|
38
37
|
/** Time triggers present (schedules/ or selfSchedule) — the runbook forbids App Sleeping: cron/wake has
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { webhookRunbook } from "../channel-ingress.js";
|
|
1
2
|
import { containerArtifacts } from "../container.js";
|
|
2
3
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
4
|
/** State root = the volume mount path, kept in lockstep. `/data` matches the Fly recipe. */
|
|
@@ -7,8 +8,6 @@ const MOUNT = "/data";
|
|
|
7
8
|
* config file's `dockerfilePath` spells it WITHOUT the slash (the config-as-code schema's own
|
|
8
9
|
* convention); two mechanisms, two documented spellings, one fact each. */
|
|
9
10
|
export const dockerfilePathVar = (prefix) => `/${prefix}Dockerfile`;
|
|
10
|
-
/** railway.json — build/deploy only (Railway's config-as-code scope). No env/volume/sleeping here: those
|
|
11
|
-
* are service settings the runbook applies via CLI. healthcheckPath gates routing on a live server. */
|
|
12
11
|
/** railway.json is JSON, so its ownership marker is a KEY rather than a comment line. Railway ignores
|
|
13
12
|
* unknown keys; the predicate below is what lets `--force` reset OUR file and keep a hand-written one. */
|
|
14
13
|
const GENERATED_RAILWAY_KEY = "x-generated-by";
|
|
@@ -22,6 +21,8 @@ export function isGeneratedRailwayJson(content) {
|
|
|
22
21
|
return false;
|
|
23
22
|
}
|
|
24
23
|
}
|
|
24
|
+
/** railway.json — build/deploy only (Railway's config-as-code scope). No env/volume/sleeping here: those
|
|
25
|
+
* are service settings the runbook applies via CLI. healthcheckPath gates routing on a live server. */
|
|
25
26
|
function railwayJson(prefix) {
|
|
26
27
|
return `${JSON.stringify({
|
|
27
28
|
$schema: "https://railway.com/railway.schema.json",
|
|
@@ -45,7 +46,7 @@ export function planRailwayDeploy(input) {
|
|
|
45
46
|
{ path: configPath, content: railwayJson(input.agentPrefix) },
|
|
46
47
|
...containerArtifacts(input),
|
|
47
48
|
];
|
|
48
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets
|
|
49
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
49
50
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
50
51
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
51
52
|
// Order matters, not cosmetics: `railway init` creates a PROJECT with no service, but the volume and
|
|
@@ -100,36 +101,20 @@ export function planRailwayDeploy(input) {
|
|
|
100
101
|
}
|
|
101
102
|
// The public URL is minted, not deterministic (unlike Fly's <app>.fly.dev) — ONE mint step, then each
|
|
102
103
|
// channel's webhook uses that domain (mint once even when both channels are present).
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
runbook.push(``, `# Public URL — Railway mints a *.up.railway.app domain (NOT deterministic). Generate it, then read`, `# the printed https URL and use it as <your-domain> in the webhook step(s) below:`, `railway domain`);
|
|
109
|
-
}
|
|
110
|
-
if (channels.includes("telegram")) {
|
|
111
|
-
runbook.push(`# Register the Telegram webhook (default route POST /telegram; if you remapped it in`, `# channels/telegram.ts, use your path). secret_token MUST equal TELEGRAM_SECRET_TOKEN:`, `curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`, ` -d url=https://<your-domain>/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`);
|
|
112
|
-
}
|
|
113
|
-
if (channels.includes("github")) {
|
|
114
|
-
runbook.push(`# Set the GitHub webhook (repo Settings → Webhooks). Default route POST /webhook; if you remapped it`, `# in channels/github.ts, use your path:`, `# Payload URL = https://<your-domain>/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
115
|
-
}
|
|
116
|
-
if (channels.includes("slack")) {
|
|
117
|
-
runbook.push(`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`, `# answers Slack's challenge), and match scopes/subscriptions to channels/slack.ts groupBehavior:`, `# Request URL = https://<your-domain>/slack`);
|
|
118
|
-
}
|
|
119
|
-
for (const kind of ["feishu", "lark"]) {
|
|
120
|
-
if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
|
|
121
|
-
continue;
|
|
122
|
-
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
123
|
-
runbook.push(`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`, `# POST /${kind}; the service must be RUNNING when you save (the console verifies with a challenge):`, `# Request URL = https://<your-domain>/${kind}`);
|
|
104
|
+
// Mint only when something below needs it: with nothing to point at the domain (no channels, or only
|
|
105
|
+
// long-connection ones), "use it in the step(s) below" would refer to steps that do not follow.
|
|
106
|
+
const steps = webhookRunbook(`https://<your-domain>`, channels);
|
|
107
|
+
if (steps.length > 0) {
|
|
108
|
+
runbook.push(``, `# Public URL — Railway mints a *.up.railway.app domain (NOT deterministic). Generate it, then read`, `# the printed https URL and use it as <your-domain> in the webhook step(s) below:`, `railway domain`, ...steps);
|
|
124
109
|
}
|
|
125
110
|
// Scale-to-zero: App Sleeping is dashboard-only (no CLI/API) — a manual step, not a generated setting.
|
|
126
111
|
// A github channel should NOT enable it: fire-and-forget reviews have no replay (unlike Telegram's L1
|
|
127
112
|
// turn store), so a sleep mid-review would drop it — the same floor the Fly plan enforces via config.
|
|
128
|
-
runbook.push(``, channels.
|
|
113
|
+
runbook.push(``, channels.some((channel) => channel.name === "github")
|
|
129
114
|
? `# Scale-to-zero: do NOT enable App Sleeping — github turns have no replay, a sleep mid-review is lost.`
|
|
130
115
|
: input.hasTimeTriggers
|
|
131
116
|
? `# Scale-to-zero: do NOT enable App Sleeping — schedules/wake-ups have no external wake-up; a sleeping service sleeps through them.`
|
|
132
|
-
: (
|
|
117
|
+
: channels.some((channel) => channel.ingress === "long-connection")
|
|
133
118
|
? `# Scale-to-zero: do NOT enable App Sleeping — a long-connection channel must remain connected.`
|
|
134
119
|
: `# Scale-to-zero (optional, dashboard-only — no CLI/API): Settings → Deploy → Serverless → App Sleeping.`, `# Keep this a SINGLE service: the ${MOUNT} volume is tied to one service; extra replicas split state.`);
|
|
135
120
|
return { artifacts, runbook };
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
|
|
24
24
|
* `RAILWAY_API_KEY`), not a project token: `init` creates a project that a project token can't predate.
|
|
25
25
|
*/
|
|
26
|
-
import type
|
|
27
|
-
import type {
|
|
26
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
27
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
28
28
|
import type { CliRunner } from "../runner.ts";
|
|
29
29
|
export interface RailwayRunPlan {
|
|
30
30
|
/** Names both the project (`railway init --name`) and the service (`railway add --service`). Railway
|
|
@@ -38,8 +38,8 @@ export interface RailwayRunPlan {
|
|
|
38
38
|
secrets: Record<string, string>;
|
|
39
39
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
40
40
|
missingSecrets: string[];
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
42
|
+
channels: readonly DeclaredChannel[];
|
|
43
43
|
/** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. Off
|
|
44
44
|
* by default so `--run` only creates on an unlinked dir and never deploys into a pre-existing (possibly
|
|
45
45
|
* unrelated/production) project; the flag is the operator's explicit "yes, this project". */
|
|
@@ -84,9 +84,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
|
84
84
|
*/
|
|
85
85
|
export declare function parseHasVolume(stdout: string, mountPath: string): boolean;
|
|
86
86
|
/**
|
|
87
|
-
* Run the deploy through `railway`. `log` reports progress; the injected
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
|
|
88
|
+
* the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
|
|
89
|
+
* the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
90
|
+
* fail-visible.
|
|
91
91
|
*/
|
|
92
|
-
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void,
|
|
92
|
+
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<RailwayRunOutcome>;
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `fastagent deploy railway --run` — drive the Railway CLI to completion. The middle of the deploy the
|
|
3
|
+
* plain runbook hands to the operator; `--run` executes it so a coding agent runs ONE command.
|
|
4
|
+
*
|
|
5
|
+
* Railway's model forces differences from the Fly runner (fly/run.ts), all validated against CLI 5.15.0:
|
|
6
|
+
*
|
|
7
|
+
* - **`--run` PROVISIONS a project and only runs on an UNLINKED directory** — so it can never deploy
|
|
8
|
+
* into a project it didn't create. `railway init` isn't check-then-act (it ALWAYS makes a new project)
|
|
9
|
+
* and `railway status` exits 0 even when unlinked (linkedness is read from stdout: JSON when linked,
|
|
10
|
+
* empty when not). We do NOT track ownership: a pre-existing link is refused (Railway has no globally
|
|
11
|
+
* unique name to give free identity like Fly's app name, and synthesizing one — a machine-local marker
|
|
12
|
+
* — was a large, bug-prone premature optimization). The only "yes, this project" signal is the operator's
|
|
13
|
+
* explicit `--into-linked`, which provisions INTO the linked project; a routine redeploy is just
|
|
14
|
+
* `railway up`. The volume is check-then-act, so `--into-linked` (or a create that failed at volume-add) still gets it.
|
|
15
|
+
* - **Every command needs `--service` explicitly** to stay non-interactive (a bare command prompts to
|
|
16
|
+
* pick a service). `railway volume add` is the exception — it has NO `--service` flag and rides the
|
|
17
|
+
* linked service, so the service must be created (and thus linked) first.
|
|
18
|
+
* - **The public URL is minted, not deterministic** — bare `railway domain --json` returns the service
|
|
19
|
+
* domain (minting one if absent), read for the webhook. We avoid the newer `domain list` subcommand:
|
|
20
|
+
* on a CLI without it, `railway domain list` registers a bogus custom domain named "list" (destructive).
|
|
21
|
+
*
|
|
22
|
+
* Secrets go in one-per-`variable set --stdin` (value on stdin, never argv/process listing — Railway has
|
|
23
|
+
* no bulk stdin import like Fly's `secrets import`). Auth needs an ACCOUNT credential (login or
|
|
24
|
+
* `RAILWAY_API_KEY`), not a project token: `init` creates a project that a project token can't predate.
|
|
25
|
+
*/
|
|
26
|
+
import { registerWebhooks } from "../channel-ingress.js";
|
|
2
27
|
/**
|
|
3
28
|
* Whether `railway status --json` shows a linked project: non-empty stdout. Unlinked prints its message
|
|
4
29
|
* to stderr and leaves stdout EMPTY (the exit code is 0 either way, so it can't be the signal). ANY
|
|
@@ -62,12 +87,12 @@ export function parseHasVolume(stdout, mountPath) {
|
|
|
62
87
|
return jsonStrings(stdout).includes(mountPath);
|
|
63
88
|
}
|
|
64
89
|
/**
|
|
65
|
-
* Run the deploy through `railway`. `log` reports progress; the injected
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
90
|
+
* Run the deploy through `railway`. `log` reports progress; the injected {@link Registrars} perform
|
|
91
|
+
* the post-deploy webhook steps from the builder machine (Slack's control credential never travels to
|
|
92
|
+
* the host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
93
|
+
* fail-visible.
|
|
69
94
|
*/
|
|
70
|
-
export async function deployRailwayRun(plan, railway, log,
|
|
95
|
+
export async function deployRailwayRun(plan, railway, log, registrars) {
|
|
71
96
|
const gate = (g) => ({ ok: false, gate: g });
|
|
72
97
|
// Every --service below targets plan.name — the name this tool gives BOTH the project and the service
|
|
73
98
|
// (`init --name` + `add --service`). On a fresh create they match; on `--into-linked` into a hand-made
|
|
@@ -173,40 +198,15 @@ export async function deployRailwayRun(plan, railway, log, registerTelegram, reg
|
|
|
173
198
|
if (!url) {
|
|
174
199
|
return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
|
|
175
200
|
}
|
|
176
|
-
// 7. Post-deploy webhook —
|
|
177
|
-
//
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
reg.track("github", "manual"); // always a human step — re-surface it after the registrar output
|
|
186
|
-
}
|
|
187
|
-
if (plan.channels.includes("slack")) {
|
|
188
|
-
if (registerSlack) {
|
|
189
|
-
log("registering slack event URL…");
|
|
190
|
-
reg.track("slack", await registerSlack(url));
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
|
|
194
|
-
reg.track("slack", "manual");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
for (const kind of ["feishu", "lark"]) {
|
|
198
|
-
if (!plan.channels.includes(kind) || plan.longConnectionChannels?.includes(kind))
|
|
199
|
-
continue;
|
|
200
|
-
if (registerFeishu) {
|
|
201
|
-
log(`registering ${kind} event URL…`);
|
|
202
|
-
reg.track(kind, await registerFeishu(url, kind));
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
log(`${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${url}/${kind} (the service must be running when you save)`);
|
|
206
|
-
reg.track(kind, "manual"); // no registrar wired — the console step above is the operator's
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const registrationGateMsg = reg.gate();
|
|
201
|
+
// 7. Post-deploy webhook — which channels, in what words, and the gate policy are all the shared
|
|
202
|
+
// kernel's; Railway contributes the minted URL and how to retry.
|
|
203
|
+
const registrationGateMsg = await registerWebhooks({
|
|
204
|
+
baseUrl: url,
|
|
205
|
+
channels: plan.channels,
|
|
206
|
+
registrars,
|
|
207
|
+
log,
|
|
208
|
+
retryHint: "re-run with --into-linked to retry registration",
|
|
209
|
+
});
|
|
210
210
|
if (registrationGateMsg)
|
|
211
211
|
return gate(registrationGateMsg);
|
|
212
212
|
return { ok: true, url };
|