@fastagent-sh/fastagent 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +12 -11
- package/dist/channels/agentcore-service.js +14 -18
- package/dist/channels/agentcore.d.ts +11 -19
- package/dist/channels/agentcore.js +14 -7
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +239 -96
- package/dist/channels/discover.d.ts +26 -4
- package/dist/channels/discover.js +12 -23
- package/dist/channels/feishu/feishu-api.js +5 -8
- package/dist/channels/feishu/feishu.js +35 -18
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +22 -54
- package/dist/channels/slack/slack.js +13 -13
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +57 -69
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +4 -31
- package/dist/cli/commands/info.js +1 -2
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/start.js +31 -41
- package/dist/cli/serve.d.ts +12 -21
- package/dist/cli/serve.js +20 -25
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +51 -4
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +26 -22
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.js +20 -39
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +14 -2
- package/dist/engines/pi/create.js +2 -2
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +3 -4
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +1 -2
- package/dist/engines/pi/open.js +1 -1
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +6 -33
- package/dist/engines/pi/session-control.d.ts +19 -6
- package/dist/engines/pi/session-control.js +565 -470
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +4 -4
- package/dist/engines/pi/session-settings.js +7 -4
- package/dist/engines/pi/session-store.d.ts +83 -13
- package/dist/engines/pi/session-store.js +408 -55
- package/dist/engines/pi/tool-context.d.ts +21 -8
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.js +2 -2
- package/dist/env.js +1 -2
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +27 -2
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +1 -2
- package/dist/service.js +27 -5
- package/dist/session-remote.d.ts +7 -2
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +226 -92
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +1 -1
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* a caller-provided temp parameters file (`file://…`, mode 0600, deleted by the caller) — the write
|
|
15
15
|
* is injected to keep this module pure and the security-sensitive wiring testable.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
18
|
-
import type
|
|
17
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
18
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
19
19
|
import type { CliRunner } from "../runner.ts";
|
|
20
20
|
export interface AgentcoreRunPlan {
|
|
21
21
|
/** The base name — stack `fastagent-<name>`, ECR repo `fastagent/<name>`. */
|
|
@@ -35,7 +35,8 @@ export interface AgentcoreRunPlan {
|
|
|
35
35
|
secrets: Record<string, string>;
|
|
36
36
|
/** Required secret names with NO local value — gated before any side effect. */
|
|
37
37
|
missingSecrets: string[];
|
|
38
|
-
|
|
38
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
39
|
+
channels: readonly DeclaredChannel[];
|
|
39
40
|
/** Whether the topology includes the forwarder Lambda (route channels / schedules / selfSchedule).
|
|
40
41
|
* It owns the state snapshot's presigned URLs, so its absence means an invoke-only deployment
|
|
41
42
|
* with no cross-deploy state to keep. */
|
|
@@ -70,7 +71,7 @@ export declare function parseCheckpointReply(stdout: string): CheckpointReply |
|
|
|
70
71
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
71
72
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
72
73
|
*/
|
|
73
|
-
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>,
|
|
74
|
+
export declare function deployAgentcoreRun(plan: AgentcoreRunPlan, aws: CliRunner, docker: CliRunner, log: (msg: string) => void, writeSecretFile: (content: string) => Promise<string>, writeForwarderZip: (bytes: Uint8Array) => Promise<string>, registrars: Registrars,
|
|
74
75
|
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
75
76
|
probe?: {
|
|
76
77
|
fetchImpl?: typeof fetch;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerWebhooks } from "../channel-ingress.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { Buffer } from "node:buffer";
|
|
4
4
|
import { AUTH_SEED_CHUNK_SIZE, AUTH_SEED_MAX_CHUNKS, cfnParamName, forwarderSource, ingressSessionId, stateBucketName, } from "./plan.js";
|
|
@@ -123,7 +123,7 @@ export function parseCheckpointReply(stdout) {
|
|
|
123
123
|
* post-deploy webhook steps from the builder machine against the forwarder's Function URL. Every
|
|
124
124
|
* gate is fail-visible; `writeSecretFile` is the caller's 0600-temp-file seam (see the header).
|
|
125
125
|
*/
|
|
126
|
-
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip,
|
|
126
|
+
export async function deployAgentcoreRun(plan, aws, docker, log, writeSecretFile, writeForwarderZip, registrars,
|
|
127
127
|
/** Injected in tests; the probe itself stays inside the run so no deploy can skip it. */
|
|
128
128
|
probe = {}) {
|
|
129
129
|
const gate = (g) => ({ ok: false, gate: g });
|
|
@@ -457,40 +457,17 @@ probe = {}) {
|
|
|
457
457
|
}
|
|
458
458
|
// 9. Post-deploy webhook registration — same registrar seam as every host, pointed at the
|
|
459
459
|
// forwarder's Function URL. Gate policy is the shared registration-gate kernel.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
log
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (registerSlack) {
|
|
472
|
-
log("registering slack event URL…");
|
|
473
|
-
reg.track("slack", await registerSlack(url));
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
log(`slack: set Event Subscriptions → Request URL → ${url}/slack`);
|
|
477
|
-
reg.track("slack", "manual");
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
for (const kind of ["feishu", "lark"]) {
|
|
481
|
-
if (!plan.channels.includes(kind))
|
|
482
|
-
continue;
|
|
483
|
-
if (registerFeishu) {
|
|
484
|
-
log(`registering ${kind} event URL…`);
|
|
485
|
-
reg.track(kind, await registerFeishu(url, kind));
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
log(`${kind}: set the event Request URL (developer console → Events & Callbacks) → ${url}/${kind}`);
|
|
489
|
-
reg.track(kind, "manual");
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
const registrationGateMsg = reg.gate();
|
|
460
|
+
// No long-connection channels reach here: `deploy.ts` gates them before the driver runs (AgentCore
|
|
461
|
+
// has no resident process to hold a connection), so the deploy's channels are all webhook ones.
|
|
462
|
+
const registrationGateMsg = url
|
|
463
|
+
? await registerWebhooks({
|
|
464
|
+
baseUrl: url,
|
|
465
|
+
channels: plan.channels,
|
|
466
|
+
registrars,
|
|
467
|
+
log,
|
|
468
|
+
retryHint: "re-run to retry registration (steps already done are skipped)",
|
|
469
|
+
})
|
|
470
|
+
: undefined;
|
|
494
471
|
if (registrationGateMsg)
|
|
495
472
|
return gate(registrationGateMsg);
|
|
496
473
|
return { ok: true, runtimeArn, url };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW A RUNNING CHANNEL IS REACHED — the URL-neutral half of "point this channel at the agent": its
|
|
3
|
+
* default route, whether anything can set that URL end-to-end, and the words an operator needs when
|
|
4
|
+
* nothing can. Only the base URL varies, which is the argument every function here takes — a deploy
|
|
5
|
+
* host's (`<app>.fly.dev`, a minted Railway domain, a Function URL) or `dev --tunnel`'s Quick Tunnel.
|
|
6
|
+
* It lives under `deploy/` because that is where its answers are mostly consumed and where the gate
|
|
7
|
+
* policy it composes with lives; the serving path reads the same answers through `tunnel.ts`.
|
|
8
|
+
*
|
|
9
|
+
* It exists because that knowledge belongs to the CHANNEL and was written per HOST: three runbook
|
|
10
|
+
* plans, three `--run` drivers, a path table in the docker plan and the `--tunnel` announcer each
|
|
11
|
+
* hand-wrote the same five-branch if-chain. A rule spelled seven times drifts, and it did — the
|
|
12
|
+
* long-connection exception reached only the feishu/lark branches, so a long-connection Telegram
|
|
13
|
+
* deploy printed `setWebhook` in its runbook, which makes `getUpdates` return 409 and stops the
|
|
14
|
+
* channel the operator just deployed.
|
|
15
|
+
*
|
|
16
|
+
* Every function here takes the {@link DeclaredChannel} list and filters it ITSELF. Taking a
|
|
17
|
+
* pre-filtered list would put the rule back at the call sites, which is where it drifted from: the
|
|
18
|
+
* `--tunnel` announcer trusted its caller that way and had a default that passed every channel,
|
|
19
|
+
* long-connection ones included.
|
|
20
|
+
*
|
|
21
|
+
* A host adds its base URL and its own asides; it does not restate which channels have a webhook.
|
|
22
|
+
*/
|
|
23
|
+
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
24
|
+
import type { RegistrationOutcome } from "../channels/registration.ts";
|
|
25
|
+
import type { ChannelKind } from "../scaffold/add-channel.ts";
|
|
26
|
+
/** The registrars a host can drive. `telegram` is always available (fastagent holds the token and the
|
|
27
|
+
* URL); the others are optional so a caller without their credentials falls back to a manual step. */
|
|
28
|
+
export interface Registrars {
|
|
29
|
+
telegram: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
30
|
+
slack?: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
31
|
+
feishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
35
|
+
* knows how to instruct. ONE answer for every host and every path — the runbooks, the `--run`
|
|
36
|
+
* drivers, the docker ingress note, `--tunnel`. A custom channel is skipped here and reported by the
|
|
37
|
+
* pre-flight instead: its URL is its author's to set.
|
|
38
|
+
*/
|
|
39
|
+
export declare function webhookKinds(channels: readonly DeclaredChannel[]): ChannelKind[];
|
|
40
|
+
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
41
|
+
export declare function webhookPaths(channels: readonly DeclaredChannel[]): string[];
|
|
42
|
+
/** The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way
|
|
43
|
+
* (a literal `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in). */
|
|
44
|
+
export declare function webhookRunbook(baseUrl: string, channels: readonly DeclaredChannel[]): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. All channels
|
|
47
|
+
* are attempted — one failure does not skip the rest — and a channel whose registrar the caller did
|
|
48
|
+
* not wire reports `manual` with the operator's instruction. The two callers differ in what they do
|
|
49
|
+
* with the outcomes, not in how they are produced: a deploy gates on them, a long-running serve
|
|
50
|
+
* cannot ({@link registerWebhooks} is the gating half).
|
|
51
|
+
*/
|
|
52
|
+
export declare function pointChannelsAt(input: {
|
|
53
|
+
baseUrl: string;
|
|
54
|
+
channels: readonly DeclaredChannel[];
|
|
55
|
+
registrars: Registrars;
|
|
56
|
+
log: (msg: string) => void;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
kind: ChannelKind;
|
|
59
|
+
outcome: RegistrationOutcome;
|
|
60
|
+
}[]>;
|
|
61
|
+
/**
|
|
62
|
+
* {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS: an exit 0 claims the
|
|
63
|
+
* deployment is reachable, so a failed registration has to become a non-zero one. Returns the gate
|
|
64
|
+
* message, or undefined when nothing gates.
|
|
65
|
+
*/
|
|
66
|
+
export declare function registerWebhooks(input: {
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
channels: readonly DeclaredChannel[];
|
|
69
|
+
registrars: Registrars;
|
|
70
|
+
log: (msg: string) => void;
|
|
71
|
+
/** How THIS host retries — the only per-host words in the gate. */
|
|
72
|
+
retryHint: string;
|
|
73
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { registrationGate } from "./registration-gate.js";
|
|
2
|
+
const feishuCloud = (kind, label) => ({
|
|
3
|
+
path: `/${kind}`,
|
|
4
|
+
register: (r, baseUrl) => r.feishu?.(baseUrl, kind),
|
|
5
|
+
manual: (baseUrl) => `${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${baseUrl}/${kind} (the app must be running when you save)`,
|
|
6
|
+
runbook: (baseUrl) => [
|
|
7
|
+
`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`,
|
|
8
|
+
`# POST /${kind}; the app must be RUNNING when you save (the console verifies with a challenge):`,
|
|
9
|
+
`# Request URL = ${baseUrl}/${kind}`,
|
|
10
|
+
],
|
|
11
|
+
});
|
|
12
|
+
/** Declaration order — the order every runbook and every driver reports in. */
|
|
13
|
+
const INGRESS = {
|
|
14
|
+
telegram: {
|
|
15
|
+
path: "/telegram",
|
|
16
|
+
register: (r, baseUrl) => r.telegram(baseUrl),
|
|
17
|
+
manual: (baseUrl) => `telegram: set the webhook → ${baseUrl}/telegram (secret_token = TELEGRAM_SECRET_TOKEN)`,
|
|
18
|
+
runbook: (baseUrl) => [
|
|
19
|
+
`# Register the Telegram webhook. The path assumes the default route (POST /telegram); if you`,
|
|
20
|
+
`# remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`,
|
|
21
|
+
`curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`,
|
|
22
|
+
` -d url=${baseUrl}/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`,
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
github: {
|
|
26
|
+
path: "/webhook",
|
|
27
|
+
// The env-var name is the part a first-time operator cannot guess, and this line is all `--tunnel`
|
|
28
|
+
// prints — there is no runbook beside it to carry the detail.
|
|
29
|
+
manual: (baseUrl) => `github: set the webhook in the repo (Settings → Webhooks) → ${baseUrl}/webhook (content type application/json, secret = GITHUB_WEBHOOK_SECRET)`,
|
|
30
|
+
runbook: (baseUrl) => [
|
|
31
|
+
`# Set the GitHub webhook (repo Settings → Webhooks). Default route POST /webhook; if you remapped`,
|
|
32
|
+
`# it in channels/github.ts, use your path:`,
|
|
33
|
+
`# Payload URL = ${baseUrl}/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`,
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
slack: {
|
|
37
|
+
path: "/slack",
|
|
38
|
+
register: (r, baseUrl) => r.slack?.(baseUrl),
|
|
39
|
+
manual: (baseUrl) => `slack: set Event Subscriptions → Request URL → ${baseUrl}/slack`,
|
|
40
|
+
runbook: (baseUrl) => [
|
|
41
|
+
`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`,
|
|
42
|
+
`# answers Slack's challenge), and match scopes/subscriptions to channels/slack.ts groupBehavior:`,
|
|
43
|
+
`# Request URL = ${baseUrl}/slack`,
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
feishu: feishuCloud("feishu", "Feishu"),
|
|
47
|
+
lark: feishuCloud("lark", "Lark"),
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool
|
|
51
|
+
* knows how to instruct. ONE answer for every host and every path — the runbooks, the `--run`
|
|
52
|
+
* drivers, the docker ingress note, `--tunnel`. A custom channel is skipped here and reported by the
|
|
53
|
+
* pre-flight instead: its URL is its author's to set.
|
|
54
|
+
*/
|
|
55
|
+
export function webhookKinds(channels) {
|
|
56
|
+
const declared = new Set(channels.filter((c) => c.ingress === "webhook").map((c) => c.name));
|
|
57
|
+
return Object.keys(INGRESS).filter((kind) => declared.has(kind));
|
|
58
|
+
}
|
|
59
|
+
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
60
|
+
export function webhookPaths(channels) {
|
|
61
|
+
return webhookKinds(channels).map((kind) => INGRESS[kind].path);
|
|
62
|
+
}
|
|
63
|
+
/** The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way
|
|
64
|
+
* (a literal `https://app.fly.dev`, or a placeholder like `<your-domain>` the operator fills in). */
|
|
65
|
+
export function webhookRunbook(baseUrl, channels) {
|
|
66
|
+
return webhookKinds(channels).flatMap((kind) => INGRESS[kind].runbook(baseUrl));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Point every channel that has a webhook at `baseUrl`, reporting what each one ended as. All channels
|
|
70
|
+
* are attempted — one failure does not skip the rest — and a channel whose registrar the caller did
|
|
71
|
+
* not wire reports `manual` with the operator's instruction. The two callers differ in what they do
|
|
72
|
+
* with the outcomes, not in how they are produced: a deploy gates on them, a long-running serve
|
|
73
|
+
* cannot ({@link registerWebhooks} is the gating half).
|
|
74
|
+
*/
|
|
75
|
+
export async function pointChannelsAt(input) {
|
|
76
|
+
const outcomes = [];
|
|
77
|
+
for (const kind of webhookKinds(input.channels)) {
|
|
78
|
+
const ingress = INGRESS[kind];
|
|
79
|
+
// Calling IS the question: a channel whose registrar the caller did not wire returns undefined.
|
|
80
|
+
const running = ingress.register?.(input.registrars, input.baseUrl);
|
|
81
|
+
if (!running) {
|
|
82
|
+
input.log(ingress.manual(input.baseUrl));
|
|
83
|
+
outcomes.push({ kind, outcome: "manual" }); // a human's step — re-surfaced after registrar output
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
input.log(`registering ${kind} webhook…`);
|
|
87
|
+
outcomes.push({ kind, outcome: await running });
|
|
88
|
+
}
|
|
89
|
+
return outcomes;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS: an exit 0 claims the
|
|
93
|
+
* deployment is reachable, so a failed registration has to become a non-zero one. Returns the gate
|
|
94
|
+
* message, or undefined when nothing gates.
|
|
95
|
+
*/
|
|
96
|
+
export async function registerWebhooks(input) {
|
|
97
|
+
const reg = registrationGate(input.log, input.retryHint);
|
|
98
|
+
for (const { kind, outcome } of await pointChannelsAt(input))
|
|
99
|
+
reg.track(kind, outcome);
|
|
100
|
+
return reg.gate();
|
|
101
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* process, its loopback port, exact environment-variable names, and one persistent state volume.
|
|
5
5
|
* `--tunnel` can add an ephemeral Cloudflare Quick Tunnel service; durable ingress remains operator-owned.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
8
8
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
9
9
|
export interface DockerPlanInput extends ContainerInput {
|
|
10
10
|
/** Stable Compose project name, sanitized by {@link toDockerProjectName}. */
|
|
@@ -13,10 +13,8 @@ export interface DockerPlanInput extends ContainerInput {
|
|
|
13
13
|
port: number;
|
|
14
14
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
15
15
|
modelAuth: string | undefined;
|
|
16
|
-
/**
|
|
17
|
-
channels:
|
|
18
|
-
/** All long-connection channel basenames, including custom channels. */
|
|
19
|
-
longConnectionChannels?: string[];
|
|
16
|
+
/** Every declared channel and its ingress — the source of the secret list and the ingress note. */
|
|
17
|
+
channels: readonly DeclaredChannel[];
|
|
20
18
|
/** Generate an optional Cloudflare Quick Tunnel service in Compose. Generation only; `--run` starts it. */
|
|
21
19
|
tunnel: boolean;
|
|
22
20
|
/** Extra environment-variable names declared in config.deploy.secrets. */
|
|
@@ -39,7 +37,5 @@ export declare function isGeneratedCompose(content: string): boolean;
|
|
|
39
37
|
export declare function composeHasTunnelService(content: string): boolean;
|
|
40
38
|
/** Compose project names are lowercase [a-z0-9_-] and must start with an alphanumeric character. */
|
|
41
39
|
export declare function toDockerProjectName(directoryName: string): string;
|
|
42
|
-
/** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
|
|
43
|
-
export declare function dockerWebhookPaths(channels: ChannelKind[]): string[];
|
|
44
40
|
/** Compute local-Docker artifacts + the runbook; no Docker process is touched here. */
|
|
45
41
|
export declare function planDockerDeploy(input: DockerPlanInput): DockerPlan;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { webhookPaths } from "../channel-ingress.js";
|
|
1
2
|
import { containerArtifacts } from "../container.js";
|
|
2
3
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
4
|
const MOUNT = "/data";
|
|
@@ -24,23 +25,12 @@ export function toDockerProjectName(directoryName) {
|
|
|
24
25
|
.replace(/[-_]+$/g, "");
|
|
25
26
|
return `fastagent-${slug || "agent"}`;
|
|
26
27
|
}
|
|
27
|
-
/** Default first-party webhook paths. Workspace glue may remap them, so guidance labels them defaults. */
|
|
28
|
-
export function dockerWebhookPaths(channels) {
|
|
29
|
-
const path = {
|
|
30
|
-
github: "/webhook",
|
|
31
|
-
telegram: "/telegram",
|
|
32
|
-
slack: "/slack",
|
|
33
|
-
feishu: "/feishu",
|
|
34
|
-
lark: "/lark",
|
|
35
|
-
};
|
|
36
|
-
return channels.map((kind) => path[kind]);
|
|
37
|
-
}
|
|
38
28
|
/** `${NAME:-}` without making JavaScript treat it as interpolation. */
|
|
39
29
|
function composeInterpolation(name) {
|
|
40
30
|
return `\${${name}:-}`;
|
|
41
31
|
}
|
|
42
32
|
function composeYaml(input) {
|
|
43
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets
|
|
33
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
44
34
|
// Always leave the auth-seed seam in the committed topology. `--run` uses it for OAuth/stored auth;
|
|
45
35
|
// it is empty otherwise. Values never land in this file — Compose interpolates them at invocation.
|
|
46
36
|
const envNames = [...new Set([...secrets.map((secret) => secret.name), "FASTAGENT_AUTH_SEED"])];
|
|
@@ -100,11 +90,10 @@ export function planDockerDeploy(input) {
|
|
|
100
90
|
const composePath = `${input.agentPrefix}${DOCKER_COMPOSE_FILE}`;
|
|
101
91
|
const artifacts = [{ path: composePath, content: composeYaml(input) }, ...containerArtifacts(input)];
|
|
102
92
|
const compose = `docker compose -f ${composePath}`;
|
|
103
|
-
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets
|
|
93
|
+
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
104
94
|
const required = secrets.filter((secret) => secret.required);
|
|
105
95
|
const optional = secrets.filter((secret) => !secret.required);
|
|
106
|
-
const
|
|
107
|
-
const paths = dockerWebhookPaths(routeChannels);
|
|
96
|
+
const paths = webhookPaths(input.channels);
|
|
108
97
|
const runbook = [
|
|
109
98
|
`# Run FastAgent in local Docker. ${composePath} / Dockerfile(.dockerignore) are generated above.`,
|
|
110
99
|
`# Existing artifacts are authoritative and kept; edit Dockerfile or ${composePath} freely.`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RegistrationOutcome } from "../../channels/registration.ts";
|
|
1
2
|
import type { CliRunner } from "../runner.ts";
|
|
2
3
|
export interface DockerRunPlan {
|
|
3
4
|
/** Compose file relative to the runner cwd (the workspace root). */
|
|
@@ -10,6 +11,10 @@ export interface DockerRunPlan {
|
|
|
10
11
|
missingSecrets: string[];
|
|
11
12
|
/** Neither an env-key credential nor a readable auth.json is available. */
|
|
12
13
|
needsModelCredential: boolean;
|
|
14
|
+
/** Register the deployment's webhooks against the tunnel URL, reporting what each registrar
|
|
15
|
+
* answered. REQUIRED, not optional: an absent one would delete this whole step in silence, which
|
|
16
|
+
* is the failure this driver's gate exists to end. A topology with no tunnel never calls it. */
|
|
17
|
+
announce: DockerAnnounce;
|
|
13
18
|
/** `--tunnel` was requested for this run; a kept Compose file must actually contain that service. */
|
|
14
19
|
requireTunnel: boolean;
|
|
15
20
|
}
|
|
@@ -17,10 +22,21 @@ export type DockerRunOutcome = {
|
|
|
17
22
|
ok: true;
|
|
18
23
|
url?: string;
|
|
19
24
|
tunnelUrl?: string;
|
|
20
|
-
}
|
|
25
|
+
}
|
|
26
|
+
/** `url`/`tunnelUrl` travel with a gate too: Compose is up, so the operator still needs to know
|
|
27
|
+
* where it is and what to re-run. */
|
|
28
|
+
| {
|
|
21
29
|
ok: false;
|
|
22
30
|
gate: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
tunnelUrl?: string;
|
|
23
33
|
};
|
|
34
|
+
/** Register the deployment's webhooks against its public URL, reporting what each registrar
|
|
35
|
+
* answered. Injected so the driver stays free of channel specifics — and so a test can fail one. */
|
|
36
|
+
type DockerAnnounce = (baseUrl: string) => Promise<{
|
|
37
|
+
kind: string;
|
|
38
|
+
outcome: RegistrationOutcome;
|
|
39
|
+
}[]>;
|
|
24
40
|
export type DockerHealthProbe = (healthUrl: string) => Promise<boolean>;
|
|
25
41
|
export type DockerTunnelUrlProbe = (docker: CliRunner, composeFile: string, env: NodeJS.ProcessEnv) => Promise<string | undefined>;
|
|
26
42
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
@@ -38,3 +54,4 @@ export declare function waitForComposeTunnelUrl(docker: CliRunner, composeFile:
|
|
|
38
54
|
* service is still success (an operator-owned sidecar/reverse proxy may be its only ingress).
|
|
39
55
|
*/
|
|
40
56
|
export declare function deployDockerRun(plan: DockerRunPlan, docker: CliRunner, log: (message: string) => void, healthProbe?: DockerHealthProbe, tunnelUrlProbe?: DockerTunnelUrlProbe): Promise<DockerRunOutcome>;
|
|
57
|
+
export {};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { waitForHealth } from "../../channels/wait-health.js";
|
|
7
7
|
import { parseTunnelUrl } from "../../tunnel.js";
|
|
8
|
+
import { registrationGate } from "../registration-gate.js";
|
|
8
9
|
import { MIN_DOCKER_COMPOSE_VERSION } from "./plan.js";
|
|
9
10
|
/** Resolve Docker Compose's `host:port` output to a loopback URL (safe for 0.0.0.0/[::] bindings too). */
|
|
10
11
|
export function localUrlFromComposePort(stdout) {
|
|
@@ -122,5 +123,14 @@ export async function deployDockerRun(plan, docker, log, healthProbe = defaultHe
|
|
|
122
123
|
if (!tunnelUrl) {
|
|
123
124
|
return gate(`tunnel did not publish a Quick Tunnel URL — inspect \`docker compose -f ${plan.composeFile} logs tunnel\``);
|
|
124
125
|
}
|
|
126
|
+
// Registration lives HERE, like every other host's driver, and not at the CLI: this is the layer
|
|
127
|
+
// that owns the outcome, so it is the layer that can gate on one. While it sat above, docker was
|
|
128
|
+
// the one target whose `--run` could exit 0 with a webhook that never registered.
|
|
129
|
+
const reg = registrationGate(log, `re-run this deploy to retry registration (Compose is already up)`);
|
|
130
|
+
for (const { kind, outcome } of await plan.announce(tunnelUrl))
|
|
131
|
+
reg.track(kind, outcome);
|
|
132
|
+
const blocked = reg.gate();
|
|
133
|
+
if (blocked)
|
|
134
|
+
return { ok: false, gate: blocked, url, tunnelUrl };
|
|
125
135
|
return { ok: true, url, tunnelUrl };
|
|
126
136
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* snapshot is discarded replays on the next start (the Telegram L1 turn store) — at-least-once, the
|
|
17
17
|
* documented floor. State on the /data volume survives stop/suspend on the same machine.
|
|
18
18
|
*/
|
|
19
|
-
import type {
|
|
19
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
20
20
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
21
21
|
export interface FlyPlanInput extends ContainerInput {
|
|
22
22
|
/** Fly app name — globally unique, lowercase; the CLI sanitizes it from the dir basename. */
|
|
@@ -28,10 +28,9 @@ export interface FlyPlanInput extends ContainerInput {
|
|
|
28
28
|
* `"OAuth"`/`"stored credential"` (a local login the server can't use), or undefined (unconfigured).
|
|
29
29
|
*/
|
|
30
30
|
modelAuth: string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
longConnectionChannels?: string[];
|
|
31
|
+
/** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
|
|
32
|
+
* whether a machine must stay up for an outbound connection. */
|
|
33
|
+
channels: readonly DeclaredChannel[];
|
|
35
34
|
/** Extra secret env-var names (fastagent.config deploy.secrets) — added to the runbook's secret list. */
|
|
36
35
|
extraSecrets?: string[];
|
|
37
36
|
/** `auto_stop_machines` — `"suspend"` (default, fast resume) or `"stop"` (cold start). CLI `--stop`. */
|
package/dist/deploy/fly/plan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { webhookRunbook } from "../channel-ingress.js";
|
|
1
2
|
import { containerArtifacts } from "../container.js";
|
|
2
3
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
3
4
|
function flyToml(appName, port, hasGithub, autostop, scaleToZero, hasTimeTriggers, hasLongConnectionChannel) {
|
|
@@ -62,7 +63,7 @@ export function planFlyDeploy(input) {
|
|
|
62
63
|
const artifacts = [
|
|
63
64
|
{
|
|
64
65
|
path: flyTomlPath,
|
|
65
|
-
content: flyToml(appName, port, channels.
|
|
66
|
+
content: flyToml(appName, port, channels.some((channel) => channel.name === "github"), input.autostop, input.scaleToZero, input.hasTimeTriggers, channels.some((channel) => channel.ingress === "long-connection")),
|
|
66
67
|
},
|
|
67
68
|
...containerArtifacts(input),
|
|
68
69
|
];
|
|
@@ -70,7 +71,7 @@ export function planFlyDeploy(input) {
|
|
|
70
71
|
// model key (when local auth is an env key) + every discovered channel's secrets. Names + hints as
|
|
71
72
|
// COMMENT lines (a `#` inside a `\`-continued command would break the shell), then one flat, executable
|
|
72
73
|
// `fly secrets set` the coding agent fills — `<value>` placeholders, never inline comments.
|
|
73
|
-
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets
|
|
74
|
+
const secrets = deploymentSecrets(modelAuth, channels, input.extraSecrets);
|
|
74
75
|
const requiredSecrets = secrets.filter((secret) => secret.required);
|
|
75
76
|
const optionalSecrets = secrets.filter((secret) => !secret.required);
|
|
76
77
|
const deployCmd = `fly deploy . --config ${flyTomlPath} --dockerfile ${input.agentPrefix}Dockerfile --app ${appName}`;
|
|
@@ -110,26 +111,10 @@ export function planFlyDeploy(input) {
|
|
|
110
111
|
? `# Model auth: none found at the local auth path — a global \`fastagent login\` isn't read here; pass --auth-path <file> (e.g. ~/.fastagent/.secrets/auth.json), or \`--run\` carries it automatically.`
|
|
111
112
|
: `# Model auth: your local auth is "${modelAuth}" — the plan can't read its value to set as a secret.`, `# Set your provider API key as a Fly secret (fly secrets set KEY=...), OR place auth.json at /data/.secrets/ on the volume.`);
|
|
112
113
|
}
|
|
113
|
-
// The fastagent-only post-step: point each channel
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const post = [];
|
|
118
|
-
if (channels.includes("telegram")) {
|
|
119
|
-
post.push(`# After deploy — register the Telegram webhook. The path assumes the default route (POST /telegram);`, `# if you remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`, `curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \\`, ` -d url=https://${appName}.fly.dev/telegram -d secret_token=<TELEGRAM_SECRET_TOKEN>`);
|
|
120
|
-
}
|
|
121
|
-
if (channels.includes("github")) {
|
|
122
|
-
post.push(`# After deploy — set the GitHub webhook (repo Settings → Webhooks). Path assumes the default route`, `# (POST /webhook); if you remapped it in channels/github.ts, use your path:`, `# Payload URL = https://${appName}.fly.dev/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
123
|
-
}
|
|
124
|
-
if (channels.includes("slack")) {
|
|
125
|
-
post.push(`# After deploy — set Slack Event Subscriptions → Request URL. Path assumes POST /slack;`, `# Slack verifies the running endpoint with a challenge:`, `# Request URL = https://${appName}.fly.dev/slack`, `# Ensure OAuth scopes + message.* subscriptions match the groupBehavior in channels/slack.ts.`);
|
|
126
|
-
}
|
|
127
|
-
for (const kind of ["feishu", "lark"]) {
|
|
128
|
-
if (!channels.includes(kind) || input.longConnectionChannels?.includes(kind))
|
|
129
|
-
continue;
|
|
130
|
-
const label = kind === "feishu" ? "Feishu" : "Lark";
|
|
131
|
-
post.push(`# After deploy — set the ${label} event Request URL (developer console → Events & Callbacks).`, `# Path assumes the default route (POST /${kind}); the app must be RUNNING when you save (the console`, `# verifies the URL with a challenge):`, `# Request URL = https://${appName}.fly.dev/${kind}`);
|
|
132
|
-
}
|
|
114
|
+
// The fastagent-only post-step: point each channel at the live URL. WHICH channels and in what words
|
|
115
|
+
// is the shared channel-ingress kernel's answer; Fly's contribution is that its URL is deterministic.
|
|
116
|
+
const steps = webhookRunbook(`https://${appName}.fly.dev`, channels);
|
|
117
|
+
const post = steps.length > 0 ? [`# After deploy:`, ...steps] : [];
|
|
133
118
|
if (post.length > 0)
|
|
134
119
|
runbook.push(``, ...post);
|
|
135
120
|
// Single-machine tier: state lives on ONE volume tied to ONE machine. Scaling to multiple machines
|
package/dist/deploy/fly/run.d.ts
CHANGED
|
@@ -17,23 +17,9 @@
|
|
|
17
17
|
* (one machine, the single-machine tier). Secrets go in via `secrets import` over stdin, so values
|
|
18
18
|
* never land in argv/process listings.
|
|
19
19
|
*/
|
|
20
|
-
import type
|
|
21
|
-
import type {
|
|
20
|
+
import { type Registrars } from "../channel-ingress.ts";
|
|
21
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
22
22
|
import type { CliRunner } from "../runner.ts";
|
|
23
|
-
/**
|
|
24
|
-
* The bytes to seed to the auth file, or undefined to leave it alone — the pure core of `start`'s
|
|
25
|
-
* FASTAGENT_AUTH_SEED materialization. ABSENT-ONLY by design: a present file (a refreshed volume copy)
|
|
26
|
-
* is never overwritten by the stale seed, so a box that ran its own OAuth refresh is not rolled back.
|
|
27
|
-
*/
|
|
28
|
-
export declare function authSeedBytes(seed: string | undefined, fileExists: boolean): Buffer | undefined;
|
|
29
|
-
/**
|
|
30
|
-
* Collect the (possibly CHUNKED) auth seed from the environment: `FASTAGENT_AUTH_SEED` plus numbered
|
|
31
|
-
* continuations (`_2`, `_3`, …) concatenated in order. Hosts whose env values carry a small max
|
|
32
|
-
* length (AgentCore: 2048 chars — a real OAuth auth.json's base64 exceeds it) split the seed across
|
|
33
|
-
* them at deploy time; single-var hosts (Fly/Railway) never set a continuation and are unchanged.
|
|
34
|
-
* Collection stops at the first absent/empty continuation — the writer fills them contiguously.
|
|
35
|
-
*/
|
|
36
|
-
export declare function collectAuthSeed(env: NodeJS.ProcessEnv): string | undefined;
|
|
37
23
|
export interface FlyRunPlan {
|
|
38
24
|
appName: string;
|
|
39
25
|
region: string;
|
|
@@ -42,8 +28,8 @@ export interface FlyRunPlan {
|
|
|
42
28
|
secrets: Record<string, string>;
|
|
43
29
|
/** Required secret names with NO local value — the run gates on these before any side effect. */
|
|
44
30
|
missingSecrets: string[];
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
/** Every declared channel and its ingress — the driver asks which of them have a webhook. */
|
|
32
|
+
channels: readonly DeclaredChannel[];
|
|
47
33
|
/** fly.toml path passed to `fly deploy -c` (relative to the run cwd = the workspace root). */
|
|
48
34
|
flyConfig: string;
|
|
49
35
|
/** Dockerfile path passed explicitly (`fastagent/Dockerfile`, with the workspace as context —
|
|
@@ -58,9 +44,9 @@ export type FlyRunOutcome = {
|
|
|
58
44
|
gate: string;
|
|
59
45
|
};
|
|
60
46
|
/**
|
|
61
|
-
* Run the deploy through `fly`. `log` reports progress; the injected
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
47
|
+
* Run the deploy through `fly`. `log` reports progress; the injected {@link Registrars} perform the
|
|
48
|
+
* post-deploy webhook steps from the builder machine (Slack's control credential never travels to the
|
|
49
|
+
* host). A registrar the caller did not wire becomes the printed manual step. Every gate is
|
|
50
|
+
* fail-visible.
|
|
65
51
|
*/
|
|
66
|
-
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void,
|
|
52
|
+
export declare function deployFlyRun(plan: FlyRunPlan, fly: CliRunner, log: (msg: string) => void, registrars: Registrars): Promise<FlyRunOutcome>;
|