@fastagent-sh/fastagent 0.29.0 → 0.30.1
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/kernel.js +3 -0
- 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.d.ts +7 -0
- package/dist/cli/ticket.js +106 -30
- 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,12 +1,11 @@
|
|
|
1
|
+
import { manualWebhookStep, webhookKinds, webhookPath, } from "../channels/webhooks.js";
|
|
1
2
|
import { registrationGate } from "./registration-gate.js";
|
|
2
3
|
import { waitForHealth } from "../channels/wait-health.js";
|
|
3
4
|
const feishuCloud = (kind, label) => ({
|
|
4
|
-
path: `/${kind}`,
|
|
5
5
|
// The subscription mode is the published version's: a switch from long connection waits for the publish.
|
|
6
6
|
delivers: "delivers to this deployment once its published version is in webhook mode (after a first deploy, publish the " +
|
|
7
7
|
"version it opened)",
|
|
8
8
|
register: (r, baseUrl) => r.feishu?.(baseUrl, kind),
|
|
9
|
-
manual: (baseUrl) => `${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${baseUrl}/${kind} (the app must be running when you save)`,
|
|
10
9
|
runbook: (baseUrl) => [
|
|
11
10
|
`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`,
|
|
12
11
|
`# POST /${kind}; the app must be RUNNING when you save (the console verifies with a challenge):`,
|
|
@@ -16,10 +15,8 @@ const feishuCloud = (kind, label) => ({
|
|
|
16
15
|
/** Declaration order — the order every runbook and every driver reports in. */
|
|
17
16
|
const INGRESS = {
|
|
18
17
|
telegram: {
|
|
19
|
-
path: "/telegram",
|
|
20
18
|
delivers: "now delivers to this deployment",
|
|
21
19
|
register: (r, baseUrl) => r.telegram(baseUrl),
|
|
22
|
-
manual: (baseUrl) => `telegram: set the webhook → ${baseUrl}/telegram (secret_token = TELEGRAM_SECRET_TOKEN)`,
|
|
23
20
|
runbook: (baseUrl) => [
|
|
24
21
|
`# Register the Telegram webhook. The path assumes the default route (POST /telegram); if you`,
|
|
25
22
|
`# remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`,
|
|
@@ -28,30 +25,21 @@ const INGRESS = {
|
|
|
28
25
|
],
|
|
29
26
|
},
|
|
30
27
|
slack: {
|
|
31
|
-
path: "/slack",
|
|
32
28
|
delivers: "now delivers to this deployment",
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// The Request URL is the app's page, which a person sets (channel-setup.md §3): no credential here changes it.
|
|
30
|
+
register: () => undefined,
|
|
35
31
|
runbook: (baseUrl) => [
|
|
36
32
|
`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`,
|
|
37
|
-
`# answers Slack's challenge), and match scopes/subscriptions to the app \`
|
|
33
|
+
`# answers Slack's challenge), and match scopes/subscriptions to the app \`channels setup slack\` created:`,
|
|
38
34
|
`# Request URL = ${baseUrl}/slack`,
|
|
39
35
|
],
|
|
40
36
|
},
|
|
41
37
|
feishu: feishuCloud("feishu", "Feishu"),
|
|
42
38
|
lark: feishuCloud("lark", "Lark"),
|
|
43
39
|
};
|
|
44
|
-
/**
|
|
45
|
-
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool knows how to
|
|
46
|
-
* instruct.
|
|
47
|
-
*/
|
|
48
|
-
export function webhookKinds(channels) {
|
|
49
|
-
const declared = new Set(channels.filter((c) => c.ingress === "webhook").map((c) => c.name));
|
|
50
|
-
return Object.keys(INGRESS).filter((kind) => declared.has(kind));
|
|
51
|
-
}
|
|
52
40
|
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
53
41
|
export function webhookPaths(channels) {
|
|
54
|
-
return webhookKinds(channels).map(
|
|
42
|
+
return webhookKinds(channels).map(webhookPath);
|
|
55
43
|
}
|
|
56
44
|
/**
|
|
57
45
|
* The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way (a literal
|
|
@@ -68,7 +56,7 @@ export async function pointChannelsAt(input) {
|
|
|
68
56
|
// Calling IS the question: a channel whose registrar the caller did not wire returns undefined.
|
|
69
57
|
const running = ingress.register(input.registrars, input.baseUrl);
|
|
70
58
|
if (!running) {
|
|
71
|
-
input.log(
|
|
59
|
+
input.log(manualWebhookStep(kind, input.baseUrl));
|
|
72
60
|
outcomes.push({ kind, outcome: "manual" }); // a human's step — re-surfaced after registrar output
|
|
73
61
|
continue;
|
|
74
62
|
}
|
|
@@ -77,7 +65,7 @@ export async function pointChannelsAt(input) {
|
|
|
77
65
|
}
|
|
78
66
|
// What the registrar said is in the log; the person who reads none gets the one route to set.
|
|
79
67
|
for (const { kind } of outcomes.filter(({ outcome }) => outcome === "manual")) {
|
|
80
|
-
input.registrars.leave?.(
|
|
68
|
+
input.registrars.leave?.(manualWebhookStep(kind, input.baseUrl));
|
|
81
69
|
}
|
|
82
70
|
return outcomes;
|
|
83
71
|
}
|
|
@@ -134,9 +122,10 @@ export function loginGate(input) {
|
|
|
134
122
|
const byHand = input.baseUrl === undefined ? [] : webhookRunbook(input.baseUrl, input.channels);
|
|
135
123
|
for (const line of byHand)
|
|
136
124
|
input.log(line);
|
|
137
|
-
// "
|
|
138
|
-
return (`${input.notLoggedIn}. This deploy
|
|
139
|
-
`
|
|
125
|
+
// "Confirmed none" is about THIS run only: a channel the box or an earlier deploy pointed here still delivers.
|
|
126
|
+
return (`${input.notLoggedIn}. This deploy confirmed no webhook, and logging in confirms none; any already pointed here, ` +
|
|
127
|
+
`by the box itself or by an earlier deploy, still delivers, and every message it brings fails until the box is ` +
|
|
128
|
+
`logged in. Once it is, ` +
|
|
140
129
|
`${input.afterLogin}` +
|
|
141
130
|
(byHand.length > 0 ? `. The lines above point the channels by hand.` : ``));
|
|
142
131
|
}
|
|
@@ -168,3 +157,111 @@ export async function registerWebhooks(input) {
|
|
|
168
157
|
}
|
|
169
158
|
return reg.gate();
|
|
170
159
|
}
|
|
160
|
+
/** How long a deploy waits for a box to end its own registration: the box's registrars retry for about 190 s. */
|
|
161
|
+
const BOX_REGISTRATION_TIMEOUT_MS = 240_000;
|
|
162
|
+
const readBoxWebhooks = async (healthUrl) => {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
|
|
165
|
+
const body = (await res.json());
|
|
166
|
+
return Array.isArray(body.webhooks)
|
|
167
|
+
? body.webhooks
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined; // not routable, or not answering JSON yet: polled again until the deadline
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* What a box that points its own channels made of them (channel-setup.md §5): read from its `/health` until no channel
|
|
176
|
+
* is `pending`, then gated as {@link registerWebhooks} gates. Slack's Request URL no credential on the box can set, so
|
|
177
|
+
* `registrars.slack` sets it from this machine, as before.
|
|
178
|
+
*/
|
|
179
|
+
export async function confirmWebhooks(input) {
|
|
180
|
+
const kinds = webhookKinds(input.channels);
|
|
181
|
+
const reg = registrationGate(input.log, input.retryHint);
|
|
182
|
+
const outcomes = new Map();
|
|
183
|
+
if (kinds.includes("slack")) {
|
|
184
|
+
input.log(manualWebhookStep("slack", input.publicUrl));
|
|
185
|
+
outcomes.set("slack", "manual");
|
|
186
|
+
}
|
|
187
|
+
const own = kinds.filter((kind) => kind !== "slack");
|
|
188
|
+
if (own.length > 0) {
|
|
189
|
+
input.log(`waiting for the box to point ${own.join(", ")} at ${input.publicUrl}…`);
|
|
190
|
+
const probe = input.probe ?? readBoxWebhooks;
|
|
191
|
+
const deadline = Date.now() + (input.timeoutMs ?? BOX_REGISTRATION_TIMEOUT_MS);
|
|
192
|
+
for (;;) {
|
|
193
|
+
const read = await probe(`${input.baseUrl}/health`);
|
|
194
|
+
const ended = own.map((kind) => read?.find((entry) => entry.channel === kind));
|
|
195
|
+
if (ended.every((entry) => entry !== undefined && entry.outcome !== "pending")) {
|
|
196
|
+
for (const [i, kind] of own.entries())
|
|
197
|
+
outcomes.set(kind, ended[i]?.outcome);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (Date.now() >= deadline) {
|
|
201
|
+
for (const [i, kind] of own.entries()) {
|
|
202
|
+
if (ended[i] === undefined || ended[i]?.outcome === "pending") {
|
|
203
|
+
input.log(`${kind}: the box did not report its registration within the wait — see its log`);
|
|
204
|
+
outcomes.set(kind, "failed");
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
outcomes.set(kind, ended[i]?.outcome);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, input.intervalMs ?? 2_000));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const kind of kinds) {
|
|
216
|
+
if (outcomes.get(kind) === "registered" || kind === "slack")
|
|
217
|
+
continue;
|
|
218
|
+
// What the box said and why is in its log; the line a person acts on is said here.
|
|
219
|
+
input.log(manualWebhookStep(kind, input.publicUrl));
|
|
220
|
+
input.registrars.leave?.(manualWebhookStep(kind, input.publicUrl));
|
|
221
|
+
}
|
|
222
|
+
if (input.record !== undefined)
|
|
223
|
+
await recordConfirmed(input, kinds, outcomes, input.record);
|
|
224
|
+
for (const kind of kinds) {
|
|
225
|
+
const outcome = outcomes.get(kind);
|
|
226
|
+
reg.track(kind, outcome);
|
|
227
|
+
if (outcome === "registered")
|
|
228
|
+
input.log(`the selected production ${kind} app ${INGRESS[kind].delivers}`);
|
|
229
|
+
}
|
|
230
|
+
return reg.gate();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* For each channel whose URL only a person sets (Slack; Lark without its config API), wait for the box to report it
|
|
234
|
+
* reached at the delivered URL, then record that URL in the binding, so every later deploy, CI included, reports it
|
|
235
|
+
* `registered` from the start (channel-setup.md §5).
|
|
236
|
+
*/
|
|
237
|
+
async function recordConfirmed(input, kinds, outcomes, record) {
|
|
238
|
+
const probe = input.probe ?? readBoxWebhooks;
|
|
239
|
+
const waiting = new Set(kinds.filter((k) => (k === "slack" || k === "lark") && outcomes.get(k) !== "failed"));
|
|
240
|
+
if (waiting.size === 0)
|
|
241
|
+
return;
|
|
242
|
+
const later = (kind) => `\`fastagent channels setup ${kind} --config <file> --record-url\``;
|
|
243
|
+
const manual = [...waiting].filter((kind) => outcomes.get(kind) === "manual");
|
|
244
|
+
if (manual.length > 0) {
|
|
245
|
+
input.log(`waiting up to ${Math.round(record.waitMs / 60_000)} min for ` +
|
|
246
|
+
`${manual.map((kind) => `${input.publicUrl}${webhookPath(kind)}`).join(" and ")} to be saved on the app's ` +
|
|
247
|
+
`page (the platform verifies it with this box); stop it any time and record it later with ${later(manual[0])}`);
|
|
248
|
+
}
|
|
249
|
+
// One deadline for every channel still waiting: each is recorded as the box reports it reached.
|
|
250
|
+
const deadline = Date.now() + record.waitMs;
|
|
251
|
+
for (;;) {
|
|
252
|
+
const read = await probe(`${input.baseUrl}/health`);
|
|
253
|
+
for (const kind of [...waiting]) {
|
|
254
|
+
if (read?.find((entry) => entry.channel === kind)?.outcome !== "registered")
|
|
255
|
+
continue;
|
|
256
|
+
waiting.delete(kind);
|
|
257
|
+
outcomes.set(kind, "registered");
|
|
258
|
+
const url = `${input.publicUrl}${webhookPath(kind)}`;
|
|
259
|
+
// The deployment is done and the platform reaches it: a record not written is said, never the deploy's failure.
|
|
260
|
+
await record.write(kind, url).then((where) => input.log(`${kind}: the platform reached the box at ${url}; recorded it in ${where}`), (error) => input.log(`${kind}: the platform reached the box at ${url}, but it was not recorded (${error.message}); ` +
|
|
261
|
+
`record it with ${later(kind)}`));
|
|
262
|
+
}
|
|
263
|
+
if (waiting.size === 0 || Date.now() >= deadline)
|
|
264
|
+
return;
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, input.intervalMs ?? 2_000));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -1,17 +1,38 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* What a deploy builds the image from beyond the program, in the build context: the instance configuration it delivers
|
|
3
|
+
* and the text that configuration names, the release manifest and the package list. FastAgent's own directory, so it is
|
|
4
|
+
* ignored by git and absent from the author's root, and the image copies it to {@link IMAGE_CONFIG_DIR}. A directory of
|
|
5
|
+
* its own, never a file in the program's: what an instance configuration names beside it (its text) is
|
|
6
|
+
* read from its own directory, and the program's directory holds the program's.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DELIVERED_CONFIG_DIR = ".fastagent/deploy";
|
|
9
|
+
/**
|
|
10
|
+
* The configuration a deploy delivers, with the public URL the host gives the box as its `http.publicUrl`, where the
|
|
11
|
+
* operator's configuration names none (a custom domain is theirs to name): the URL the box points its channels at.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withPublicUrl(config: string | undefined, publicUrl: string): {
|
|
14
|
+
config: string;
|
|
15
|
+
publicUrl: string;
|
|
16
|
+
};
|
|
17
|
+
/** The `http.publicUrl` an operator's configuration names, if it does. */
|
|
18
|
+
export declare function ownPublicUrl(config: string | undefined): string | undefined;
|
|
19
|
+
/** The delivered configuration as the artifact a deploy writes, to rewrite it once the host has given the URL. */
|
|
20
|
+
export declare function deliveredConfigArtifact(config: string): Artifact;
|
|
21
|
+
/** What every deploy writes whoever wrote it before: build output, by its path, never kept or reported stale. */
|
|
2
22
|
export declare const BUILD_OUTPUTS: readonly string[];
|
|
3
23
|
export interface Artifact {
|
|
4
24
|
path: string;
|
|
5
25
|
content: string;
|
|
6
26
|
}
|
|
7
|
-
/** Where a generated image holds the
|
|
8
|
-
export declare const
|
|
27
|
+
/** Where a generated image holds the agent: the build context, which is the agent directory. */
|
|
28
|
+
export declare const IMAGE_APP_DIR = "/app";
|
|
9
29
|
/**
|
|
10
30
|
* The shell command that runs `fastagent login --stdio <args>` inside a running generated image, as the server there
|
|
11
|
-
* would see it: in the
|
|
31
|
+
* would see it: in the image's agent directory, with the storage and credential paths resolved by the same defaults
|
|
12
32
|
* `prepareStartWorkspace` applies. A host's own shell does not reliably hand those over — its session may not start
|
|
13
33
|
* in the server's directory, and the platform's variables are the only ones it inherits. `storage` names the root
|
|
14
|
-
* outright for a shell that inherits none of them
|
|
34
|
+
* outright for a shell that inherits none of them; `program` is where the image has the agent, the generated one's
|
|
35
|
+
* by default. The CLI is the one the server runs: the agent's own install when
|
|
15
36
|
* it has one (under Bun when the image is Bun's), else the pinned global.
|
|
16
37
|
*
|
|
17
38
|
* That own install may predate `login --stdio` (an agent pinned to an older release, deployed by a newer CLI). It then
|
|
@@ -19,7 +40,10 @@ export declare const IMAGE_DEFINITION_DIR = "/app/definition";
|
|
|
19
40
|
* result line), so the command answers for it: one result line naming the mismatch, instead of a shell that ended
|
|
20
41
|
* with nothing to say. No quote or backtick in it: hosts wrap this whole line in `sh -c '…'`.
|
|
21
42
|
*/
|
|
22
|
-
export declare function boxLoginCommand(args: readonly string[], storage?:
|
|
43
|
+
export declare function boxLoginCommand(args: readonly string[], { storage, program }?: {
|
|
44
|
+
storage?: string;
|
|
45
|
+
program?: string;
|
|
46
|
+
}): string;
|
|
23
47
|
/** Fixed first-line prefix of a fastagent-generated Dockerfile. */
|
|
24
48
|
export declare const GENERATED_DOCKERFILE_MARKER = "# Generated by `fastagent deploy`";
|
|
25
49
|
/** The ONE predicate for "did fastagent generate this Dockerfile?". */
|
|
@@ -28,16 +52,12 @@ export declare function isGeneratedDockerignore(content: string): boolean;
|
|
|
28
52
|
export interface ContainerInput {
|
|
29
53
|
/** Stable for one release; generated once by the deploy preflight, never on container restart. */
|
|
30
54
|
releaseId: string;
|
|
31
|
-
/** Whether the agent has a package.json (a code agent); else a pure markdown/skills agent. */
|
|
32
|
-
hasPackageJson: boolean;
|
|
33
55
|
/** The package manager the generated image targets. */
|
|
34
56
|
runtime: "node" | "bun";
|
|
35
57
|
/** Whether the runtime's lockfile exists (package-lock.json for npm, bun.lock/bun.lockb for bun). */
|
|
36
58
|
hasLockfile: boolean;
|
|
37
59
|
/** For `runtime: "bun"`, the `FROM oven/bun:<tag>` version (from packageManager `bun@x`), else `"1"`. */
|
|
38
60
|
bunVersion?: string;
|
|
39
|
-
/** This fastagent version, to PIN the global install on the markdown path (reproducible redeploys). */
|
|
40
|
-
version: string;
|
|
41
61
|
/**
|
|
42
62
|
* The Debian packages the image installs: what FastAgent needs (git for content and a shipped `.git`, CA certificates
|
|
43
63
|
* and curl for mise), then the agent's `aptPackages`.
|
|
@@ -52,6 +72,11 @@ export interface ContainerInput {
|
|
|
52
72
|
* interpolate the operator's shell.
|
|
53
73
|
*/
|
|
54
74
|
modelSpec?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The instance configuration the deployment delivers (`deploy --config`), as the file's text: the image copies it to
|
|
77
|
+
* {@link IMAGE_CONFIG_DIR}, and `start` reads it through `FASTAGENT_CONFIG`. Without one, `{}`: the program's defaults.
|
|
78
|
+
*/
|
|
79
|
+
config?: string;
|
|
55
80
|
/** The agent's name, recorded in the release manifest: the agent directory's own name. */
|
|
56
81
|
agent: string;
|
|
57
82
|
/** Agent-dir-relative machinery paths the NAME-based excludes below would miss. */
|
package/dist/deploy/container.js
CHANGED
|
@@ -1,16 +1,48 @@
|
|
|
1
1
|
/** The portable container — Dockerfile + .dockerignore, host-neutral. */
|
|
2
|
-
import { CONTENT_DIRNAME,
|
|
2
|
+
import { CONTENT_DIRNAME, INSTANCE_CONFIG_FILE, INSTANCES_DIRNAME, SECRETS_DIRNAME, STATE_DIRNAME } from "../paths.js";
|
|
3
3
|
import { RUNTIME_FLOOR } from "../runtime.js";
|
|
4
|
-
import {
|
|
4
|
+
import { RELEASE_FILE, parseDeploymentRelease } from "./workspace.js";
|
|
5
5
|
import { MISE_ENVIRONMENT } from "../environment/mise.js";
|
|
6
6
|
import { BOX_CLI_PREDATES } from "./box-shell.js";
|
|
7
|
+
/**
|
|
8
|
+
* What a deploy builds the image from beyond the program, in the build context: the instance configuration it delivers
|
|
9
|
+
* and the text that configuration names, the release manifest and the package list. FastAgent's own directory, so it is
|
|
10
|
+
* ignored by git and absent from the author's root, and the image copies it to {@link IMAGE_CONFIG_DIR}. A directory of
|
|
11
|
+
* its own, never a file in the program's: what an instance configuration names beside it (its text) is
|
|
12
|
+
* read from its own directory, and the program's directory holds the program's.
|
|
13
|
+
*/
|
|
14
|
+
export const DELIVERED_CONFIG_DIR = `${INSTANCES_DIRNAME}/deploy`;
|
|
15
|
+
const DELIVERED_CONFIG_FILE = `${DELIVERED_CONFIG_DIR}/${INSTANCE_CONFIG_FILE}`;
|
|
16
|
+
const RELEASE_PATH = `${DELIVERED_CONFIG_DIR}/${RELEASE_FILE}`;
|
|
7
17
|
/**
|
|
8
18
|
* The packages the image installs, one per line as mise is given them: build output every deploy writes beside the
|
|
9
19
|
* release manifest, so the layer that installs them is built again when they change and not when anything else does.
|
|
10
20
|
*/
|
|
11
21
|
const PACKAGES_FILE = "fastagent.packages";
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
const PACKAGES_PATH = `${DELIVERED_CONFIG_DIR}/${PACKAGES_FILE}`;
|
|
23
|
+
/** Where a generated image has the instance configuration, read-only: the filesystem hierarchy's for configuration. */
|
|
24
|
+
const IMAGE_CONFIG_DIR = "/etc/fastagent";
|
|
25
|
+
/**
|
|
26
|
+
* The configuration a deploy delivers, with the public URL the host gives the box as its `http.publicUrl`, where the
|
|
27
|
+
* operator's configuration names none (a custom domain is theirs to name): the URL the box points its channels at.
|
|
28
|
+
*/
|
|
29
|
+
export function withPublicUrl(config, publicUrl) {
|
|
30
|
+
const values = (config === undefined ? {} : JSON.parse(config));
|
|
31
|
+
const own = ownPublicUrl(config);
|
|
32
|
+
if (own !== undefined)
|
|
33
|
+
return { config: config, publicUrl: own };
|
|
34
|
+
return { config: `${JSON.stringify({ ...values, http: { ...values.http, publicUrl } }, null, 2)}\n`, publicUrl };
|
|
35
|
+
}
|
|
36
|
+
/** The `http.publicUrl` an operator's configuration names, if it does. */
|
|
37
|
+
export function ownPublicUrl(config) {
|
|
38
|
+
return config === undefined ? undefined : JSON.parse(config).http?.publicUrl;
|
|
39
|
+
}
|
|
40
|
+
/** The delivered configuration as the artifact a deploy writes, to rewrite it once the host has given the URL. */
|
|
41
|
+
export function deliveredConfigArtifact(config) {
|
|
42
|
+
return { path: DELIVERED_CONFIG_FILE, content: config };
|
|
43
|
+
}
|
|
44
|
+
/** What every deploy writes whoever wrote it before: build output, by its path, never kept or reported stale. */
|
|
45
|
+
export const BUILD_OUTPUTS = [RELEASE_PATH, PACKAGES_PATH, DELIVERED_CONFIG_FILE];
|
|
14
46
|
/**
|
|
15
47
|
* The result line for a box CLI that refused this CLI's `login --stdio` as a usage error: an option it does not know
|
|
16
48
|
* (`--stdio` itself, or one added since, such as `--method`). See {@link boxLoginCommand}.
|
|
@@ -21,16 +53,17 @@ const STDIO_PREDATED = JSON.stringify({
|
|
|
21
53
|
reason: "failed",
|
|
22
54
|
message: BOX_CLI_PREDATES,
|
|
23
55
|
}).replaceAll('"', '\\"');
|
|
24
|
-
/** Where a generated image keeps its storage (`
|
|
56
|
+
/** Where a generated image keeps its storage (`FASTAGENT_ROOT`); every host mounts its volume here. */
|
|
25
57
|
const STORAGE_DIR = "/data";
|
|
26
|
-
/** Where a generated image holds the
|
|
27
|
-
export const
|
|
58
|
+
/** Where a generated image holds the agent: the build context, which is the agent directory. */
|
|
59
|
+
export const IMAGE_APP_DIR = "/app";
|
|
28
60
|
/**
|
|
29
61
|
* The shell command that runs `fastagent login --stdio <args>` inside a running generated image, as the server there
|
|
30
|
-
* would see it: in the
|
|
62
|
+
* would see it: in the image's agent directory, with the storage and credential paths resolved by the same defaults
|
|
31
63
|
* `prepareStartWorkspace` applies. A host's own shell does not reliably hand those over — its session may not start
|
|
32
64
|
* in the server's directory, and the platform's variables are the only ones it inherits. `storage` names the root
|
|
33
|
-
* outright for a shell that inherits none of them
|
|
65
|
+
* outright for a shell that inherits none of them; `program` is where the image has the agent, the generated one's
|
|
66
|
+
* by default. The CLI is the one the server runs: the agent's own install when
|
|
34
67
|
* it has one (under Bun when the image is Bun's), else the pinned global.
|
|
35
68
|
*
|
|
36
69
|
* That own install may predate `login --stdio` (an agent pinned to an older release, deployed by a newer CLI). It then
|
|
@@ -38,8 +71,8 @@ export const IMAGE_DEFINITION_DIR = "/app/definition";
|
|
|
38
71
|
* result line), so the command answers for it: one result line naming the mismatch, instead of a shell that ended
|
|
39
72
|
* with nothing to say. No quote or backtick in it: hosts wrap this whole line in `sh -c '…'`.
|
|
40
73
|
*/
|
|
41
|
-
export function boxLoginCommand(args, storage) {
|
|
42
|
-
for (const value of [...args, ...(storage ? [storage] : [])]) {
|
|
74
|
+
export function boxLoginCommand(args, { storage, program = IMAGE_APP_DIR } = {}) {
|
|
75
|
+
for (const value of [...args, ...(storage ? [storage] : []), program]) {
|
|
43
76
|
if (!/^[\w.@/-]+$/.test(value))
|
|
44
77
|
throw new Error(`cannot pass ${JSON.stringify(value)} to a shell on the box`);
|
|
45
78
|
}
|
|
@@ -47,10 +80,10 @@ export function boxLoginCommand(args, storage) {
|
|
|
47
80
|
...(storage
|
|
48
81
|
? [`root="${storage}"`, `export FASTAGENT_SECRETS_DIR="$root/.secrets"`]
|
|
49
82
|
: [
|
|
50
|
-
`root="\${
|
|
83
|
+
`root="\${FASTAGENT_ROOT:-${STORAGE_DIR}}"`,
|
|
51
84
|
`export FASTAGENT_SECRETS_DIR="\${FASTAGENT_SECRETS_DIR:-$root/.secrets}"`,
|
|
52
85
|
]),
|
|
53
|
-
`cd "$
|
|
86
|
+
`cd "${program}" || exit 1`,
|
|
54
87
|
`f=fastagent`,
|
|
55
88
|
`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`,
|
|
56
89
|
`$f login --stdio ${args.join(" ")}`,
|
|
@@ -85,9 +118,11 @@ function aptLayer(packages) {
|
|
|
85
118
|
}
|
|
86
119
|
/** mise's own directories for the build, thrown away with the layer: nothing of them is read at run time. */
|
|
87
120
|
const BUILD_MISE_DIR = "/tmp/mise";
|
|
121
|
+
/** Where the build holds the package list while it installs them: not the program's directory. */
|
|
122
|
+
const BUILD_PACKAGES = "/tmp/fastagent.packages";
|
|
88
123
|
/**
|
|
89
124
|
* The layer that installs the agent's packages, after its dependencies (the agent's own mise among them) and before the
|
|
90
|
-
* definition, so it stays cached while the definition changes. It reads them from {@link
|
|
125
|
+
* definition, so it stays cached while the definition changes. It reads them from {@link PACKAGES_PATH}, which deploy
|
|
91
126
|
* writes, rather than listing them: the Dockerfile is kept once written, and a list here would make every package
|
|
92
127
|
* change one that `--run` stops on. The packages go to mise's system directory (`--system`,
|
|
93
128
|
* /usr/local/share/mise/installs), which every mise reads beside its own data directory, so a start installs nothing.
|
|
@@ -101,10 +136,10 @@ function environmentLayer(dir) {
|
|
|
101
136
|
const steps = [
|
|
102
137
|
`export ${isolation} MISE_DATA_DIR=${BUILD_MISE_DIR} MISE_CACHE_DIR=${BUILD_MISE_DIR}/cache MISE_STATE_DIR=${BUILD_MISE_DIR}/state MISE="${mise}"`,
|
|
103
138
|
// One argument per line, read as it is: no quote or backslash in a package means anything to xargs.
|
|
104
|
-
`xargs -d '\\n' -a ${
|
|
139
|
+
`xargs -d '\\n' -a ${BUILD_PACKAGES} "$MISE" install --system`,
|
|
105
140
|
`rm -rf ${BUILD_MISE_DIR}`,
|
|
106
141
|
];
|
|
107
|
-
return `COPY ${
|
|
142
|
+
return `COPY ${PACKAGES_PATH} ${BUILD_PACKAGES}
|
|
108
143
|
RUN ${steps.join(" \\\n && ")}
|
|
109
144
|
`;
|
|
110
145
|
}
|
|
@@ -113,27 +148,26 @@ RUN ${steps.join(" \\\n && ")}
|
|
|
113
148
|
* (docker/run.ts, agentcore/run.ts): a copy cached before the floor's release would start a Node the CLI refuses.
|
|
114
149
|
*/
|
|
115
150
|
export const NODE_IMAGE = `node:${RUNTIME_FLOOR.node.split(".")[0]}-slim`;
|
|
151
|
+
/** Why the image builds `dist/` itself: an upload that follows `.gitignore` (`railway up`) drops it, and `start` needs it. */
|
|
152
|
+
const BUILD_STEP_NOTE = "# `start` opens dist/, which an upload that follows .gitignore leaves out: build it here.";
|
|
153
|
+
/**
|
|
154
|
+
* The instance configuration's layer, after the build: the directory
|
|
155
|
+
* the deploy wrote, read-only where the image's configuration goes, and named to `start`.
|
|
156
|
+
*/
|
|
157
|
+
const CONFIG_LAYER = `# The instance configuration \`deploy --config\` delivers ({} without one: the program's defaults).
|
|
158
|
+
COPY ${DELIVERED_CONFIG_DIR}/ ${IMAGE_CONFIG_DIR}/
|
|
159
|
+
ENV FASTAGENT_CONFIG=${IMAGE_CONFIG_DIR}/${INSTANCE_CONFIG_FILE}
|
|
160
|
+
`;
|
|
116
161
|
function dockerfile(input) {
|
|
117
|
-
// The build context is the agent directory, baked at
|
|
162
|
+
// The build context is the agent directory, baked at IMAGE_APP_DIR; deps install (and the local bin lives)
|
|
118
163
|
// there.
|
|
119
|
-
const dir =
|
|
164
|
+
const dir = IMAGE_APP_DIR;
|
|
120
165
|
const layoutNote = `The agent directory is baked at ${dir}`;
|
|
121
166
|
// apt layer right after FROM (cached across code changes): the agent's tools may shell out to git etc., which the
|
|
122
167
|
// slim image lacks.
|
|
123
168
|
const apt = aptLayer(input.apt);
|
|
124
169
|
// The caches stay off the volume: they are rebuildable, and a host mount is the slow disk.
|
|
125
|
-
const deployment = `ENV FASTAGENT_RELEASE_FILE=${
|
|
126
|
-
// No package.json → pure markdown/skills agent: install the pinned CLI GLOBALLY and run `fastagent` from PATH.
|
|
127
|
-
if (!input.hasPackageJson) {
|
|
128
|
-
return `${GENERATED_DOCKERFILE_MARKER}. ${layoutNote}
|
|
129
|
-
# npm-based (a markdown/skills agent installs the pinned CLI globally; ${NODE_IMAGE} has npm).
|
|
130
|
-
FROM ${NODE_IMAGE}
|
|
131
|
-
${apt}WORKDIR ${dir}
|
|
132
|
-
${deployment}RUN npm i -g @fastagent-sh/fastagent@${input.version}
|
|
133
|
-
COPY . .
|
|
134
|
-
CMD ["fastagent", "start", "${dir}"]
|
|
135
|
-
`;
|
|
136
|
-
}
|
|
170
|
+
const deployment = `ENV FASTAGENT_RELEASE_FILE=${IMAGE_CONFIG_DIR}/${RELEASE_FILE}\nENV FASTAGENT_ROOT=${STORAGE_DIR}\nENV npm_config_cache=/tmp/fastagent/npm\nENV BUN_INSTALL_CACHE_DIR=/tmp/fastagent/bun\n`;
|
|
137
171
|
const environment = input.packages && input.packages.length > 0 ? environmentLayer(dir) : "";
|
|
138
172
|
const isBun = input.runtime === "bun";
|
|
139
173
|
const base = isBun ? `oven/bun:${input.bunVersion ?? "1"}` : NODE_IMAGE;
|
|
@@ -156,7 +190,9 @@ ${deployment}`;
|
|
|
156
190
|
return `${head}COPY package.json bun.lock* *.tgz ./
|
|
157
191
|
RUN ${install}
|
|
158
192
|
${environment}COPY . .
|
|
159
|
-
|
|
193
|
+
${BUILD_STEP_NOTE}
|
|
194
|
+
RUN bun run fastagent build
|
|
195
|
+
${CONFIG_LAYER}CMD ["bun", "run", "fastagent", "start", "${dir}"]
|
|
160
196
|
`;
|
|
161
197
|
}
|
|
162
198
|
// `npm ci` requires a lockfile and hard-fails without one (a common `init --no-install` agent); fall back to `npm
|
|
@@ -165,7 +201,9 @@ CMD ["bun", "run", "fastagent", "start", "${dir}"]
|
|
|
165
201
|
return `${head}COPY package.json package-lock.json* *.tgz ./
|
|
166
202
|
RUN ${install}
|
|
167
203
|
${environment}COPY . .
|
|
168
|
-
|
|
204
|
+
${BUILD_STEP_NOTE}
|
|
205
|
+
RUN ./node_modules/.bin/fastagent build
|
|
206
|
+
${CONFIG_LAYER}CMD ["./node_modules/.bin/fastagent", "start", "${dir}"]
|
|
169
207
|
`;
|
|
170
208
|
}
|
|
171
209
|
/** Patterns are RECURSIVE (`**/`) on purpose. */
|
|
@@ -179,8 +217,12 @@ const DOCKERIGNORE_BASE = `${GENERATED_DOCKERIGNORE_MARKER}. Delete this line to
|
|
|
179
217
|
**/node_modules
|
|
180
218
|
**/${SECRETS_DIRNAME}/**
|
|
181
219
|
**/${STATE_DIRNAME}
|
|
220
|
+
/${INSTANCES_DIRNAME}
|
|
221
|
+
# what a deploy writes for the image: the delivered configuration and text, the release manifest, the package list
|
|
222
|
+
!/${DELIVERED_CONFIG_DIR}
|
|
182
223
|
/${CONTENT_DIRNAME}
|
|
183
|
-
|
|
224
|
+
# the project's own instance configuration: the development instance's, which a deploy does not carry
|
|
225
|
+
/${INSTANCE_CONFIG_FILE}
|
|
184
226
|
**/.cache
|
|
185
227
|
**/.env
|
|
186
228
|
**/.env.*
|
|
@@ -202,12 +244,12 @@ export function containerArtifacts(input) {
|
|
|
202
244
|
};
|
|
203
245
|
parseDeploymentRelease(JSON.stringify(release));
|
|
204
246
|
return [
|
|
205
|
-
{ path:
|
|
247
|
+
{ path: RELEASE_PATH, content: `${JSON.stringify(release, null, 2)}\n` },
|
|
248
|
+
// Written without a configuration too, as none: one an earlier deploy delivered would otherwise ship again.
|
|
249
|
+
{ path: DELIVERED_CONFIG_FILE, content: input.config ?? "{}\n" },
|
|
206
250
|
// Written without packages too, empty: a list an earlier deploy left would otherwise outlive the last package.
|
|
207
|
-
{ path:
|
|
251
|
+
{ path: PACKAGES_PATH, content: (input.packages ?? []).map((spec) => `${spec}\n`).join("") },
|
|
208
252
|
{ path: "Dockerfile", content: dockerfile(input) },
|
|
209
|
-
// Both names: BuildKit prefers the one beside the Dockerfile, the classic builder reads the context root's.
|
|
210
253
|
{ path: ".dockerignore", content: ignore },
|
|
211
|
-
{ path: "Dockerfile.dockerignore", content: ignore },
|
|
212
254
|
];
|
|
213
255
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InstanceConfigSource } from "../harnesses/pi-durable/config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Why the text `source` names cannot be delivered or would not open on the host, or undefined when it can and would:
|
|
4
|
+
* the open's own refusals (a path of the wrong kind, a name taken twice), and a path that is not strictly inside the
|
|
5
|
+
* configuration's directory, written relative to it, which the image could not reproduce.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deliveredTextProblem(agentDir: string, source: InstanceConfigSource): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Replace `.fastagent/deploy/` in `agentDir` with the text `source` names, whole: what an earlier deploy delivered and
|
|
10
|
+
* this one does not is gone, as the release manifest and the configuration are rewritten. The configuration's own file
|
|
11
|
+
* is written after, with the other build outputs. Answers how many paths were copied.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deliverText(agentDir: string, source: InstanceConfigSource | undefined): Promise<number>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The instance text a deploy delivers (agent-artifact.md §6.1): the files and directories the delivered configuration
|
|
3
|
+
* names, copied beside it into `.fastagent/deploy/` with their layout, so the paths the configuration wrote, relative to
|
|
4
|
+
* its file, resolve the same in the image's `/etc/fastagent/`. Only what is beside the configuration is delivered: a
|
|
5
|
+
* path elsewhere on this machine does not exist on the host.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
10
|
+
import { isUnderDir } from "../paths.js";
|
|
11
|
+
import { openText } from "../harnesses/pi-durable/text.js";
|
|
12
|
+
import { declareText } from "../harnesses/pi-durable/text-sources.js";
|
|
13
|
+
import { DELIVERED_CONFIG_DIR } from "./container.js";
|
|
14
|
+
/** What the configuration names beyond the program's own text, each kind with its sources. */
|
|
15
|
+
function declared(agentDir, source) {
|
|
16
|
+
const text = declareText(agentDir, source);
|
|
17
|
+
return ["instructions", "skills", "prompts"].flatMap((kind) => text[kind].filter((entry) => entry.declaredBy !== undefined).map((entry) => ({ kind, source: entry })));
|
|
18
|
+
}
|
|
19
|
+
/** The first symbolic link under `root` that points at nothing, which `cp` would fail on after the person's steps. */
|
|
20
|
+
function danglingLink(root) {
|
|
21
|
+
if (!statSync(root).isDirectory())
|
|
22
|
+
return undefined;
|
|
23
|
+
for (const entry of readdirSync(root, { recursive: true, withFileTypes: true })) {
|
|
24
|
+
const path = join(entry.parentPath, entry.name);
|
|
25
|
+
if (entry.isSymbolicLink() && !existsSync(path))
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Why the text `source` names cannot be delivered or would not open on the host, or undefined when it can and would:
|
|
32
|
+
* the open's own refusals (a path of the wrong kind, a name taken twice), and a path that is not strictly inside the
|
|
33
|
+
* configuration's directory, written relative to it, which the image could not reproduce.
|
|
34
|
+
*/
|
|
35
|
+
export function deliveredTextProblem(agentDir, source) {
|
|
36
|
+
try {
|
|
37
|
+
openText(agentDir, source);
|
|
38
|
+
for (const { kind, source: entry } of declared(agentDir, source)) {
|
|
39
|
+
const at = `${source.label}: ${kind} "${entry.given}"`;
|
|
40
|
+
const { directory } = source;
|
|
41
|
+
const given = entry.given ?? "";
|
|
42
|
+
if (directory === undefined || isAbsolute(given) || given === "~" || given.startsWith("~/")) {
|
|
43
|
+
return `${at} is not relative to the configuration: a deploy delivers what is beside it, so write a relative path`;
|
|
44
|
+
}
|
|
45
|
+
if (!isUnderDir(entry.path, directory) || entry.path === directory) {
|
|
46
|
+
return `${at} is outside the configuration's directory (${directory}): a deploy delivers only what is inside it`;
|
|
47
|
+
}
|
|
48
|
+
if (isUnderDir(agentDir, entry.path)) {
|
|
49
|
+
return `${at} contains the agent directory ${agentDir}: it cannot be delivered into it`;
|
|
50
|
+
}
|
|
51
|
+
const dangling = danglingLink(entry.path);
|
|
52
|
+
if (dangling !== undefined)
|
|
53
|
+
return `${at} holds a symbolic link to nothing: ${dangling}`;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return error instanceof Error ? error.message : String(error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Replace `.fastagent/deploy/` in `agentDir` with the text `source` names, whole: what an earlier deploy delivered and
|
|
63
|
+
* this one does not is gone, as the release manifest and the configuration are rewritten. The configuration's own file
|
|
64
|
+
* is written after, with the other build outputs. Answers how many paths were copied.
|
|
65
|
+
*/
|
|
66
|
+
export async function deliverText(agentDir, source) {
|
|
67
|
+
// Copying is an act, not a finding: whatever the pre-flight only warned about (a generate-only run) must not be
|
|
68
|
+
// carried out, or a path outside the configuration's directory would be written outside `.fastagent/deploy/`.
|
|
69
|
+
const problem = source === undefined ? undefined : deliveredTextProblem(agentDir, source);
|
|
70
|
+
if (problem !== undefined)
|
|
71
|
+
throw new Error(problem);
|
|
72
|
+
const target = join(agentDir, DELIVERED_CONFIG_DIR);
|
|
73
|
+
if (source?.directory !== undefined && isUnderDir(source.directory, target)) {
|
|
74
|
+
throw new Error(`${source.label} is inside ${target}, which every deploy replaces: keep the configuration elsewhere`);
|
|
75
|
+
}
|
|
76
|
+
await rm(target, { recursive: true, force: true });
|
|
77
|
+
if (source?.directory === undefined)
|
|
78
|
+
return 0;
|
|
79
|
+
const entries = declared(agentDir, source);
|
|
80
|
+
for (const { source: entry } of entries) {
|
|
81
|
+
const to = join(target, relative(source.directory, entry.path));
|
|
82
|
+
await mkdir(dirname(to), { recursive: true });
|
|
83
|
+
await cp(entry.path, to, { recursive: true, dereference: true });
|
|
84
|
+
}
|
|
85
|
+
return entries.length;
|
|
86
|
+
}
|
|
@@ -14,11 +14,8 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
14
14
|
boxLogin?: string;
|
|
15
15
|
/** Every declared channel and its ingress — the source of the ingress note. */
|
|
16
16
|
channels: readonly DeclaredChannel[];
|
|
17
|
-
/** Generate an optional Cloudflare Quick Tunnel service in Compose. */
|
|
18
|
-
tunnel: boolean;
|
|
19
17
|
/**
|
|
20
|
-
* The runbook's variable list (`deploymentSecrets`):
|
|
21
|
-
* carries.
|
|
18
|
+
* The runbook's variable list (`deploymentSecrets`): the declared names, each hinted by the file that declared it.
|
|
22
19
|
*/
|
|
23
20
|
secrets?: readonly DeploymentSecret[];
|
|
24
21
|
/** The value file as the pre-flight resolved it (it follows `FASTAGENT_SECRETS_DIR`), agent-dir-relative. */
|
|
@@ -35,13 +32,15 @@ export interface DockerPlan {
|
|
|
35
32
|
export declare const DOCKER_COMPOSE_FILE = "fastagent.compose.yml";
|
|
36
33
|
export declare const DOCKER_VALUE_FILE = ".secrets/production/.env";
|
|
37
34
|
export declare const MIN_DOCKER_COMPOSE_VERSION = "2.3.3";
|
|
38
|
-
/** Pinned: generated deployment artifacts must not silently move when Cloudflare publishes `latest`. */
|
|
39
|
-
export declare const CLOUDFLARED_IMAGE = "cloudflare/cloudflared:2026.7.1";
|
|
40
35
|
/** Whether a Compose file still opts into generated-drift warnings (drop the marker after taking ownership). */
|
|
41
36
|
export declare function isGeneratedCompose(content: string): boolean;
|
|
42
|
-
/** Whether an authoritative Compose file already carries the generated tunnel service contract. */
|
|
43
|
-
export declare function composeHasTunnelService(content: string): boolean;
|
|
44
37
|
/** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
|
|
45
38
|
export declare function toDockerProjectName(directoryName: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* What `--run` says of the webhook channels once Compose is up. Docker has no URL of its own: with `http.publicUrl`
|
|
41
|
+
* named in the delivered configuration the agent points its own channels there (its `/health` says how each went) and
|
|
42
|
+
* only Slack's Request URL is the operator's; without one, nothing can reach those paths yet.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ingressNotes(channels: readonly DeclaredChannel[], publicUrl: string | undefined, healthUrl: string | undefined): string[];
|
|
46
45
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
47
46
|
export declare function planDockerDeploy(input: DockerPlanInput): DockerPlan;
|