@fastagent-sh/fastagent 0.19.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/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +12 -11
- package/dist/channels/agentcore-service.js +14 -18
- package/dist/channels/agentcore.d.ts +11 -19
- package/dist/channels/agentcore.js +14 -7
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +239 -96
- package/dist/channels/discover.d.ts +26 -4
- package/dist/channels/discover.js +12 -23
- package/dist/channels/feishu/feishu-api.js +5 -8
- package/dist/channels/feishu/feishu.js +35 -18
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- 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/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +22 -54
- package/dist/channels/slack/slack.js +13 -13
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- 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 +2 -2
- package/dist/channels/telegram/telegram.js +57 -69
- 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 +4 -31
- package/dist/cli/commands/info.js +1 -2
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/start.js +31 -41
- package/dist/cli/serve.d.ts +12 -21
- package/dist/cli/serve.js +20 -25
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +51 -4
- 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 +26 -22
- 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 +3 -4
- package/dist/engines/pi/agent-session-factory.js +20 -39
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +14 -2
- package/dist/engines/pi/create.js +2 -2
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +3 -4
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +1 -2
- package/dist/engines/pi/open.js +1 -1
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +6 -33
- package/dist/engines/pi/session-control.d.ts +19 -6
- package/dist/engines/pi/session-control.js +565 -470
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- 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 +4 -4
- package/dist/engines/pi/session-settings.js +7 -4
- package/dist/engines/pi/session-store.d.ts +83 -13
- package/dist/engines/pi/session-store.js +408 -55
- package/dist/engines/pi/tool-context.d.ts +21 -8
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.js +2 -2
- package/dist/env.js +1 -2
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +27 -2
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +1 -2
- package/dist/service.js +27 -5
- package/dist/session-remote.d.ts +7 -2
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +226 -92
- 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 +1 -1
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";
|
|
17
|
+
import { resolveSecretsDir, resolveStateRoot, exists } from "../paths.js";
|
|
18
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 };
|
package/dist/deploy/runner.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* binary and the command sequence differ. Tests inject a fake recorder; production spawns the real CLI.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { log } from "../log.js";
|
|
7
8
|
/**
|
|
8
9
|
* Production {@link CliRunner}: spawn `bin` in `cwd` (the workspace, so a build/upload context is the
|
|
9
10
|
* agent). stderr is always inherited to the terminal; stdout is inherited unless `capture`. A spawn
|
|
@@ -22,11 +23,30 @@ export function spawnRunner(bin, cwd) {
|
|
|
22
23
|
});
|
|
23
24
|
let out = "";
|
|
24
25
|
let err = "";
|
|
26
|
+
let stdinError;
|
|
25
27
|
child.stdout?.on("data", (d) => (out += String(d)));
|
|
26
28
|
child.stderr?.on("data", (d) => (err += String(d)));
|
|
27
|
-
if (opts?.input)
|
|
29
|
+
if (opts?.input) {
|
|
30
|
+
// A host CLI that rejects before reading (bad auth, a refused command) closes stdin under us:
|
|
31
|
+
// the write then emits `error` on a stream with no listener, which Node turns into an uncaught
|
|
32
|
+
// exception — crashing the deploy and losing the gate message the exit code was about to carry.
|
|
33
|
+
// HELD, not dropped: the exit code says it only when the CLI exits non-zero (see below).
|
|
34
|
+
child.stdin?.on("error", (error) => (stdinError ??= error));
|
|
28
35
|
child.stdin?.end(opts.input);
|
|
29
|
-
|
|
36
|
+
}
|
|
37
|
+
child.on("close", (code) => {
|
|
38
|
+
// A CLI that exits 0 having refused part of its stdin took TRUNCATED input — an auth seed runs
|
|
39
|
+
// well past the 64KB pipe buffer, so "success" here would deploy a half-written secret and say
|
|
40
|
+
// nothing. The exit code is what callers read, so the failure has to reach them as one.
|
|
41
|
+
const truncated = stdinError !== undefined && (code ?? 1) === 0;
|
|
42
|
+
if (truncated) {
|
|
43
|
+
// `error`, not `warn`: this line is the ONLY evidence of the failure — the caller's gate says
|
|
44
|
+
// "see the output above", and above it the host CLI printed success. A log level that mutes
|
|
45
|
+
// it (CI often sets `error`) leaves a stopped deploy with no diagnosable cause.
|
|
46
|
+
log.error(`[fastagent] ${bin} exited 0 without reading all of its input — ${stdinError?.message}`);
|
|
47
|
+
}
|
|
48
|
+
res({ code: truncated ? 1 : (code ?? 1), stdout: out, stderr: opts?.captureStderr ? err : undefined });
|
|
49
|
+
});
|
|
30
50
|
child.on("error", () => res({ code: 127, stdout: "", stderr: opts?.captureStderr ? "" : undefined })); // ENOENT
|
|
31
51
|
});
|
|
32
52
|
}
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The secret set a deployed agent needs, computed from the definition — host-neutral. Required values
|
|
3
|
-
* gate every target; optional channel values travel only when configured. Only the SET command differs
|
|
4
|
-
* (`fly secrets import` vs `railway variables set`). The runbooks list both classes; `--run` reads local values.
|
|
5
|
-
*/
|
|
6
|
-
import { type ChannelKind } from "../scaffold/add-channel.ts";
|
|
1
|
+
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
7
2
|
/**
|
|
8
3
|
* Is this local auth source an env-var API key (→ becomes a deploy secret) vs OAuth / stored / none?
|
|
9
4
|
* Positive match on the UPPER_SNAKE env-var naming shape, NOT a negative exclude of today's sentinel
|
|
@@ -17,7 +12,7 @@ export declare function isEnvKey(source: string | undefined): source is string;
|
|
|
17
12
|
* deployment prerequisites. An OAuth/stored login has no env key here — it carries as
|
|
18
13
|
* `FASTAGENT_AUTH_SEED` on the `--run` path (see each host's run module), not as a named runbook secret.
|
|
19
14
|
*/
|
|
20
|
-
export declare function deploymentSecrets(modelAuth: string | undefined, channels:
|
|
15
|
+
export declare function deploymentSecrets(modelAuth: string | undefined, channels: readonly DeclaredChannel[], extraSecrets?: string[]): {
|
|
21
16
|
name: string;
|
|
22
17
|
hint: string;
|
|
23
18
|
required: boolean;
|
|
@@ -42,8 +37,7 @@ export declare function assembleSecrets(input: {
|
|
|
42
37
|
* no value to carry and no gate to raise — see {@link modelCredentialCarry}. */
|
|
43
38
|
modelKeyInDefinition?: boolean;
|
|
44
39
|
authFile: Buffer | undefined;
|
|
45
|
-
channels:
|
|
46
|
-
longConnectionChannels?: string[];
|
|
40
|
+
channels: readonly DeclaredChannel[];
|
|
47
41
|
/** Extra secret env-var names from `fastagent.config` deploy.secrets — carried like channel secrets. */
|
|
48
42
|
extraSecrets?: string[];
|
|
49
43
|
env: NodeJS.ProcessEnv;
|
|
@@ -52,3 +46,18 @@ export declare function assembleSecrets(input: {
|
|
|
52
46
|
missingSecrets: string[];
|
|
53
47
|
needsModelCredential: boolean;
|
|
54
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
51
|
+
* FASTAGENT_AUTH_SEED materialization (the read side of {@link assembleSecrets}'s carry). ABSENT-ONLY
|
|
52
|
+
* by design: a present file (a refreshed volume copy) is never overwritten by the stale seed, so a box
|
|
53
|
+
* that ran its own OAuth refresh is not rolled back.
|
|
54
|
+
*/
|
|
55
|
+
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
58
|
+
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
59
|
+
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
60
|
+
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
61
|
+
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
62
|
+
*/
|
|
63
|
+
export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
|