@fastagent-sh/fastagent 0.22.1 → 0.23.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/channels/agentcore-protocol.d.ts +2 -1
- package/dist/channels/agentcore-service.js +10 -18
- package/dist/channels/agentcore.js +6 -3
- package/dist/channels/control.js +23 -0
- package/dist/channels/slack/config-api.d.ts +2 -2
- package/dist/channels/slack/config-api.js +2 -2
- package/dist/channels/slack/onboard.js +2 -2
- package/dist/channels/slack/register-webhook.js +2 -2
- package/dist/channels/slack/slack-api.js +4 -4
- package/dist/cli/add-feishu.js +4 -2
- package/dist/cli/auth-view.d.ts +15 -6
- package/dist/cli/auth-view.js +24 -8
- package/dist/cli/box-login.d.ts +33 -0
- package/dist/cli/box-login.js +162 -0
- package/dist/cli/commands/deploy/agentcore.js +48 -11
- package/dist/cli/commands/deploy/docker.js +24 -13
- package/dist/cli/commands/deploy/fly.js +30 -7
- package/dist/cli/commands/deploy/railway.js +21 -7
- package/dist/cli/commands/deploy/shared.d.ts +18 -17
- package/dist/cli/commands/deploy/shared.js +20 -21
- package/dist/cli/commands/destroy.js +2 -2
- package/dist/cli/commands/info.js +9 -6
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/login.d.ts +7 -1
- package/dist/cli/commands/login.js +123 -10
- package/dist/cli/commands/logs.js +2 -2
- package/dist/cli/commands/models.d.ts +4 -1
- package/dist/cli/commands/models.js +37 -4
- package/dist/cli/commands/routine-run.js +2 -2
- package/dist/cli/commands/start.d.ts +0 -5
- package/dist/cli/commands/start.js +3 -13
- package/dist/cli/kernel.d.ts +2 -0
- package/dist/cli/kernel.js +1 -1
- package/dist/cli/login-relay.d.ts +38 -0
- package/dist/cli/login-relay.js +121 -0
- package/dist/cli/program.js +46 -8
- package/dist/cli/shared.d.ts +4 -4
- package/dist/cli/shared.js +24 -30
- package/dist/deploy/agentcore/plan.d.ts +10 -11
- package/dist/deploy/agentcore/plan.js +8 -14
- package/dist/deploy/agentcore/run.d.ts +8 -1
- package/dist/deploy/agentcore/run.js +20 -14
- package/dist/deploy/agentcore/shell.d.ts +25 -0
- package/dist/deploy/agentcore/shell.js +202 -0
- package/dist/deploy/box-shell.d.ts +43 -0
- package/dist/deploy/box-shell.js +28 -0
- package/dist/deploy/build-context.js +16 -7
- package/dist/deploy/channel-ingress.d.ts +20 -1
- package/dist/deploy/channel-ingress.js +34 -7
- package/dist/deploy/container.d.ts +14 -0
- package/dist/deploy/container.js +45 -1
- package/dist/deploy/docker/plan.d.ts +7 -3
- package/dist/deploy/docker/plan.js +9 -14
- package/dist/deploy/docker/run.d.ts +14 -7
- package/dist/deploy/docker/run.js +58 -35
- package/dist/deploy/fly/plan.d.ts +6 -3
- package/dist/deploy/fly/plan.js +6 -8
- package/dist/deploy/fly/run.d.ts +8 -2
- package/dist/deploy/fly/run.js +15 -25
- package/dist/deploy/preflight.d.ts +9 -7
- package/dist/deploy/preflight.js +74 -20
- package/dist/deploy/railway/plan.d.ts +6 -3
- package/dist/deploy/railway/plan.js +7 -9
- package/dist/deploy/railway/run.d.ts +10 -6
- package/dist/deploy/railway/run.js +29 -13
- package/dist/deploy/runner.d.ts +16 -0
- package/dist/deploy/runner.js +31 -0
- package/dist/deploy/secrets.d.ts +4 -13
- package/dist/deploy/secrets.js +8 -27
- package/dist/dev-supervisor.js +5 -2
- package/dist/engines/pi/agent-models.d.ts +70 -0
- package/dist/engines/pi/agent-models.js +93 -0
- package/dist/engines/pi/auth.d.ts +56 -3
- package/dist/engines/pi/auth.js +63 -16
- package/dist/engines/pi/config.d.ts +0 -25
- package/dist/engines/pi/config.js +3 -37
- package/dist/engines/pi/create.d.ts +22 -16
- package/dist/engines/pi/create.js +28 -28
- package/dist/engines/pi/entry-images.d.ts +14 -0
- package/dist/engines/pi/entry-images.js +23 -0
- package/dist/engines/pi/invoke-session.js +6 -4
- package/dist/engines/pi/login.d.ts +20 -13
- package/dist/engines/pi/login.js +19 -12
- package/dist/engines/pi/models.d.ts +115 -36
- package/dist/engines/pi/models.js +218 -48
- package/dist/engines/pi/open.d.ts +42 -11
- package/dist/engines/pi/open.js +53 -21
- package/dist/engines/pi/service.d.ts +2 -2
- package/dist/engines/pi/service.js +1 -0
- package/dist/engines/pi/session-builder.d.ts +3 -2
- package/dist/engines/pi/session-builder.js +28 -58
- package/dist/engines/pi/session-control.js +14 -3
- package/dist/engines/pi/turn-kit.js +11 -5
- package/dist/open-url.d.ts +15 -1
- package/dist/open-url.js +35 -3
- package/dist/paths.d.ts +4 -2
- package/dist/paths.js +6 -2
- package/dist/pi.d.ts +5 -3
- package/dist/pi.js +3 -2
- package/dist/service.d.ts +5 -8
- package/dist/session-remote.js +21 -0
- package/dist/session.d.ts +28 -7
- package/package.json +2 -1
package/dist/deploy/container.js
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
/** The portable container — Dockerfile + .dockerignore, host-neutral. */
|
|
2
2
|
import { SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
|
|
3
3
|
import { RELEASE_FILE, parseDeploymentRelease } from "./workspace.js";
|
|
4
|
+
/** The result line for a box CLI that refused `login --stdio` as an unknown option (see {@link boxLoginCommand}). */
|
|
5
|
+
const STDIO_PREDATED = JSON.stringify({
|
|
6
|
+
type: "result",
|
|
7
|
+
ok: false,
|
|
8
|
+
reason: "failed",
|
|
9
|
+
message: "the fastagent installed in the agent on the box predates login --stdio: align the @fastagent-sh/fastagent " +
|
|
10
|
+
"version in the agent package.json with the CLI that deploys it, then redeploy",
|
|
11
|
+
}).replaceAll('"', '\\"');
|
|
12
|
+
/** Where a generated image keeps its storage (`FASTAGENT_STORAGE_DIR`); every host mounts its volume here. */
|
|
13
|
+
const STORAGE_DIR = "/data";
|
|
14
|
+
/**
|
|
15
|
+
* The shell command that runs `fastagent login --stdio <args>` inside a running generated image, as the server there
|
|
16
|
+
* would see it: in the deployed agent directory, with the storage and credential paths resolved by the same defaults
|
|
17
|
+
* `prepareStartWorkspace` applies. A host's own shell does not reliably hand those over — its session may not start
|
|
18
|
+
* in the server's directory, and the platform's variables are the only ones it inherits. `storage` names the root
|
|
19
|
+
* outright for a shell that inherits none of them. The CLI is the one the server runs: the agent's own install when
|
|
20
|
+
* it has one (under Bun when the image is Bun's), else the pinned global.
|
|
21
|
+
*
|
|
22
|
+
* That own install may predate `login --stdio` (an agent pinned to an older release, deployed by a newer CLI). It then
|
|
23
|
+
* refuses the flag as a usage error, exit 2, which a `login --stdio` that ran never gives (it answers 0 or 1 after its
|
|
24
|
+
* result line), so the command answers for it: one result line naming the mismatch, instead of a shell that ended
|
|
25
|
+
* with nothing to say. No quote or backtick in it: hosts wrap this whole line in `sh -c '…'`.
|
|
26
|
+
*/
|
|
27
|
+
export function boxLoginCommand(agent, args, storage) {
|
|
28
|
+
for (const value of [agent, ...args, ...(storage ? [storage] : [])]) {
|
|
29
|
+
if (!/^[\w.@/-]+$/.test(value))
|
|
30
|
+
throw new Error(`cannot pass ${JSON.stringify(value)} to a shell on the box`);
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
...(storage
|
|
34
|
+
? [`root="${storage}"`, `export FASTAGENT_SECRETS_DIR="$root/.secrets"`]
|
|
35
|
+
: [
|
|
36
|
+
`root="\${FASTAGENT_STORAGE_DIR:-${STORAGE_DIR}}"`,
|
|
37
|
+
`export FASTAGENT_SECRETS_DIR="\${FASTAGENT_SECRETS_DIR:-$root/.secrets}"`,
|
|
38
|
+
]),
|
|
39
|
+
`cd "$root/base/${agent}" || exit 1`,
|
|
40
|
+
`f=fastagent`,
|
|
41
|
+
`if [ -x node_modules/.bin/fastagent ]; then f=node_modules/.bin/fastagent; if command -v bun >/dev/null; then f="bun run fastagent"; fi; fi`,
|
|
42
|
+
`$f login --stdio ${args.join(" ")}`,
|
|
43
|
+
`code=$?`,
|
|
44
|
+
`if [ $code -eq 2 ]; then printf "%s\\n" "${STDIO_PREDATED}"; fi`,
|
|
45
|
+
`exit $code`,
|
|
46
|
+
].join("; ");
|
|
47
|
+
}
|
|
4
48
|
/** Fixed first-line prefix of a fastagent-generated Dockerfile. */
|
|
5
49
|
export const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
6
50
|
/** The ONE predicate for "did fastagent generate this Dockerfile?". */
|
|
@@ -41,7 +85,7 @@ function dockerfile(input) {
|
|
|
41
85
|
// PIN the agent into the image.
|
|
42
86
|
const pin = `ENV FASTAGENT_AGENT=${dir}\n`;
|
|
43
87
|
// The caches stay off the volume: they are rebuildable, and a host mount is the slow disk.
|
|
44
|
-
const deployment = `ENV FASTAGENT_RELEASE_FILE=/app/${into(RELEASE_FILE)}\nENV FASTAGENT_STORAGE_DIR
|
|
88
|
+
const deployment = `ENV FASTAGENT_RELEASE_FILE=/app/${into(RELEASE_FILE)}\nENV FASTAGENT_STORAGE_DIR=${STORAGE_DIR}\nENV npm_config_cache=/tmp/fastagent/npm\nENV BUN_INSTALL_CACHE_DIR=/tmp/fastagent/bun\n`;
|
|
45
89
|
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent` from PATH.
|
|
46
90
|
if (!input.hasPackageJson) {
|
|
47
91
|
return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** `fastagent deploy docker` — the local-Docker plan. */
|
|
2
2
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
3
3
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { DeploymentSecret } from "../secrets.ts";
|
|
5
5
|
export interface DockerPlanInput extends ContainerInput {
|
|
6
6
|
/** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
|
|
7
7
|
projectName: string;
|
|
8
8
|
/** Container + default host port. */
|
|
9
9
|
port: number;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The provider the deployment logs in to itself (`fastagent login --deployment`): its credential is not a variable
|
|
12
|
+
* the plan carries.
|
|
13
|
+
*/
|
|
14
|
+
boxLogin?: string;
|
|
12
15
|
/** Every declared channel and its ingress — the source of the ingress note. */
|
|
13
16
|
channels: readonly DeclaredChannel[];
|
|
14
17
|
/** Generate an optional Cloudflare Quick Tunnel service in Compose. */
|
|
@@ -29,6 +32,7 @@ export interface DockerPlan {
|
|
|
29
32
|
/** Ordered local build/run/operate instructions. */
|
|
30
33
|
runbook: string[];
|
|
31
34
|
}
|
|
35
|
+
export declare const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
32
36
|
export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
33
37
|
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
34
38
|
export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { webhookPaths } from "../channel-ingress.js";
|
|
2
2
|
import { containerArtifacts } from "../container.js";
|
|
3
|
-
import {
|
|
3
|
+
import { deploymentLoginCommand } from "../box-shell.js";
|
|
4
4
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
5
5
|
const MOUNT = "/data";
|
|
6
|
-
const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
6
|
+
export const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
7
7
|
const GENERATED_COMPOSE_MARKER = "# Generated by `fastagent deploy docker`";
|
|
8
8
|
export const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
9
9
|
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
@@ -81,17 +81,12 @@ services:
|
|
|
81
81
|
- ${SECRETS_DIRNAME}/.env
|
|
82
82
|
environment:
|
|
83
83
|
PORT: "${input.port}"
|
|
84
|
-
# Machinery on the ONE state volume: mutable state and (
|
|
84
|
+
# Machinery on the ONE state volume: mutable state and credentials (logged in on the box). Pinned AFTER
|
|
85
85
|
# env_file so a local path in that file (the scaffold lists these) cannot send the container's state, sessions
|
|
86
86
|
# or credentials somewhere outside the volume — or at a host path that does not exist here at all.
|
|
87
87
|
FASTAGENT_STATE_DIR: "${MOUNT}/.state"
|
|
88
88
|
FASTAGENT_SECRETS_DIR: "${MOUNT}/.secrets"
|
|
89
89
|
FASTAGENT_AUTH_PATH: "${MOUNT}/.secrets/auth.json"
|
|
90
|
-
# The ONE value that is not in the value file: \`--run\` mints it from the local auth.json. Left as a seam in
|
|
91
|
-
# the committed topology so a hand-run \`up\` can supply it the same way — from the ENVIRONMENT of that \`up\`.
|
|
92
|
-
# Writing this one key into the value file does nothing: \`environment:\` is applied after \`env_file\`, so the
|
|
93
|
-
# line below would blank it and the container would start without seeding auth.json.
|
|
94
|
-
FASTAGENT_AUTH_SEED: "${composeInterpolation("FASTAGENT_AUTH_SEED")}"
|
|
95
90
|
volumes:
|
|
96
91
|
- state:${MOUNT}
|
|
97
92
|
# The agent's log is where a failure's reason is recorded (a scheduled fire logs the detail; what the turn
|
|
@@ -126,13 +121,13 @@ export function planDockerDeploy(input) {
|
|
|
126
121
|
if (secrets.length > 0) {
|
|
127
122
|
runbook.push(`# Environment: the container reads ${input.valueFile} whole (Compose \`env_file\`) — that file declares the`, `# deployed environment (in CI, write it before running the command):`, ...secrets.map((secret) => `# ${secret.name}: ${secret.hint}`));
|
|
128
123
|
}
|
|
129
|
-
if (!isEnvKey(input.modelAuth)) {
|
|
130
|
-
runbook.push(input.modelAuth === undefined
|
|
131
|
-
? `# Model auth: none found locally — run \`fastagent login\` or set a provider API key before --run.`
|
|
132
|
-
: `# Model auth: local source is "${input.modelAuth}" — \`--run\` carries auth.json as FASTAGENT_AUTH_SEED.`, `# For a manual Compose run, provide a provider API key or set FASTAGENT_AUTH_SEED yourself.`);
|
|
133
|
-
}
|
|
134
124
|
runbook.push(`# Run from the WORKSPACE ROOT (the directory containing ${input.agentPrefix}).`, `# The build bakes the whole directory as the agent's workspace; only the agent's own`, `# dependencies (${input.agentPrefix}package.json) are installed.`);
|
|
135
|
-
runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT} retains the workspace, state and credentials.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`,
|
|
125
|
+
runbook.push(``, `# Before building a new definition release, run \`fastagent deploy docker\` to refresh its manifest.`, `# The Compose volume at ${MOUNT} retains the workspace, state and credentials.`, `${compose} up -d --build`, `curl --fail http://127.0.0.1:${input.port}/health`, ...(input.boxLogin
|
|
126
|
+
? [
|
|
127
|
+
`# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself:`,
|
|
128
|
+
`${deploymentLoginCommand("docker", input.boxLogin)}`,
|
|
129
|
+
]
|
|
130
|
+
: []), ``, `# Operate it:`, `${compose} logs -f agent`, `${compose} ps`, `${compose} down # stops containers; keeps the state volume`, `# ${compose} down -v # DESTRUCTIVE: deletes workspace, credentials and all agent state`);
|
|
136
131
|
if (input.tunnel) {
|
|
137
132
|
runbook.push(``, `# Quick Tunnel: \`--run\` starts the Compose tunnel service and reads its ephemeral public URL.`, `# Telegram/Feishu/Lark and locally onboarded Slack auto-register; manual Slack prints its console URL. Re-run after the`, `# tunnel container/Docker daemon restarts: a new Quick Tunnel URL must replace the old webhook URL.`, `${compose} logs -f tunnel`);
|
|
138
133
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
2
|
+
import type { BoxLoginStep } from "../box-shell.ts";
|
|
3
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
1
4
|
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
2
5
|
import type { CliRunner } from "../runner.ts";
|
|
3
6
|
export interface DockerRunPlan {
|
|
@@ -6,21 +9,25 @@ export interface DockerRunPlan {
|
|
|
6
9
|
/** Container port from config; used to ask Compose for the effective published host port. */
|
|
7
10
|
port: number;
|
|
8
11
|
/**
|
|
9
|
-
* What `--run` assembled. The container reads the value file itself through the generated `env_file`, so
|
|
10
|
-
*
|
|
11
|
-
* reported to the operator and otherwise unused.
|
|
12
|
+
* What `--run` assembled. The container reads the value file itself through the generated `env_file`, so nothing
|
|
13
|
+
* travels through this process: the names are reported to the operator and otherwise unused.
|
|
12
14
|
*/
|
|
13
15
|
secrets: Record<string, string>;
|
|
14
16
|
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
15
17
|
missingSecrets: string[];
|
|
16
18
|
/** That value file, workspace-relative, so the gate names the file this deploy actually read. */
|
|
17
19
|
valueFile: string;
|
|
18
|
-
/** Neither an env-key credential nor a readable auth.json is available. */
|
|
19
|
-
needsModelCredential: boolean;
|
|
20
20
|
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar answered. */
|
|
21
21
|
announce: DockerAnnounce;
|
|
22
|
+
/** Every declared channel and its ingress: which of them a failed login leaves unregistered. */
|
|
23
|
+
channels: readonly DeclaredChannel[];
|
|
22
24
|
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
23
25
|
requireTunnel: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Log the box in (`fastagent login --deployment`) once it is up and before any webhook is pointed at it: a channel pointed at
|
|
28
|
+
* a box with no model credential answers every message with a failure. Resolves a gate line, or undefined.
|
|
29
|
+
*/
|
|
30
|
+
boxLogin?: BoxLoginStep;
|
|
24
31
|
}
|
|
25
32
|
export type DockerRunOutcome = {
|
|
26
33
|
ok: true;
|
|
@@ -52,14 +59,14 @@ export interface ComposeTunnel {
|
|
|
52
59
|
url: string;
|
|
53
60
|
connected: boolean;
|
|
54
61
|
}
|
|
55
|
-
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string
|
|
62
|
+
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string) => Promise<ComposeTunnel | undefined>;
|
|
56
63
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
57
64
|
export declare function localUrlFromComposePort(stdout: string): string | undefined;
|
|
58
65
|
/**
|
|
59
66
|
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
60
67
|
* connection.
|
|
61
68
|
*/
|
|
62
|
-
export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string,
|
|
69
|
+
export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile: string, options?: {
|
|
63
70
|
attempts?: number;
|
|
64
71
|
intervalMs?: number;
|
|
65
72
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** `fastagent deploy docker --run` — reconcile the generated/user-owned Compose application locally. */
|
|
2
1
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
2
|
+
import { loginGate } from "../channel-ingress.js";
|
|
3
3
|
import { TUNNEL_DNS_LAG_MS, hasTunnelConnection, parseTunnelUrl } from "../../tunnel.js";
|
|
4
4
|
import { registrationGate } from "../registration-gate.js";
|
|
5
5
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
@@ -23,12 +23,12 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
23
23
|
* Poll the detached cloudflared service's logs until its Quick Tunnel URL is assigned AND the tunnel reports an edge
|
|
24
24
|
* connection.
|
|
25
25
|
*/
|
|
26
|
-
export async function waitForComposeTunnelUrl(docker, composeFile,
|
|
26
|
+
export async function waitForComposeTunnelUrl(docker, composeFile, options = {}) {
|
|
27
27
|
const compose = ["compose", "-f", composeFile];
|
|
28
28
|
const attempts = options.attempts ?? 60;
|
|
29
29
|
let assigned;
|
|
30
30
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
31
|
-
const logs = await docker([...compose, "logs", "--no-color", "tunnel"], { capture: true
|
|
31
|
+
const logs = await docker([...compose, "logs", "--no-color", "tunnel"], { capture: true });
|
|
32
32
|
if (logs.code === 0) {
|
|
33
33
|
assigned ??= parseTunnelUrl(logs.stdout);
|
|
34
34
|
if (assigned && hasTunnelConnection(logs.stdout)) {
|
|
@@ -41,14 +41,27 @@ export async function waitForComposeTunnelUrl(docker, composeFile, env, options
|
|
|
41
41
|
}
|
|
42
42
|
return assigned ? { url: assigned, connected: false } : undefined;
|
|
43
43
|
}
|
|
44
|
-
const defaultTunnelUrlProbe = (docker, composeFile
|
|
44
|
+
const defaultTunnelUrlProbe = (docker, composeFile) => waitForComposeTunnelUrl(docker, composeFile);
|
|
45
|
+
/**
|
|
46
|
+
* `/health` asked from inside the agent container, for a topology that publishes no host port. The image always has
|
|
47
|
+
* node or bun (the generated Dockerfile's base), and both have `fetch`; nothing else is assumed installed.
|
|
48
|
+
*/
|
|
49
|
+
async function containerHealthy(docker, compose, port, stillStarting) {
|
|
50
|
+
const probe = `r=$(command -v node || command -v bun) && "$r" -e ` +
|
|
51
|
+
`"fetch('http://127.0.0.1:${port}/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"`;
|
|
52
|
+
const deadline = Date.now() + 180_000; // the same first-boot budget as the published-port probe
|
|
53
|
+
while (Date.now() < deadline) {
|
|
54
|
+
if ((await docker([...compose, "exec", "-T", "agent", "sh", "-c", probe], { capture: true })).code === 0)
|
|
55
|
+
return true;
|
|
56
|
+
if (!(await stillStarting()))
|
|
57
|
+
return false;
|
|
58
|
+
await sleep(1_000);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
45
62
|
export async function deployDockerRun(plan, docker, log, healthProbe = defaultHealthProbe, tunnelUrlProbe = defaultTunnelUrlProbe) {
|
|
46
63
|
const gate = (message) => ({ ok: false, gate: message });
|
|
47
64
|
const compose = ["compose", "-f", plan.composeFile];
|
|
48
|
-
// The ONE value that is not in the value file, so the ONE that has to cross this process. Set even when absent, so
|
|
49
|
-
// a same-named variable in the builder's shell cannot interpolate into the container in its place — `spawnRunner`
|
|
50
|
-
// merges over `process.env`.
|
|
51
|
-
const env = { FASTAGENT_AUTH_SEED: plan.secrets.FASTAGENT_AUTH_SEED ?? "" };
|
|
52
65
|
// CLI/plugin gate first: unlike a daemon error, spawn ENOENT becomes 127 at the shared runner seam.
|
|
53
66
|
const version = await docker(["compose", "version"], { capture: true });
|
|
54
67
|
if (version.code === 127) {
|
|
@@ -57,17 +70,14 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
57
70
|
if (version.code !== 0) {
|
|
58
71
|
return gate("Docker Compose plugin is unavailable — install/enable `docker compose`, then re-run");
|
|
59
72
|
}
|
|
60
|
-
//
|
|
61
|
-
if (plan.needsModelCredential) {
|
|
62
|
-
return gate("no model credential — run `fastagent login`, or set a provider API key in .env, then re-run");
|
|
63
|
-
}
|
|
73
|
+
// The values gate precedes the first side effect (build/create).
|
|
64
74
|
const missingValues = missingValuesGate(plan.missingSecrets, plan.valueFile);
|
|
65
75
|
if (missingValues)
|
|
66
76
|
return gate(missingValues);
|
|
67
77
|
// Name what the container must find, and WHERE — the generated Compose reads the value file itself, so this run
|
|
68
|
-
// hands Compose nothing
|
|
69
|
-
//
|
|
70
|
-
const secretNames = Object.keys(plan.secrets)
|
|
78
|
+
// hands Compose nothing. Saying "passing N secrets to Compose" would be false, and doubly so on a hand-owned
|
|
79
|
+
// Compose file that has no `env_file` entry at all.
|
|
80
|
+
const secretNames = Object.keys(plan.secrets);
|
|
71
81
|
if (secretNames.length > 0) {
|
|
72
82
|
log(`${secretNames.length} value(s) the container reads from ${plan.valueFile}: ${secretNames.join(", ")}`);
|
|
73
83
|
}
|
|
@@ -75,7 +85,7 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
75
85
|
return gate("Docker daemon is unavailable — start Docker Engine/Desktop, then re-run");
|
|
76
86
|
}
|
|
77
87
|
// The file on disk is authoritative.
|
|
78
|
-
const configured = await docker([...compose, "config", "--services"], { capture: true
|
|
88
|
+
const configured = await docker([...compose, "config", "--services"], { capture: true });
|
|
79
89
|
if (configured.code !== 0) {
|
|
80
90
|
return gate(`could not load ${plan.composeFile} — generated files require Docker Compose >= ` +
|
|
81
91
|
`${MIN_DOCKER_COMPOSE_VERSION}; upgrade Compose or fix the file, then re-run`);
|
|
@@ -92,19 +102,16 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
92
102
|
// Quick Tunnel logs are the control-plane output (the assigned URL).
|
|
93
103
|
if (hasTunnel) {
|
|
94
104
|
log("recreating the ephemeral tunnel service…");
|
|
95
|
-
if ((await docker([...compose, "rm", "-s", "-f", "tunnel"]
|
|
105
|
+
if ((await docker([...compose, "rm", "-s", "-f", "tunnel"])).code !== 0) {
|
|
96
106
|
return gate(`could not recreate the tunnel service — inspect \`docker compose -f ${plan.composeFile} ps\``);
|
|
97
107
|
}
|
|
98
108
|
}
|
|
99
109
|
log(`building and reconciling ${plan.composeFile}…`);
|
|
100
|
-
if ((await docker([...compose, "up", "-d", "--build"]
|
|
110
|
+
if ((await docker([...compose, "up", "-d", "--build"])).code !== 0) {
|
|
101
111
|
return gate(`\`docker compose up\` failed — see the Docker output above; fix ${plan.composeFile} and re-run`);
|
|
102
112
|
}
|
|
103
113
|
// Detached `up` can return 0 just before a bad command exits.
|
|
104
|
-
const running = await docker([...compose, "ps", "--status", "running", "--services"], {
|
|
105
|
-
capture: true,
|
|
106
|
-
env,
|
|
107
|
-
});
|
|
114
|
+
const running = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true });
|
|
108
115
|
const runningServices = running.stdout.split(/\s+/).filter(Boolean);
|
|
109
116
|
if (running.code !== 0 || !runningServices.includes("agent")) {
|
|
110
117
|
return gate(`the Compose service "agent" is not running — inspect with \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
@@ -113,31 +120,47 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
113
120
|
return gate(`the Compose service "tunnel" is not running — inspect with \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
114
121
|
}
|
|
115
122
|
// A user-owned topology may deliberately remove the host port and expose only through its own ingress.
|
|
116
|
-
const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true
|
|
123
|
+
const published = await docker([...compose, "port", "agent", String(plan.port)], { capture: true });
|
|
117
124
|
const url = published.code === 0 ? localUrlFromComposePort(published.stdout) : undefined;
|
|
118
|
-
|
|
125
|
+
// Throttled, and an unreadable answer reads as "still starting": the health poll runs twice a second, and a
|
|
126
|
+
// `compose ps` per poll would cost more than the wait it shortens.
|
|
127
|
+
let lastCheck = Date.now();
|
|
128
|
+
const stillStarting = async () => {
|
|
129
|
+
if (Date.now() - lastCheck < 5_000)
|
|
130
|
+
return true;
|
|
131
|
+
lastCheck = Date.now();
|
|
132
|
+
const ps = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true });
|
|
133
|
+
return ps.code !== 0 || ps.stdout.split(/\s+/).includes("agent");
|
|
134
|
+
};
|
|
135
|
+
if (!url && plan.boxLogin) {
|
|
136
|
+
// The login runs in the prepared workspace, with the CLI the first boot installs: it needs the box READY, and
|
|
137
|
+
// with no published port the only place to ask `/health` is inside the container.
|
|
138
|
+
log("agent is running (no host-published port found); waiting for /health inside the container before login…");
|
|
139
|
+
if (!(await containerHealthy(docker, compose, plan.port, stillStarting))) {
|
|
140
|
+
return gate(`agent did not become healthy inside the container — inspect \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (!url) {
|
|
119
144
|
log("agent is running (no host-published port found; using the Compose ingress readiness floor)");
|
|
120
145
|
}
|
|
121
146
|
else {
|
|
122
147
|
const healthUrl = `${url}/health`;
|
|
123
|
-
// Throttled, and an unreadable answer reads as "still starting": the health poll runs twice a second, and a
|
|
124
|
-
// `compose ps` per poll would cost more than the wait it shortens.
|
|
125
|
-
let lastCheck = Date.now();
|
|
126
|
-
const stillStarting = async () => {
|
|
127
|
-
if (Date.now() - lastCheck < 5_000)
|
|
128
|
-
return true;
|
|
129
|
-
lastCheck = Date.now();
|
|
130
|
-
const ps = await docker([...compose, "ps", "--status", "running", "--services"], { capture: true, env });
|
|
131
|
-
return ps.code !== 0 || ps.stdout.split(/\s+/).includes("agent");
|
|
132
|
-
};
|
|
133
148
|
if (!(await healthProbe(healthUrl, stillStarting))) {
|
|
134
149
|
return gate(`agent did not become healthy at ${healthUrl} — inspect \`docker compose -f ${plan.composeFile} logs agent\``);
|
|
135
150
|
}
|
|
136
151
|
}
|
|
152
|
+
// `url` travels with this gate: Compose is up, and the operator needs to know where.
|
|
153
|
+
const notLoggedIn = await plan.boxLogin?.run();
|
|
154
|
+
if (notLoggedIn) {
|
|
155
|
+
// Only a tunnel is ours to announce; without one, the webhooks were never this run's to register.
|
|
156
|
+
const afterLogin = "re-run `fastagent deploy docker --run` (it keeps the login and announces the tunnel)";
|
|
157
|
+
const channels = hasTunnel ? plan.channels : [];
|
|
158
|
+
return { ok: false, gate: loginGate({ notLoggedIn, channels, log, afterLogin }), url };
|
|
159
|
+
}
|
|
137
160
|
if (!hasTunnel)
|
|
138
161
|
return { ok: true, url };
|
|
139
162
|
log("waiting for the Compose tunnel service to publish its Quick Tunnel URL…");
|
|
140
|
-
const tunnel = await tunnelUrlProbe(docker, plan.composeFile
|
|
163
|
+
const tunnel = await tunnelUrlProbe(docker, plan.composeFile);
|
|
141
164
|
if (!tunnel) {
|
|
142
165
|
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
143
166
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/** `fastagent deploy fly` — the Fly.io deploy PLAN, computed from the resolved definition. */
|
|
2
2
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
3
3
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { DeploymentSecret } from "../secrets.ts";
|
|
5
5
|
export interface FlyPlanInput extends ContainerInput {
|
|
6
6
|
/** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
|
|
7
7
|
appName: string;
|
|
8
8
|
/** The port the app listens on (config.http.port ?? 8787); fly.toml routes to it. */
|
|
9
9
|
port: number;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The provider the deployment logs in to itself (`fastagent login --deployment`): its credential is not a variable
|
|
12
|
+
* the plan carries.
|
|
13
|
+
*/
|
|
14
|
+
boxLogin?: string;
|
|
12
15
|
/**
|
|
13
16
|
* Every declared channel and its ingress — the source of the webhook steps, and whether a machine
|
|
14
17
|
* must stay up for an outbound connection.
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { webhookRunbook } from "../channel-ingress.js";
|
|
2
2
|
import { containerArtifacts } from "../container.js";
|
|
3
|
+
import { deploymentLoginCommand } from "../box-shell.js";
|
|
3
4
|
import { CRON_CAN_BE_EXTERNAL, WAKEUPS_WHEN_ASLEEP, residencyFor } from "../residency.js";
|
|
4
|
-
import { isEnvKey } from "../secrets.js";
|
|
5
5
|
function flyToml(appName, port, residency) {
|
|
6
6
|
// WHY is residency.ts's to decide; the SETTING and its wording are Fly's. Anything else is an edit to the
|
|
7
7
|
// generated file, which is the artifact's job.
|
|
@@ -26,7 +26,7 @@ primary_region = "iad" # set your region (list: \`fly platform regions\`)
|
|
|
26
26
|
|
|
27
27
|
[env]
|
|
28
28
|
FASTAGENT_STATE_DIR = "/data/.state" # mutable machine state — sessions, channel state, schedule
|
|
29
|
-
FASTAGENT_SECRETS_DIR = "/data/.secrets" #
|
|
29
|
+
FASTAGENT_SECRETS_DIR = "/data/.secrets" # credentials the box logged in with — must persist across restarts
|
|
30
30
|
PORT = "${port}"
|
|
31
31
|
|
|
32
32
|
[http_service]
|
|
@@ -54,7 +54,7 @@ export function isGeneratedFlyToml(content) {
|
|
|
54
54
|
}
|
|
55
55
|
/** Compute the Fly deploy plan from the resolved definition. */
|
|
56
56
|
export function planFlyDeploy(input) {
|
|
57
|
-
const { appName, port,
|
|
57
|
+
const { appName, port, channels } = input;
|
|
58
58
|
// Artifacts sit under the agent prefix, so they never touch the workspace's own deploy files.
|
|
59
59
|
const flyTomlPath = `${input.agentPrefix}fly.toml`;
|
|
60
60
|
const artifacts = [
|
|
@@ -92,11 +92,9 @@ export function planFlyDeploy(input) {
|
|
|
92
92
|
runbook.push(``, `# The volume keeps /data/base (including uncommitted work), .state and .secrets across restarts and deploys.`, `# Each generated release replaces only /data/base/${input.agentPrefix}; other workspace files are initialized once.`, input.shipsGit
|
|
93
93
|
? `# Git is available for optional collaboration. Verify uploaded history with \`git status\`; clone if the host stripped it.`
|
|
94
94
|
: `# To use Git for collaboration, add deploy: { apt: ["git"] }. Storage durability does not require Git.`);
|
|
95
|
-
//
|
|
96
|
-
if (
|
|
97
|
-
runbook.push(``,
|
|
98
|
-
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; set FASTAGENT_AUTH_PATH (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
99
|
-
: `# 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.`);
|
|
95
|
+
// The credential is created on the box, never carried: the box is then the only holder of its grant.
|
|
96
|
+
if (input.boxLogin) {
|
|
97
|
+
runbook.push(``, `# Model auth: once it is up, the deployment logs in to ${input.boxLogin} itself (from this workspace):`, `${deploymentLoginCommand("fly", input.boxLogin)}`);
|
|
100
98
|
}
|
|
101
99
|
// The fastagent-only post-step: point each channel at the live URL.
|
|
102
100
|
const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** `fastagent deploy fly --run` — drive flyctl to completion. */
|
|
2
|
+
import type { BoxLoginStep } from "../box-shell.ts";
|
|
2
3
|
import { type PublicHealthProbe, type Registrars } from "../channel-ingress.ts";
|
|
3
4
|
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
4
|
-
import type
|
|
5
|
+
import { type CliRunner } from "../runner.ts";
|
|
5
6
|
export interface FlyRunPlan {
|
|
6
7
|
appName: string;
|
|
7
|
-
/** `KEY=value` secrets to set on Fly: the value file's variables
|
|
8
|
+
/** `KEY=value` secrets to set on Fly: the value file's variables. */
|
|
8
9
|
secrets: Record<string, string>;
|
|
9
10
|
/** Declared names the value file supplies no value for — the run gates on these before any side effect. */
|
|
10
11
|
missingSecrets: string[];
|
|
@@ -19,6 +20,11 @@ export interface FlyRunPlan {
|
|
|
19
20
|
* resolve it relative to the config's own directory).
|
|
20
21
|
*/
|
|
21
22
|
dockerfile: string;
|
|
23
|
+
/**
|
|
24
|
+
* Log the box in (`fastagent login --deployment`) once it is up and before any webhook is pointed at it: a channel pointed at
|
|
25
|
+
* a box with no model credential answers every message with a failure. Resolves a gate line, or undefined.
|
|
26
|
+
*/
|
|
27
|
+
boxLogin?: BoxLoginStep;
|
|
22
28
|
}
|
|
23
29
|
/** Done, or a gate the operator must clear before re-running (printed + non-zero exit by the CLI). */
|
|
24
30
|
export type FlyRunOutcome = {
|
package/dist/deploy/fly/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { loginGate, publicHealthGate, registerWebhooks, } from "../channel-ingress.js";
|
|
2
|
+
import { readOutput } from "../runner.js";
|
|
3
3
|
import { missingValuesGate } from "../secrets.js";
|
|
4
4
|
/**
|
|
5
5
|
* The `Type` values that put an app on `https://<app>.fly.dev`, BY FAMILY — one entry per allocate command, because
|
|
@@ -28,27 +28,6 @@ export function listHasName(stdout, name) {
|
|
|
28
28
|
throw new Error(`expected a JSON array, got ${typeof entries}`);
|
|
29
29
|
return entries.some((o) => o.Name === name);
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
* A read-only `fly … list --json`, reduced to the question the next step asks of it — or the gate for a list we cannot
|
|
33
|
-
* act on.
|
|
34
|
-
*/
|
|
35
|
-
async function readList(fly, args, read) {
|
|
36
|
-
// The command as RUN, not a restatement of it.
|
|
37
|
-
const cmd = `fly ${args.join(" ")}`;
|
|
38
|
-
const result = await fly(args, { capture: true });
|
|
39
|
-
if (result.code !== 0)
|
|
40
|
-
return { gate: `\`${cmd}\` failed — see the flyctl output above; fix and re-run` };
|
|
41
|
-
try {
|
|
42
|
-
return { value: read(result.stdout) };
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
// The one place a parse failure is allowed to stop being an exception.
|
|
46
|
-
return {
|
|
47
|
-
gate: `\`${cmd}\` was unreadable (${error instanceof Error ? error.message : String(error)}) — ` +
|
|
48
|
-
`run it yourself and check the flyctl version; fix and re-run`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
31
|
export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
53
32
|
const gate = (g) => ({ ok: false, gate: g });
|
|
54
33
|
// 1.
|
|
@@ -60,7 +39,7 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
60
39
|
if (missingValues)
|
|
61
40
|
return gate(missingValues);
|
|
62
41
|
// 3.
|
|
63
|
-
const appExists = await
|
|
42
|
+
const appExists = await readOutput(fly, "fly", ["apps", "list", "--json"], (out) => listHasName(out, plan.appName));
|
|
64
43
|
if ("gate" in appExists)
|
|
65
44
|
return gate(appExists.gate);
|
|
66
45
|
if (appExists.value) {
|
|
@@ -77,7 +56,7 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
77
56
|
// which machine (guest size + image) must fit on the host the volume is pinned to. Pre-creating one here placed it
|
|
78
57
|
// blind, and a host with disk but no compute failed the deploy with `insufficient resources … with existing volume`.
|
|
79
58
|
// 5.
|
|
80
|
-
const addresses = await
|
|
59
|
+
const addresses = await readOutput(fly, "fly", ["ips", "list", "-a", plan.appName, "--json"], ingressAddresses);
|
|
81
60
|
if ("gate" in addresses)
|
|
82
61
|
return gate(addresses.gate);
|
|
83
62
|
// Check-then-act PER FAMILY: an app that already holds one must still be given the other, or the gate between the
|
|
@@ -131,9 +110,20 @@ export async function deployFlyRun(plan, fly, log, registrars, healthProbe) {
|
|
|
131
110
|
inspectHint: `the app itself deployed — inspect \`fly logs -a ${plan.appName}\`, then re-run once it answers ` +
|
|
132
111
|
`(a re-run repeats the remote build)`,
|
|
133
112
|
probe: healthProbe,
|
|
113
|
+
...(plan.boxLogin ? { login: plan.boxLogin.command } : {}),
|
|
134
114
|
});
|
|
135
115
|
if (healthGate)
|
|
136
116
|
return gate(healthGate);
|
|
117
|
+
const notLoggedIn = await plan.boxLogin?.run();
|
|
118
|
+
if (notLoggedIn) {
|
|
119
|
+
return gate(loginGate({
|
|
120
|
+
notLoggedIn,
|
|
121
|
+
channels: plan.channels,
|
|
122
|
+
log,
|
|
123
|
+
baseUrl,
|
|
124
|
+
afterLogin: "re-run `fastagent deploy fly --run` (it keeps the login)",
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
137
127
|
// 9.
|
|
138
128
|
const registrationGateMsg = await registerWebhooks({
|
|
139
129
|
baseUrl,
|
|
@@ -26,20 +26,22 @@ interface DeployFacts {
|
|
|
26
26
|
values: ReadonlyMap<string, string>;
|
|
27
27
|
/** That file, workspace-relative — the name every "set it here" message must use. */
|
|
28
28
|
valueFile: string;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The variable the value file carries the model's key in (the provider's own, or a models.json `"$NAME"`), or
|
|
31
|
+
* undefined when no key travels that way ({@link credentialRoute}).
|
|
32
|
+
*/
|
|
30
33
|
modelAuth: string | undefined;
|
|
31
34
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* The provider the deployment must log in to itself (`fastagent login --deployment`): the model's credential does
|
|
36
|
+
* not travel, because it is neither a variable in the value file nor carried by the definition (a models.json
|
|
37
|
+
* literal `apiKey` or `!command`). Nothing from this machine's credentials file is ever copied, so a login on the
|
|
38
|
+
* box is its only holder.
|
|
35
39
|
*/
|
|
36
|
-
|
|
40
|
+
boxLogin: string | undefined;
|
|
37
41
|
/** Every tool/routine/channel declaration — the names the value file must supply, by declaring file. */
|
|
38
42
|
declaredSecrets: DeclaredSecret[];
|
|
39
43
|
/** The runbook's variable list: the declared names, then everything else the value file carries. */
|
|
40
44
|
secrets: DeploymentSecret[];
|
|
41
|
-
/** The project-level auth file `--run` reads to carry the credential (probed with the same path). */
|
|
42
|
-
authPath: string;
|
|
43
45
|
/** Container facts shared by the plan and the generated Dockerfile — ONE source, so they can't drift. */
|
|
44
46
|
container: ContainerInput;
|
|
45
47
|
port: number;
|