@fastagent-sh/fastagent 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.js +50 -6
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** `fastagent deploy railway --run` — drive the Railway CLI to completion. */
|
|
2
2
|
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
|
-
import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
3
|
+
import { type BoxWebhooksProbe, type UrlRecorder, type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
4
4
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
5
5
|
import { type CliRunner } from "../runner.ts";
|
|
6
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
6
7
|
export interface RailwayRunPlan {
|
|
8
|
+
/** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
|
|
9
|
+
recordUrl?: UrlRecorder;
|
|
7
10
|
/** Names both the project (`railway init --name`) and the service (`railway add --service`). */
|
|
8
11
|
name: string;
|
|
9
12
|
/**
|
|
@@ -11,12 +14,11 @@ export interface RailwayRunPlan {
|
|
|
11
14
|
* in lockstep).
|
|
12
15
|
*/
|
|
13
16
|
mountPath: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
valueFile: string;
|
|
17
|
+
/**
|
|
18
|
+
* Make sure the service holds a value for every name the deployment needs, through whoever can supply one
|
|
19
|
+
* (secret-store.ts): resolves a gate line, or undefined. It runs once the service exists.
|
|
20
|
+
*/
|
|
21
|
+
provideSecrets: (store: SecretStore) => Promise<string | undefined>;
|
|
20
22
|
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
21
23
|
channels: readonly DeclaredChannel[];
|
|
22
24
|
/** Opt-in (CLI `--into-linked`) to provision INTO the project this directory is already linked to. */
|
|
@@ -33,6 +35,12 @@ export interface RailwayRunPlan {
|
|
|
33
35
|
hostLogin?: () => Promise<string | undefined>;
|
|
34
36
|
/** The command a coding agent runs to deploy this, its person's steps handed to its user (`--run --json`). */
|
|
35
37
|
forAgent: string;
|
|
38
|
+
/**
|
|
39
|
+
* Put the service's domain into the configuration the image delivers (`http.publicUrl`), before `railway up` uploads
|
|
40
|
+
* it, so the box points its own channels there. Resolves the URL the configuration then names, the operator's own
|
|
41
|
+
* when it had one.
|
|
42
|
+
*/
|
|
43
|
+
deliverPublicUrl: (url: string) => Promise<string>;
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
46
|
* Done (with the live URL), or a gate the operator must clear before re-running (printed + non-zero exit by the CLI).
|
|
@@ -44,13 +52,6 @@ export type RailwayRunOutcome = {
|
|
|
44
52
|
ok: false;
|
|
45
53
|
gate: string;
|
|
46
54
|
};
|
|
47
|
-
/** Whether `railway status --json` shows a linked project: non-empty stdout. */
|
|
48
|
-
export declare function isLinked(stdout: string): boolean;
|
|
49
|
-
/**
|
|
50
|
-
* The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
|
|
51
|
-
* --json` puts `name` at the top level on 5.15.0).
|
|
52
|
-
*/
|
|
53
|
-
export declare function linkedName(stdout: string): string | undefined;
|
|
54
55
|
/** The first Railway-provided domain as an https URL, or undefined if none is present. */
|
|
55
56
|
export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
56
57
|
/**
|
|
@@ -61,4 +62,9 @@ export declare function parseDomainUrl(stdout: string): string | undefined;
|
|
|
61
62
|
* that is not JSON throws: read as "no volume", it would add a second one.
|
|
62
63
|
*/
|
|
63
64
|
export declare function volumeOn(stdout: string, service: string, mountPath: string): string | undefined;
|
|
64
|
-
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void,
|
|
65
|
+
export declare function deployRailwayRun(plan: RailwayRunPlan, railway: CliRunner, log: (msg: string) => void,
|
|
66
|
+
/** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
|
|
67
|
+
registrars: Pick<Registrars, "leave">, healthProbe?: PublicHealthProbe, sleep?: (ms: number) => Promise<void>, boxWebhooks?: {
|
|
68
|
+
probe: BoxWebhooksProbe;
|
|
69
|
+
intervalMs?: number;
|
|
70
|
+
}): Promise<RailwayRunOutcome>;
|
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
import { loginGate, publicHealthGate,
|
|
1
|
+
import { confirmWebhooks, loginGate, publicHealthGate, } from "../channel-ingress.js";
|
|
2
2
|
import { readOutput } from "../runner.js";
|
|
3
|
-
import { hostSignedOutGate
|
|
4
|
-
|
|
5
|
-
export function isLinked(stdout) {
|
|
6
|
-
return stdout.trim() !== "";
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
|
|
10
|
-
* --json` puts `name` at the top level on 5.15.0).
|
|
11
|
-
*/
|
|
12
|
-
export function linkedName(stdout) {
|
|
13
|
-
try {
|
|
14
|
-
const v = JSON.parse(stdout);
|
|
15
|
-
if (typeof v.name === "string")
|
|
16
|
-
return v.name;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// non-JSON but non-empty → still linked, just no name to show
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
3
|
+
import { hostSignedOutGate } from "../secrets.js";
|
|
4
|
+
import { ensureRailwayService, railwayStore } from "./store.js";
|
|
23
5
|
/** Every string leaf of a parsed JSON value. */
|
|
24
6
|
function jsonStrings(stdout) {
|
|
25
7
|
const walk = (v) => typeof v === "string"
|
|
@@ -72,16 +54,13 @@ export function volumeOn(stdout, service, mountPath) {
|
|
|
72
54
|
*/
|
|
73
55
|
const VOLUME_ATTACH_TIMEOUT_MS = 60_000;
|
|
74
56
|
const VOLUME_POLL_MS = 2_000;
|
|
75
|
-
export async function deployRailwayRun(plan, railway, log,
|
|
57
|
+
export async function deployRailwayRun(plan, railway, log,
|
|
58
|
+
/** Slack's, which this machine still sets (its Request URL no credential on the box can change). */
|
|
59
|
+
registrars, healthProbe, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), boxWebhooks) {
|
|
76
60
|
const gate = (g) => ({ ok: false, gate: g });
|
|
77
61
|
// Every --service below targets plan.name — the name this tool gives BOTH the project and the service (`init
|
|
78
62
|
// --name` + `add --service`).
|
|
79
63
|
const svc = ["--service", plan.name];
|
|
80
|
-
// 1. Gate missing required secret VALUES before any side effect (no half-created infra), and before asking a
|
|
81
|
-
// person to sign in for a deploy that would stop here anyway.
|
|
82
|
-
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
83
|
-
if (missingValues)
|
|
84
|
-
return gate(missingValues);
|
|
85
64
|
// 2.
|
|
86
65
|
if ((await railway(["whoami"], { capture: true })).code !== 0) {
|
|
87
66
|
const failed = plan.hostLogin
|
|
@@ -91,58 +70,22 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
|
|
|
91
70
|
return gate(failed);
|
|
92
71
|
}
|
|
93
72
|
// 3.
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"INTO this project, re-run with --into-linked. To start fresh, `railway unlink` first.");
|
|
102
|
-
}
|
|
103
|
-
log(`provisioning into linked project ${linkedName(status.stdout) ?? plan.name} (--into-linked)`);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// --into-linked means "provision into the project this dir is linked to" — but it isn't linked.
|
|
107
|
-
if (plan.intoLinked) {
|
|
108
|
-
log("warn: --into-linked was passed but this directory isn't linked to any project — creating a fresh one");
|
|
109
|
-
}
|
|
110
|
-
log(`creating project ${plan.name}…`);
|
|
111
|
-
if ((await railway(["init", "--name", plan.name])).code !== 0) {
|
|
112
|
-
return gate("`railway init` failed — if you have multiple workspaces, run it once interactively (or pass --workspace) to pick one, then re-run");
|
|
113
|
-
}
|
|
114
|
-
// Create + link the service (init makes only a project); it precedes the volume, which has no --service flag and
|
|
115
|
-
// rides the linked service.
|
|
116
|
-
log(`creating service ${plan.name}…`);
|
|
117
|
-
// `--variables` with one of the machinery variables set just below: in a terminal, a bare `railway add` stops at
|
|
118
|
-
// "Enter a variable <esc to skip>" (5.62.1) and waits for a key nobody was told to press.
|
|
119
|
-
const addArgs = ["add", "--service", plan.name, "--variables", `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`];
|
|
120
|
-
if ((await railway(addArgs)).code !== 0) {
|
|
121
|
-
// Precise recovery, not "fix and re-run": init already created + linked the project, so a plain re-run hits the
|
|
122
|
-
// linked-gate, and --into-linked SKIPS `add` and then fails at the volume (no service to ride).
|
|
123
|
-
return gate(`\`railway add --service\` failed — \`railway init\` already created the project (this directory is now ` +
|
|
124
|
-
`linked) but not the service. Finish with \`railway add --service ${plan.name}\` here, then re-run with ` +
|
|
125
|
-
`--into-linked; or \`railway unlink\` to detach and start fresh.`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
73
|
+
const projectGate = await ensureRailwayService(railway, { name: plan.name, mountPath: plan.mountPath, intoLinked: plan.intoLinked }, log);
|
|
74
|
+
if (projectGate !== undefined)
|
|
75
|
+
return gate(projectGate);
|
|
76
|
+
// 3a. The names the service holds, the gaps filled.
|
|
77
|
+
const secretsGate = await plan.provideSecrets(railwayStore(railway, plan.name, log, { redeploy: false }));
|
|
78
|
+
if (secretsGate !== undefined)
|
|
79
|
+
return gate(secretsGate);
|
|
128
80
|
// 3b.
|
|
129
81
|
const machineryVars = [
|
|
130
82
|
`FASTAGENT_STATE_DIR=${plan.mountPath}/.state`,
|
|
131
83
|
`FASTAGENT_SECRETS_DIR=${plan.mountPath}/.secrets`,
|
|
132
84
|
];
|
|
133
|
-
|
|
134
|
-
// list on every host.
|
|
135
|
-
const secretNames = Object.keys(plan.secrets);
|
|
136
|
-
log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")} + ${secretNames.length} secret(s)` +
|
|
137
|
-
`${secretNames.length > 0 ? `: ${secretNames.join(", ")}` : ""}…`);
|
|
85
|
+
log(`setting ${machineryVars.map((v) => v.split("=")[0]).join("/")}…`);
|
|
138
86
|
if ((await railway(["variables", "set", ...machineryVars, ...svc])).code !== 0) {
|
|
139
87
|
return gate("`railway variables set` failed — see the railway output above");
|
|
140
88
|
}
|
|
141
|
-
for (const [k, v] of Object.entries(plan.secrets)) {
|
|
142
|
-
if ((await railway(["variables", "set", k, "--stdin", ...svc], { input: v })).code !== 0) {
|
|
143
|
-
return gate(`\`railway variables set ${k}\` failed — see the railway output above`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
89
|
// 3c.
|
|
147
90
|
const volumeStatus = () => readOutput(railway, "railway", ["volume", "list", "--json"], (out) => volumeOn(out, plan.name, plan.mountPath));
|
|
148
91
|
const existing = await volumeStatus();
|
|
@@ -174,17 +117,19 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
|
|
|
174
117
|
"check `railway volume list`, then re-run with --into-linked once it shows Ready");
|
|
175
118
|
}
|
|
176
119
|
}
|
|
177
|
-
// 5.
|
|
178
|
-
|
|
179
|
-
if ((await railway(["up", "--ci", ...svc])).code !== 0) {
|
|
180
|
-
return gate("`railway up` failed — see the railway output above; fix and re-run");
|
|
181
|
-
}
|
|
182
|
-
// 6.
|
|
120
|
+
// 5. The domain before the upload: `railway domain` creates the service's on a service never deployed, and answers
|
|
121
|
+
// the existing one when run again, so the image the upload builds names it as its `http.publicUrl`.
|
|
183
122
|
log("getting the public domain…");
|
|
184
123
|
const url = parseDomainUrl((await railway(["domain", "--json", ...svc], { capture: true })).stdout);
|
|
185
124
|
if (!url) {
|
|
186
125
|
return gate("couldn't read a domain from `railway domain` — run `railway domain` manually, then set any webhook");
|
|
187
126
|
}
|
|
127
|
+
const publicUrl = await plan.deliverPublicUrl(url);
|
|
128
|
+
// 6. Deploy — CI mode streams build logs then exits (no interactive attach). Build runs on Railway.
|
|
129
|
+
log("deploying (railway up)…");
|
|
130
|
+
if ((await railway(["up", "--ci", ...svc])).code !== 0) {
|
|
131
|
+
return gate("`railway up` failed — see the railway output above; fix and re-run");
|
|
132
|
+
}
|
|
188
133
|
// 6b. `railway up --ci` exits 0 once the build is accepted, so readiness is asked here and not inferred.
|
|
189
134
|
const healthGate = await publicHealthGate({
|
|
190
135
|
baseUrl: url,
|
|
@@ -207,12 +152,21 @@ export async function deployRailwayRun(plan, railway, log, registrars, healthPro
|
|
|
207
152
|
}));
|
|
208
153
|
}
|
|
209
154
|
// 7.
|
|
210
|
-
|
|
155
|
+
// The box points its own channels at the `http.publicUrl` delivered above; this reads what it made of them.
|
|
156
|
+
const registrationGateMsg = await confirmWebhooks({
|
|
211
157
|
baseUrl: url,
|
|
158
|
+
publicUrl,
|
|
212
159
|
channels: plan.channels,
|
|
213
160
|
registrars,
|
|
214
161
|
log,
|
|
162
|
+
...(plan.recordUrl === undefined ? {} : { record: plan.recordUrl }),
|
|
215
163
|
retryHint: "re-run with --into-linked to retry registration",
|
|
164
|
+
...(boxWebhooks === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: {
|
|
167
|
+
probe: boxWebhooks.probe,
|
|
168
|
+
...(boxWebhooks.intervalMs === undefined ? {} : { intervalMs: boxWebhooks.intervalMs }),
|
|
169
|
+
}),
|
|
216
170
|
});
|
|
217
171
|
if (registrationGateMsg)
|
|
218
172
|
return gate(registrationGateMsg);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Railway's service variables as a deployment's store: `railway variable list` for the names, `variables set` for values. */
|
|
2
|
+
import type { SecretStore } from "../secret-store.ts";
|
|
3
|
+
import { type CliRunner } from "../runner.ts";
|
|
4
|
+
/** Whether `railway status --json` shows a linked project: non-empty stdout. */
|
|
5
|
+
export declare function isLinked(stdout: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
|
|
8
|
+
* --json` puts `name` at the top level on 5.15.0).
|
|
9
|
+
*/
|
|
10
|
+
export declare function linkedName(stdout: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The project and service a deployment's values and image belong to. The project named for the agent (`secrets set` or
|
|
13
|
+
* an earlier `--run` made and linked it) is continued, and given the service when it lacks one. A directory linked to
|
|
14
|
+
* any other project is refused unless `intoLinked` says to provision into it, whatever that project holds: a service of
|
|
15
|
+
* the same name does not make it the agent's. An unlinked directory is given a new project. Resolves the gate line when
|
|
16
|
+
* it cannot be done.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ensureRailwayService(railway: CliRunner, plan: {
|
|
19
|
+
name: string;
|
|
20
|
+
mountPath: string;
|
|
21
|
+
intoLinked: boolean;
|
|
22
|
+
}, log: (message: string) => void): Promise<string | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* `service`'s variables, in the project and environment this directory is linked to. `redeploy`: the service restarts on
|
|
25
|
+
* what is written, once, after the last; off, for a deploy that follows and applies them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function railwayStore(railway: CliRunner, service: string, log: (message: string) => void, options: {
|
|
28
|
+
redeploy: boolean;
|
|
29
|
+
}): SecretStore;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readOutput } from "../runner.js";
|
|
2
|
+
/** Whether `railway status --json` shows a linked project: non-empty stdout. */
|
|
3
|
+
export function isLinked(stdout) {
|
|
4
|
+
return stdout.trim() !== "";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The linked project's name for the gate message, or undefined if it can't be read (still linked — `railway status
|
|
8
|
+
* --json` puts `name` at the top level on 5.15.0).
|
|
9
|
+
*/
|
|
10
|
+
export function linkedName(stdout) {
|
|
11
|
+
try {
|
|
12
|
+
const v = JSON.parse(stdout);
|
|
13
|
+
if (typeof v.name === "string")
|
|
14
|
+
return v.name;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// non-JSON but non-empty → still linked, just no name to show
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function unrelatedProject(name, agent) {
|
|
22
|
+
return (`this directory is already linked to Railway project ${name ? `"${name}"` : "(name unreadable)"}, not to ` +
|
|
23
|
+
`this agent's own (${agent}). \`--run\` won't deploy into an unrelated project. To provision the agent ` +
|
|
24
|
+
"INTO this project, re-run with --into-linked. To start fresh, `railway unlink` first.");
|
|
25
|
+
}
|
|
26
|
+
/** Whether `railway status --json` lists a service called `name`. */
|
|
27
|
+
function hasService(stdout, name) {
|
|
28
|
+
const walk = (value) => {
|
|
29
|
+
if (value === null || typeof value !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const object = value;
|
|
32
|
+
if (object.serviceName === name)
|
|
33
|
+
return true;
|
|
34
|
+
return Object.values(object).some(walk);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
return walk(JSON.parse(stdout));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The project and service a deployment's values and image belong to. The project named for the agent (`secrets set` or
|
|
45
|
+
* an earlier `--run` made and linked it) is continued, and given the service when it lacks one. A directory linked to
|
|
46
|
+
* any other project is refused unless `intoLinked` says to provision into it, whatever that project holds: a service of
|
|
47
|
+
* the same name does not make it the agent's. An unlinked directory is given a new project. Resolves the gate line when
|
|
48
|
+
* it cannot be done.
|
|
49
|
+
*/
|
|
50
|
+
export async function ensureRailwayService(railway, plan, log) {
|
|
51
|
+
const status = await railway(["status", "--json"], { capture: true });
|
|
52
|
+
if (isLinked(status.stdout)) {
|
|
53
|
+
const name = linkedName(status.stdout);
|
|
54
|
+
if (!plan.intoLinked && name !== plan.name)
|
|
55
|
+
return unrelatedProject(name, plan.name);
|
|
56
|
+
if (plan.intoLinked)
|
|
57
|
+
log(`provisioning into linked project ${name ?? plan.name} (--into-linked)`);
|
|
58
|
+
if (hasService(status.stdout, plan.name))
|
|
59
|
+
return undefined;
|
|
60
|
+
return addService(railway, plan, log);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// --into-linked means "provision into the project this dir is linked to" — but it isn't linked.
|
|
64
|
+
if (plan.intoLinked) {
|
|
65
|
+
log("warn: --into-linked was passed but this directory isn't linked to any project — creating a fresh one");
|
|
66
|
+
}
|
|
67
|
+
log(`creating project ${plan.name}…`);
|
|
68
|
+
if ((await railway(["init", "--name", plan.name])).code !== 0) {
|
|
69
|
+
return "`railway init` failed — if you have multiple workspaces, run it once interactively (or pass --workspace) to pick one, then re-run";
|
|
70
|
+
}
|
|
71
|
+
return addService(railway, plan, log);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function addService(railway, plan, log) {
|
|
75
|
+
// `init` makes only a project; the service precedes the volume, which has no --service flag and rides the linked one.
|
|
76
|
+
log(`creating service ${plan.name}…`);
|
|
77
|
+
// `--variables` with one of the machinery variables set just below: in a terminal, a bare `railway add` stops at
|
|
78
|
+
// "Enter a variable <esc to skip>" (5.62.1) and waits for a key nobody was told to press.
|
|
79
|
+
const addArgs = ["add", "--service", plan.name, "--variables", `FASTAGENT_STATE_DIR=${plan.mountPath}/.state`];
|
|
80
|
+
if ((await railway(addArgs)).code !== 0) {
|
|
81
|
+
return (`\`railway add --service\` failed — the project ${plan.name} exists and this directory is linked to it, but ` +
|
|
82
|
+
`it has no service. Run \`railway add --service ${plan.name}\` here, then re-run; or \`railway unlink\` to ` +
|
|
83
|
+
`detach and start fresh.`);
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** The names `railway variable list --json` shows: the keys of an object. Its values are never read. */
|
|
88
|
+
function variableNames(stdout) {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(stdout);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not the parser's message: it quotes the text around the error, and this output holds every value.
|
|
95
|
+
throw new Error("the output was not JSON");
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
98
|
+
throw new Error(`expected a JSON object of variables, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`);
|
|
99
|
+
}
|
|
100
|
+
return new Set(Object.keys(parsed));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* `service`'s variables, in the project and environment this directory is linked to. `redeploy`: the service restarts on
|
|
104
|
+
* what is written, once, after the last; off, for a deploy that follows and applies them.
|
|
105
|
+
*/
|
|
106
|
+
export function railwayStore(railway, service, log, options) {
|
|
107
|
+
const svc = ["--service", service];
|
|
108
|
+
return {
|
|
109
|
+
async exists() {
|
|
110
|
+
const status = (await railway(["status", "--json"], { capture: true })).stdout;
|
|
111
|
+
if (!isLinked(status))
|
|
112
|
+
return false;
|
|
113
|
+
const name = linkedName(status);
|
|
114
|
+
if (name !== service)
|
|
115
|
+
throw new Error(unrelatedProject(name, service));
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
async ensure() {
|
|
119
|
+
const gate = await ensureRailwayService(railway, { name: service, mountPath: "/data", intoLinked: false }, log);
|
|
120
|
+
if (gate !== undefined)
|
|
121
|
+
throw new Error(gate);
|
|
122
|
+
},
|
|
123
|
+
async names() {
|
|
124
|
+
const listed = await readOutput(railway, "railway", ["variable", "list", "--json", ...svc], variableNames);
|
|
125
|
+
if ("gate" in listed)
|
|
126
|
+
throw new Error(listed.gate);
|
|
127
|
+
return listed.value;
|
|
128
|
+
},
|
|
129
|
+
async write(values) {
|
|
130
|
+
const names = Object.keys(values);
|
|
131
|
+
log(`setting ${names.length} variable(s) on ${service}: ${names.join(", ")}`);
|
|
132
|
+
// One value per call over stdin, never argv. `--skip-deploys` on all but the last of a write that is to take effect.
|
|
133
|
+
for (const [index, name] of names.entries()) {
|
|
134
|
+
const skip = options.redeploy && index === names.length - 1 ? [] : ["--skip-deploys"];
|
|
135
|
+
const result = await railway(["variables", "set", name, "--stdin", ...skip, ...svc], {
|
|
136
|
+
input: values[name],
|
|
137
|
+
});
|
|
138
|
+
if (result.code !== 0)
|
|
139
|
+
throw new Error(`\`railway variables set ${name}\` failed — see the railway output above`);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
package/dist/deploy/runner.d.ts
CHANGED
|
@@ -30,9 +30,12 @@ export declare function readOutput<T>(run: CliRunner, bin: string, args: string[
|
|
|
30
30
|
}>;
|
|
31
31
|
/**
|
|
32
32
|
* Production {@link CliRunner}: spawn `bin` in `cwd` (the agent directory, which is the build/upload context). stderr
|
|
33
|
-
* is always inherited to the terminal.
|
|
33
|
+
* is always inherited to the terminal. `stdoutToStderr`: what the CLI prints is not this command's result (a command
|
|
34
|
+
* whose stdout is data, `secrets set --generate`), so it goes to this process's stderr, still on the terminal.
|
|
34
35
|
*/
|
|
35
|
-
export declare function spawnRunner(bin: string, cwd: string
|
|
36
|
+
export declare function spawnRunner(bin: string, cwd: string, options?: {
|
|
37
|
+
stdoutToStderr?: boolean;
|
|
38
|
+
}): CliRunner;
|
|
36
39
|
/**
|
|
37
40
|
* The AWS CLI, as every command here runs it: with its pager off. Given a terminal, AWS CLI v2 pipes any output it
|
|
38
41
|
* prints through `less`, so `deploy agentcore --run` stopped at `(END)` after `ecr create-repository` until someone
|
package/dist/deploy/runner.js
CHANGED
|
@@ -25,16 +25,17 @@ export async function readOutput(run, bin, args, read) {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Production {@link CliRunner}: spawn `bin` in `cwd` (the agent directory, which is the build/upload context). stderr
|
|
28
|
-
* is always inherited to the terminal.
|
|
28
|
+
* is always inherited to the terminal. `stdoutToStderr`: what the CLI prints is not this command's result (a command
|
|
29
|
+
* whose stdout is data, `secrets set --generate`), so it goes to this process's stderr, still on the terminal.
|
|
29
30
|
*/
|
|
30
|
-
export function spawnRunner(bin, cwd) {
|
|
31
|
+
export function spawnRunner(bin, cwd, options = {}) {
|
|
31
32
|
return (args, opts) => new Promise((res) => {
|
|
32
33
|
const child = spawn(bin, args, {
|
|
33
34
|
cwd,
|
|
34
35
|
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
35
36
|
stdio: [
|
|
36
37
|
opts?.input ? "pipe" : "inherit",
|
|
37
|
-
opts?.capture ? "pipe" : "inherit",
|
|
38
|
+
opts?.capture ? "pipe" : options.stdoutToStderr ? 2 : "inherit",
|
|
38
39
|
opts?.captureStderr ? "pipe" : "inherit",
|
|
39
40
|
],
|
|
40
41
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a deployment's values live and how a deploy fills the gaps (agent-artifact.md §6.2). A target keeps its own
|
|
3
|
+
* values; `deploy` checks names and never carries a file. Most stores can say which names they hold and not the values,
|
|
4
|
+
* so that is all the interface asks of them.
|
|
5
|
+
*
|
|
6
|
+
* The gaps are filled from the person's own sources, in the order a deploy can ask least of them: a value file
|
|
7
|
+
* (`.secrets/production/.env` unless another is named) pushes the names it holds in one batch after one question; a name
|
|
8
|
+
* no source has is asked for. A coding agent's run, with nobody to ask, stops naming the commands instead.
|
|
9
|
+
*/
|
|
10
|
+
import type { SecretNeed } from "./secrets.ts";
|
|
11
|
+
export interface SecretStore {
|
|
12
|
+
/** Whether the app or service the values belong to exists: absent, it holds no value, and `names()` cannot be asked. */
|
|
13
|
+
exists(): Promise<boolean>;
|
|
14
|
+
/** Make the app or service the values belong to, when it does not exist yet. */
|
|
15
|
+
ensure(): Promise<void>;
|
|
16
|
+
/** The names the target holds a value for. */
|
|
17
|
+
names(): Promise<ReadonlySet<string>>;
|
|
18
|
+
/** Set `values` in one batch. */
|
|
19
|
+
write(values: Readonly<Record<string, string>>): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** How a person at a terminal is asked. */
|
|
22
|
+
export interface SecretAsker {
|
|
23
|
+
confirm(question: string): Promise<boolean>;
|
|
24
|
+
/** One value, typed without echo. */
|
|
25
|
+
secret(need: SecretNeed): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
/** The commands a gate names, written for the host and agent this run is about. */
|
|
28
|
+
export interface SecretCommands {
|
|
29
|
+
/** Push the names a value file holds: `fastagent secrets push fly`. */
|
|
30
|
+
push: string;
|
|
31
|
+
/** Set one name: `fastagent secrets set fly NAME`. */
|
|
32
|
+
set(name: string): string;
|
|
33
|
+
}
|
|
34
|
+
export interface ProvideSecrets {
|
|
35
|
+
/** What the target is called in what is said: "Fly". */
|
|
36
|
+
host: string;
|
|
37
|
+
store: SecretStore;
|
|
38
|
+
/** What the target must hold. */
|
|
39
|
+
needs: readonly SecretNeed[];
|
|
40
|
+
/** Every name the definition declares: a source's copy of one the target lacks is offered, optional or not. */
|
|
41
|
+
known: readonly string[];
|
|
42
|
+
/** The value file a source reads, when it exists. */
|
|
43
|
+
source?: {
|
|
44
|
+
file: string;
|
|
45
|
+
values: ReadonlyMap<string, string>;
|
|
46
|
+
};
|
|
47
|
+
/** Absent: nobody to ask. */
|
|
48
|
+
asker?: SecretAsker;
|
|
49
|
+
commands: SecretCommands;
|
|
50
|
+
/** Values FastAgent makes itself, set when neither the target nor a source has one (the Agent API's key). */
|
|
51
|
+
mint?: readonly {
|
|
52
|
+
name: string;
|
|
53
|
+
make(): string;
|
|
54
|
+
}[];
|
|
55
|
+
/** Say a line to the person who runs the deploy, for what a deploy with nobody to ask leaves undone. */
|
|
56
|
+
say?: (line: string) => void;
|
|
57
|
+
/** Say what the deploy did by itself, which leaves the person nothing to do. */
|
|
58
|
+
inform?: (line: string) => void;
|
|
59
|
+
/** What was minted, once the target holds it: kept on this machine too, where the person who calls the API looks. */
|
|
60
|
+
onMinted?: (values: Readonly<Record<string, string>>) => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
/** Resolves a gate line when the target still lacks a required name afterwards, undefined once it holds them all. */
|
|
63
|
+
export declare function provideSecrets(input: ProvideSecrets): Promise<string | undefined>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The gap the target has, as the line a person (or the agent relaying one) acts on. */
|
|
2
|
+
function gapLine(host, missing, held, commands) {
|
|
3
|
+
const names = missing.map((need) => `${need.name} (${need.hint})`).join("; ");
|
|
4
|
+
const fromFile = missing.filter((need) => held.includes(need.name));
|
|
5
|
+
const rest = missing.filter((need) => !held.includes(need.name));
|
|
6
|
+
const ways = [
|
|
7
|
+
...(fromFile.length > 0
|
|
8
|
+
? [
|
|
9
|
+
`\`${commands.push}\` sets ${fromFile.length === 1 ? "the one" : "the ones"} a value file holds (${fromFile.map((n) => n.name).join(", ")})`,
|
|
10
|
+
]
|
|
11
|
+
: []),
|
|
12
|
+
...(rest.length > 0
|
|
13
|
+
? [
|
|
14
|
+
`a person types ${fromFile.length > 0 ? "the others" : rest.length === 1 ? "it" : "them"} in a terminal, one command each: ${rest.map((need) => `\`${commands.set(need.name)}\``).join(", ")}`,
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
`\`${commands.push} --from-env\` takes them from the environment of a secret manager or a CI job (\`op run\`, \`doppler run\`)`,
|
|
18
|
+
];
|
|
19
|
+
return `${host} has no value for: ${names}. ${ways.join("; ")}. Then re-run. A value is never asked for in a conversation`;
|
|
20
|
+
}
|
|
21
|
+
/** Resolves a gate line when the target still lacks a required name afterwards, undefined once it holds them all. */
|
|
22
|
+
export async function provideSecrets(input) {
|
|
23
|
+
const { host, store, needs, known, source, asker, commands, mint = [] } = input;
|
|
24
|
+
try {
|
|
25
|
+
const have = await store.names();
|
|
26
|
+
const missing = needs.filter((need) => !have.has(need.name));
|
|
27
|
+
const ours = new Set(mint.map((made) => made.name));
|
|
28
|
+
const filed = (name) => {
|
|
29
|
+
const value = source?.values.get(name);
|
|
30
|
+
return value !== undefined && value.trim() !== "" ? value : undefined;
|
|
31
|
+
};
|
|
32
|
+
// What FastAgent made is not the person's to be asked about: the file records it, so a target that lacks it (a
|
|
33
|
+
// deployment made again) gets that value, and one the file lacks too gets a new one.
|
|
34
|
+
const minted = {};
|
|
35
|
+
const restored = [];
|
|
36
|
+
for (const { name, make } of mint) {
|
|
37
|
+
if (have.has(name))
|
|
38
|
+
continue;
|
|
39
|
+
const kept = filed(name);
|
|
40
|
+
if (kept === undefined)
|
|
41
|
+
minted[name] = make();
|
|
42
|
+
else {
|
|
43
|
+
minted[name] = kept;
|
|
44
|
+
restored.push(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const offered = [...(source?.values ?? [])]
|
|
48
|
+
.filter(([name, value]) => value.trim() !== "" && known.includes(name) && !have.has(name) && !ours.has(name))
|
|
49
|
+
.map(([name]) => name);
|
|
50
|
+
if (missing.length === 0 && offered.length === 0 && Object.keys(minted).length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const finishMinted = async () => {
|
|
53
|
+
if (restored.length > 0) {
|
|
54
|
+
input.inform?.(`set ${restored.join(", ")} on ${host} from ${source?.file}, where an earlier deploy kept it`);
|
|
55
|
+
}
|
|
56
|
+
const made = Object.fromEntries(Object.entries(minted).filter(([name]) => !restored.includes(name)));
|
|
57
|
+
if (Object.keys(made).length > 0)
|
|
58
|
+
await input.onMinted?.(made);
|
|
59
|
+
};
|
|
60
|
+
const held = offered.filter((name) => missing.some((need) => need.name === name));
|
|
61
|
+
if (asker === undefined) {
|
|
62
|
+
const lacking = missing.filter((need) => !(need.name in minted));
|
|
63
|
+
if (lacking.length > 0)
|
|
64
|
+
return gapLine(host, lacking, held, commands);
|
|
65
|
+
// Names a file holds and the target lacks, which nothing requires: said, since nobody was asked to set them.
|
|
66
|
+
if (offered.length > 0 && source !== undefined) {
|
|
67
|
+
input.say?.(`${host} lacks ${offered.join(", ")}, which ${source.file} holds; \`${commands.push}\` sets ${offered.length === 1 ? "it" : "them"}`);
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(minted).length > 0) {
|
|
70
|
+
await store.write(minted);
|
|
71
|
+
await finishMinted();
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const write = { ...minted };
|
|
76
|
+
if (offered.length > 0 && source !== undefined) {
|
|
77
|
+
const required = new Set(missing.map((need) => need.name));
|
|
78
|
+
const list = offered.map((name) => (required.has(name) ? name : `${name} (optional)`)).join(", ");
|
|
79
|
+
if (await asker.confirm(`${host} lacks ${list}, which ${source.file} holds. Set them on ${host} from it?`)) {
|
|
80
|
+
for (const name of offered)
|
|
81
|
+
write[name] = source.values.get(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const need of missing) {
|
|
85
|
+
if (need.name in write)
|
|
86
|
+
continue;
|
|
87
|
+
write[need.name] = await asker.secret(need);
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(write).length > 0)
|
|
90
|
+
await store.write(write);
|
|
91
|
+
if (Object.keys(minted).length > 0)
|
|
92
|
+
await finishMinted();
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return `${host}'s values could not be read or set: ${error instanceof Error ? error.message : String(error)}`;
|
|
97
|
+
}
|
|
98
|
+
}
|