@fastagent-sh/fastagent 0.15.0 → 0.16.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 +14 -4
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +266 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +5 -0
- package/dist/channels/context-buffer.js +5 -0
- package/dist/channels/feishu/context-buffer.d.ts +13 -8
- package/dist/channels/feishu/context-buffer.js +60 -6
- package/dist/channels/feishu/feishu-api.js +4 -1
- package/dist/channels/feishu/feishu.d.ts +0 -12
- package/dist/channels/feishu/feishu.js +143 -54
- package/dist/channels/feishu/invoke-turn.js +35 -18
- package/dist/channels/feishu/model.d.ts +0 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +21 -7
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.js +3 -2
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +30 -1
- package/dist/channels/feishu/setup-mode.js +27 -1
- package/dist/channels/github/github.js +8 -1
- package/dist/channels/http.js +1 -1
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +7 -1
- package/dist/channels/preview-kit.js +3 -2
- package/dist/channels/slack/parse.d.ts +16 -1
- package/dist/channels/slack/parse.js +46 -3
- package/dist/channels/slack/preview.d.ts +1 -2
- package/dist/channels/slack/preview.js +68 -24
- package/dist/channels/slack/scaffold/channel.ts +5 -5
- package/dist/channels/slack/slack-api.d.ts +3 -23
- package/dist/channels/slack/slack-api.js +6 -22
- package/dist/channels/slack/slack.d.ts +13 -20
- package/dist/channels/slack/slack.js +95 -50
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +19 -12
- package/dist/channels/tasks.d.ts +0 -6
- package/dist/channels/tasks.js +16 -1
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +1 -1
- package/dist/channels/text.d.ts +14 -0
- package/dist/channels/text.js +14 -0
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +7 -0
- package/dist/cli/add-feishu.d.ts +7 -4
- package/dist/cli/add-feishu.js +57 -37
- package/dist/cli/add-slack.d.ts +2 -1
- package/dist/cli/add-slack.js +6 -11
- package/dist/cli/commands/add.js +50 -51
- package/dist/cli/commands/attach.js +8 -4
- package/dist/cli/commands/chat.js +8 -8
- package/dist/cli/commands/deploy.d.ts +14 -1
- package/dist/cli/commands/deploy.js +330 -78
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +38 -25
- package/dist/cli/commands/fire.js +15 -16
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +9 -6
- package/dist/cli/commands/login.js +35 -21
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +101 -37
- package/dist/cli/commands/tool.js +30 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +66 -36
- package/dist/cli/serve.d.ts +52 -8
- package/dist/cli/serve.js +133 -32
- package/dist/cli/shared.d.ts +21 -2
- package/dist/cli/shared.js +44 -18
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +1 -1
- package/dist/deploy/docker/plan.js +12 -17
- package/dist/deploy/fly/plan.d.ts +2 -0
- package/dist/deploy/fly/plan.js +27 -19
- package/dist/deploy/fly/run.d.ts +12 -1
- package/dist/deploy/fly/run.js +36 -2
- package/dist/deploy/preflight.d.ts +11 -5
- package/dist/deploy/preflight.js +235 -65
- package/dist/deploy/railway/plan.d.ts +7 -0
- package/dist/deploy/railway/plan.js +41 -16
- package/dist/deploy/railway/run.d.ts +8 -1
- package/dist/deploy/railway/run.js +7 -2
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +9 -3
- package/dist/dev-supervisor.d.ts +11 -8
- package/dist/dev-supervisor.js +53 -51
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +1 -1
- package/dist/engines/pi/channel.js +5 -5
- package/dist/engines/pi/chat.js +2 -2
- package/dist/engines/pi/config.d.ts +12 -48
- package/dist/engines/pi/config.js +29 -110
- package/dist/engines/pi/create.d.ts +32 -24
- package/dist/engines/pi/create.js +47 -23
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- package/dist/engines/pi/login.d.ts +1 -1
- package/dist/engines/pi/models.d.ts +3 -3
- package/dist/engines/pi/models.js +1 -1
- package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +18 -13
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/tool.js +4 -0
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +4 -2
- package/dist/host/node.js +2 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +2 -2
- package/dist/pi.js +2 -2
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +7 -3
- package/dist/scaffold/add-channel.js +55 -29
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +1 -1
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -0
- package/dist/schedule/wakeups.js +18 -0
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.js +7 -7
- package/package.json +7 -4
- package/dist/channels/feishu/owned-threads.d.ts +0 -7
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/slack/owned-threads.d.ts +0 -6
- package/dist/channels/slack/owned-threads.js +0 -43
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
package/dist/cli/serve.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Agent } from "../agent.ts";
|
|
2
2
|
import { type LoadedLongConnectionChannel } from "../engines/pi/channel.ts";
|
|
3
3
|
import { type Routes } from "../host/node.ts";
|
|
4
|
+
import type { LoadedSchedule } from "../schedule/schedule.ts";
|
|
4
5
|
import type { SessionControl } from "../session.ts";
|
|
5
6
|
export interface ServingSurface {
|
|
6
7
|
routes: Routes;
|
|
@@ -15,9 +16,11 @@ export interface ServingSurface {
|
|
|
15
16
|
* The surface this deployment serves: default `GET /health` plus discovered channels, or the default
|
|
16
17
|
* POST `/invoke` only when neither a route nor a long-connection channel was declared.
|
|
17
18
|
*/
|
|
18
|
-
export declare function routesFor(
|
|
19
|
+
export declare function routesFor(agentDir: string, agent: Agent, stateRoot: string, control?: SessionControl, options?: {
|
|
20
|
+
builtinInvoke?: boolean;
|
|
21
|
+
}): Promise<ServingSurface>;
|
|
19
22
|
/**
|
|
20
|
-
* Mount the session control plane (`/control/*`) when the
|
|
23
|
+
* Mount the session control plane (`/control/*`) when the agent enabled it
|
|
21
24
|
* (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
|
|
22
25
|
* writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
|
|
23
26
|
* the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
|
|
@@ -29,20 +32,61 @@ export declare function routesFor(workspaceDir: string, agent: Agent, stateRoot:
|
|
|
29
32
|
export declare function mountSessionControl(routes: Routes, control: SessionControl | undefined, stateRoot: string, options?: {
|
|
30
33
|
tunnel?: boolean;
|
|
31
34
|
agent?: Agent;
|
|
35
|
+
host?: string;
|
|
32
36
|
}): {
|
|
33
37
|
routes: Routes;
|
|
34
38
|
announce: (boundPort: number) => void;
|
|
35
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
|
|
42
|
+
* the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
|
|
43
|
+
* `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
|
|
44
|
+
* on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
|
|
45
|
+
* the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mountAgentcore(routes: Routes, options: {
|
|
48
|
+
agent: Agent;
|
|
49
|
+
stateRoot: string;
|
|
50
|
+
schedules: LoadedSchedule[];
|
|
51
|
+
onStateReady?: () => void;
|
|
52
|
+
}): Routes;
|
|
53
|
+
/**
|
|
54
|
+
* Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
|
|
55
|
+
* (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
|
|
56
|
+
* `127.0.0.2` bind, loopback though it is — would leave the tunnel up and 502ing every request.
|
|
57
|
+
* Checked BEFORE the bind (the flag alone pre-spawn in `dev`, again once config is loaded, since
|
|
58
|
+
* `http.host` can carry the address), so the failure is clean rather than a live-but-broken public URL.
|
|
59
|
+
* `source` decides the exit code, per fail.ts: a flag COMBINATION is a usage error (2), a value that
|
|
60
|
+
* came from config is broken runtime configuration (1).
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
|
|
63
|
+
/**
|
|
64
|
+
* The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
|
|
65
|
+
* ONE function because they are one message — they were two, and `--bind` updated the first while the
|
|
66
|
+
* second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
|
|
67
|
+
* neither can be changed without the other in view, and the address has a single derivation.
|
|
68
|
+
*
|
|
69
|
+
* A wildcard bind is every interface, and naming one address there would understate it — but the curl
|
|
70
|
+
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
71
|
+
*/
|
|
72
|
+
export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
|
|
36
73
|
/**
|
|
37
74
|
* Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
|
|
38
75
|
* adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
|
|
39
|
-
* the sole clean-shutdown command.
|
|
76
|
+
* the sole clean-shutdown command. `host` unset binds all interfaces.
|
|
40
77
|
*/
|
|
41
|
-
export declare function serve(surface: ServingSurface,
|
|
78
|
+
export declare function serve(surface: ServingSurface, bind: {
|
|
79
|
+
port: number;
|
|
80
|
+
host?: string;
|
|
81
|
+
}, onListening?: (boundPort: number) => void): void;
|
|
42
82
|
/** Start a Cloudflare tunnel for route channels only. */
|
|
43
|
-
export declare function maybeTunnel(
|
|
83
|
+
export declare function maybeTunnel(agentDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
|
|
44
84
|
/**
|
|
45
|
-
* Load and start the
|
|
46
|
-
* there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals.
|
|
85
|
+
* Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
|
|
86
|
+
* there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
|
|
87
|
+
* loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
|
|
88
|
+
* ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
|
|
47
89
|
*/
|
|
48
|
-
export declare function startSchedules(
|
|
90
|
+
export declare function startSchedules(agentDir: string, agent: Agent, stateRoot: string, selfSchedule: boolean, options?: {
|
|
91
|
+
externalClock?: boolean;
|
|
92
|
+
}): Promise<LoadedSchedule[]>;
|
package/dist/cli/serve.js
CHANGED
|
@@ -4,24 +4,28 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { createStateSync } from "../channels/agentcore-state.js";
|
|
8
|
+
import { agentcoreRoutes, UnknownScheduleError } from "../channels/agentcore.js";
|
|
9
|
+
import { activeWork } from "../channels/busy.js";
|
|
7
10
|
import { controlRoutes } from "../channels/control.js";
|
|
8
11
|
import { INVOKE_EXAMPLE_BODY, createInvokeHandler } from "../channels/http.js";
|
|
9
12
|
import { text } from "../channels/respond.js";
|
|
10
13
|
import { loadChannels } from "../engines/pi/channel.js";
|
|
11
14
|
import { reportModuleLoadFailures } from "../engines/pi/report.js";
|
|
15
|
+
import { answersLocalhost, bindLabel, classifyBind, clientHost } from "../bind.js";
|
|
12
16
|
import { parseRouteKey, router, serveNode } from "../host/node.js";
|
|
13
17
|
import { log } from "../log.js";
|
|
14
18
|
import { openExternalUrl } from "../open-url.js";
|
|
15
19
|
import { loadSchedules } from "../schedule/discover.js";
|
|
16
|
-
import { createScheduler } from "../schedule/scheduler.js";
|
|
20
|
+
import { createScheduler, fireScheduleOnce } from "../schedule/scheduler.js";
|
|
17
21
|
import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
|
|
18
|
-
import { failStartup } from "./fail.js";
|
|
22
|
+
import { failStartup, failUsage } from "./fail.js";
|
|
19
23
|
/**
|
|
20
24
|
* The surface this deployment serves: default `GET /health` plus discovered channels, or the default
|
|
21
25
|
* POST `/invoke` only when neither a route nor a long-connection channel was declared.
|
|
22
26
|
*/
|
|
23
|
-
export async function routesFor(
|
|
24
|
-
const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(
|
|
27
|
+
export async function routesFor(agentDir, agent, stateRoot, control, options = {}) {
|
|
28
|
+
const { routes, longConnections, routeChannels, collisions, failures } = await loadChannels(agentDir, {
|
|
25
29
|
agent,
|
|
26
30
|
stateRoot,
|
|
27
31
|
control,
|
|
@@ -34,7 +38,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
|
|
|
34
38
|
throw new Error(`channel setup is invalid (${failures.length} load failure(s), ${collisions.length} route collision(s)) — ` +
|
|
35
39
|
`fix it, or rename an intentionally disabled file to *.disabled`);
|
|
36
40
|
}
|
|
37
|
-
const builtinInvoke = Object.keys(routes).length === 0 && longConnections.length === 0;
|
|
41
|
+
const builtinInvoke = options.builtinInvoke !== false && Object.keys(routes).length === 0 && longConnections.length === 0;
|
|
38
42
|
const channels = builtinInvoke ? { "POST /invoke": createInvokeHandler(agent) } : routes;
|
|
39
43
|
const healthCovered = Object.keys(channels).some((key) => {
|
|
40
44
|
const entry = parseRouteKey(key);
|
|
@@ -53,7 +57,7 @@ export async function routesFor(workspaceDir, agent, stateRoot, control) {
|
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
/**
|
|
56
|
-
* Mount the session control plane (`/control/*`) when the
|
|
60
|
+
* Mount the session control plane (`/control/*`) when the agent enabled it
|
|
57
61
|
* (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
|
|
58
62
|
* writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
|
|
59
63
|
* the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
|
|
@@ -87,15 +91,22 @@ export function mountSessionControl(routes, control, stateRoot, options = {}) {
|
|
|
87
91
|
// Atomic (tmp+rename, the state.ts pattern): attach re-reads this file exactly during the
|
|
88
92
|
// restart window — a torn read would be misdiagnosed as "serve gone".
|
|
89
93
|
const tmp = `${path}.tmp`;
|
|
90
|
-
writeFileSync(tmp, `${JSON.stringify({ url: `http
|
|
94
|
+
writeFileSync(tmp, `${JSON.stringify({ url: `http://${clientHost(options.host)}:${boundPort}`, token })}\n`, {
|
|
95
|
+
mode: 0o600,
|
|
96
|
+
});
|
|
91
97
|
chmodSync(tmp, 0o600); // an existing file keeps its old mode on rewrite — pin it
|
|
92
98
|
renameSync(tmp, path);
|
|
93
99
|
log.info(`[fastagent] session control on /control/* (token in ${path})`);
|
|
94
|
-
// The serve binds ALL interfaces (containers require it), so /control/* is
|
|
95
|
-
// with the bearer token as the only protection — the tunnel and deploy paths warn
|
|
96
|
-
// and the LAN path must not be the silent third way past the local trust story.
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
// The serve binds ALL interfaces by DEFAULT (containers require it), so /control/* is
|
|
101
|
+
// LAN-reachable with the bearer token as the only protection — the tunnel and deploy paths warn
|
|
102
|
+
// loudly, and the LAN path must not be the silent third way past the local trust story. A
|
|
103
|
+
// loopback bind closes exactly that reach, so it earns silence.
|
|
104
|
+
const bind = classifyBind(options.host);
|
|
105
|
+
if (bind !== "loopback") {
|
|
106
|
+
log.warn(`[fastagent] the port binds ${bind === "wildcard" ? "all interfaces" : `${options.host} (off this machine)`}: ` +
|
|
107
|
+
"/control/* is reachable on your LAN, protected only by the bearer token — bind loopback " +
|
|
108
|
+
"(--bind 127.0.0.1), firewall the port, or wrap it for real exposure (docs: design §14)");
|
|
109
|
+
}
|
|
99
110
|
if (options.tunnel) {
|
|
100
111
|
// Local trust = the token + its file permissions; --tunnel takes the whole port PUBLIC
|
|
101
112
|
// (beyond even the LAN reach the mount already warned about). The operator asked for the tunnel (webhooks), but must not DISCOVER the control
|
|
@@ -132,13 +143,95 @@ export function mountSessionControl(routes, control, stateRoot, options = {}) {
|
|
|
132
143
|
},
|
|
133
144
|
};
|
|
134
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Mount the AgentCore Runtime adapter (`POST /invocations` + `GET /ping`) over the serving routes —
|
|
148
|
+
* the deployed container's ONLY reachable surface (channels/agentcore.ts). Wired by `start` when
|
|
149
|
+
* `FASTAGENT_AGENTCORE=1` (set by the generated deploy artifacts, never by hand). A channel colliding
|
|
150
|
+
* on either path fails startup, same disposition as the control-plane mount: the adapter's paths are
|
|
151
|
+
* the platform's contract, so a channel shadowing them would silently unserve the whole deployment.
|
|
152
|
+
*/
|
|
153
|
+
export function mountAgentcore(routes, options) {
|
|
154
|
+
const { agent, stateRoot, schedules, onStateReady } = options;
|
|
155
|
+
const mounted = agentcoreRoutes({
|
|
156
|
+
routes,
|
|
157
|
+
agent,
|
|
158
|
+
stateRoot,
|
|
159
|
+
isBusy: () => activeWork() > 0,
|
|
160
|
+
// Cross-deploy durability: AgentCore wipes the state mount on every runtime version update, so
|
|
161
|
+
// the state root is restored from (and pushed to) an S3 snapshot through presigned URLs the
|
|
162
|
+
// forwarder mints per envelope. Always wired on this path — the platform gives no other way to
|
|
163
|
+
// keep an agent's memory across a deploy.
|
|
164
|
+
stateSync: createStateSync({ stateRoot }),
|
|
165
|
+
// What separates a forwarder envelope from any IAM principal's InvokeAgentRuntime call. Absent =
|
|
166
|
+
// no forwarder in this topology, so only the public `invoke` kind is servable.
|
|
167
|
+
ingressSecret: process.env.FASTAGENT_INGRESS_SECRET,
|
|
168
|
+
onStateReady,
|
|
169
|
+
fire: schedules.length === 0
|
|
170
|
+
? undefined
|
|
171
|
+
: (name, slot) => {
|
|
172
|
+
const schedule = schedules.find((s) => s.name === name);
|
|
173
|
+
if (!schedule)
|
|
174
|
+
throw new UnknownScheduleError(name);
|
|
175
|
+
return fireScheduleOnce({ agent, stateRoot, schedule, slot });
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const mountedPaths = new Set(Object.keys(mounted).map((key) => parseRouteKey(key).path));
|
|
179
|
+
const collisions = Object.keys(routes).filter((key) => mountedPaths.has(parseRouteKey(key).path));
|
|
180
|
+
if (collisions.length > 0) {
|
|
181
|
+
throw new Error(`channel route(s) ${collisions.map((key) => `"${key}"`).join(", ")} collide with the AgentCore adapter ` +
|
|
182
|
+
`(/invocations, /ping) — rename the channel route`);
|
|
183
|
+
}
|
|
184
|
+
return { ...routes, ...mounted };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
|
|
188
|
+
* (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
|
|
189
|
+
* `127.0.0.2` bind, loopback though it is — would leave the tunnel up and 502ing every request.
|
|
190
|
+
* Checked BEFORE the bind (the flag alone pre-spawn in `dev`, again once config is loaded, since
|
|
191
|
+
* `http.host` can carry the address), so the failure is clean rather than a live-but-broken public URL.
|
|
192
|
+
* `source` decides the exit code, per fail.ts: a flag COMBINATION is a usage error (2), a value that
|
|
193
|
+
* came from config is broken runtime configuration (1).
|
|
194
|
+
*/
|
|
195
|
+
export function assertTunnelBindable(host, tunnel, source) {
|
|
196
|
+
if (!tunnel || answersLocalhost(host))
|
|
197
|
+
return;
|
|
198
|
+
// Name the source, not just the exit code: under `config` there is no `--bind` to change and no flag
|
|
199
|
+
// to drop, so flag-only wording would send the reader looking for something they never typed.
|
|
200
|
+
const fix = source === "flag"
|
|
201
|
+
? "bind 0.0.0.0 (or 127.0.0.1), or drop --tunnel"
|
|
202
|
+
: "set http.host to 0.0.0.0 (or 127.0.0.1) in fastagent.config.*, override it with --bind, or drop --tunnel";
|
|
203
|
+
const message = `--tunnel reaches the serve by dialing localhost, which the bind address ${host} does not answer — ${fix}`;
|
|
204
|
+
if (source === "flag")
|
|
205
|
+
failUsage(message);
|
|
206
|
+
failStartup(new Error(message));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
|
|
210
|
+
* ONE function because they are one message — they were two, and `--bind` updated the first while the
|
|
211
|
+
* second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
|
|
212
|
+
* neither can be changed without the other in view, and the address has a single derivation.
|
|
213
|
+
*
|
|
214
|
+
* A wildcard bind is every interface, and naming one address there would understate it — but the curl
|
|
215
|
+
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
216
|
+
*/
|
|
217
|
+
export function readyAddressLines(host, boundPort, builtinInvoke) {
|
|
218
|
+
const dial = `${clientHost(host)}:${boundPort}`;
|
|
219
|
+
const lines = [
|
|
220
|
+
`[fastagent] http host on ${classifyBind(host) === "wildcard" ? `:${boundPort} (all interfaces)` : bindLabel(host, boundPort)}`,
|
|
221
|
+
];
|
|
222
|
+
if (builtinInvoke) {
|
|
223
|
+
lines.push(`[fastagent] try it: curl -s ${dial}/invoke -X POST -H 'content-type: application/json' -d '${INVOKE_EXAMPLE_BODY}'`);
|
|
224
|
+
}
|
|
225
|
+
return lines;
|
|
226
|
+
}
|
|
135
227
|
/**
|
|
136
228
|
* Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
|
|
137
229
|
* adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
|
|
138
|
-
* the sole clean-shutdown command.
|
|
230
|
+
* the sole clean-shutdown command. `host` unset binds all interfaces.
|
|
139
231
|
*/
|
|
140
|
-
export function serve(surface,
|
|
141
|
-
const
|
|
232
|
+
export function serve(surface, bind, onListening) {
|
|
233
|
+
const { port, host } = bind;
|
|
234
|
+
const hosted = serveNode(router(surface.routes), { port, host });
|
|
142
235
|
const abort = new AbortController();
|
|
143
236
|
let stopping = false;
|
|
144
237
|
const stop = (exitCode) => {
|
|
@@ -193,14 +286,12 @@ export function serve(surface, port, onListening) {
|
|
|
193
286
|
port: boundPort,
|
|
194
287
|
routeChannels: surface.routeChannels,
|
|
195
288
|
});
|
|
196
|
-
|
|
289
|
+
for (const line of readyAddressLines(host, boundPort, surface.builtinInvoke))
|
|
290
|
+
log.info(line);
|
|
197
291
|
log.info(`[fastagent] routes: ${Object.keys(surface.routes).join(", ") || "(none)"}`);
|
|
198
292
|
if (surface.longConnections.length > 0) {
|
|
199
293
|
log.info(`[fastagent] long connections: ${surface.longConnections.map((connection) => connection.name).join(", ")}`);
|
|
200
294
|
}
|
|
201
|
-
if (surface.builtinInvoke) {
|
|
202
|
-
log.info(`[fastagent] try it: curl -s localhost:${boundPort}/invoke -X POST -H 'content-type: application/json' -d '${INVOKE_EXAMPLE_BODY}'`);
|
|
203
|
-
}
|
|
204
295
|
onListening?.(boundPort);
|
|
205
296
|
}
|
|
206
297
|
catch (error) {
|
|
@@ -211,38 +302,48 @@ export function serve(surface, port, onListening) {
|
|
|
211
302
|
failStartup(error);
|
|
212
303
|
}
|
|
213
304
|
}, (error) => {
|
|
214
|
-
if (error.code === "EADDRINUSE")
|
|
215
|
-
|
|
216
|
-
|
|
305
|
+
if (error.code === "EADDRINUSE") {
|
|
306
|
+
// With a bind address the port is only taken ON THAT interface, so moving the bind is as valid
|
|
307
|
+
// a fix as moving the port.
|
|
308
|
+
failStartup(new Error(`${bindLabel(host, port)} is already in use; choose another with ` +
|
|
309
|
+
`--port${classifyBind(host) === "wildcard" ? "" : " or --bind"}`));
|
|
310
|
+
}
|
|
311
|
+
// Through `bindLabel`, like every other message about a bind: hand-concatenating gives `:::8787`
|
|
312
|
+
// for an IPv6 bind and a bare `:8787` for the wildcard, which reads as an explicit bind of nothing.
|
|
313
|
+
failStartup(new Error(`cannot bind http channel on ${bindLabel(host, port)}: ${error.message}`));
|
|
217
314
|
});
|
|
218
315
|
}
|
|
219
316
|
/** Start a Cloudflare tunnel for route channels only. */
|
|
220
|
-
export function maybeTunnel(
|
|
317
|
+
export function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoot) {
|
|
221
318
|
if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
|
|
222
319
|
return;
|
|
223
320
|
void startCloudflareTunnel(boundPort).then((instance) => {
|
|
224
321
|
if (!instance)
|
|
225
322
|
return;
|
|
226
|
-
void announceWebhooks(
|
|
323
|
+
void announceWebhooks(agentDir, instance.url, { openUrl: openExternalUrl, routeChannels, stateRoot });
|
|
227
324
|
const cleanup = () => instance.close();
|
|
228
325
|
process.once("SIGINT", cleanup);
|
|
229
326
|
process.once("SIGTERM", cleanup);
|
|
230
327
|
});
|
|
231
328
|
}
|
|
232
329
|
/**
|
|
233
|
-
* Load and start the
|
|
234
|
-
* there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals.
|
|
330
|
+
* Load and start the agent's `schedules/` — a time-trigger firing the agent on each cron. Starts iff
|
|
331
|
+
* there are static schedules OR `selfSchedule` is on. Best-effort stop on process signals. Returns the
|
|
332
|
+
* loaded schedules so a serving surface that needs them (the AgentCore adapter's fire binding) shares
|
|
333
|
+
* ONE load instead of re-discovering. `externalClock` (AgentCore) arms no resident cron timers.
|
|
235
334
|
*/
|
|
236
|
-
export async function startSchedules(
|
|
237
|
-
const { schedules, failures } = await loadSchedules(
|
|
335
|
+
export async function startSchedules(agentDir, agent, stateRoot, selfSchedule, options = {}) {
|
|
336
|
+
const { schedules, failures } = await loadSchedules(agentDir).catch(failStartup);
|
|
238
337
|
reportModuleLoadFailures(failures);
|
|
239
338
|
if (schedules.length === 0 && !selfSchedule)
|
|
240
|
-
return;
|
|
241
|
-
const scheduler = createScheduler({ agent, stateRoot, schedules });
|
|
339
|
+
return schedules;
|
|
340
|
+
const scheduler = createScheduler({ agent, stateRoot, schedules, externalClock: options.externalClock });
|
|
242
341
|
scheduler.start();
|
|
243
|
-
if (schedules.length > 0)
|
|
244
|
-
log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}`);
|
|
342
|
+
if (schedules.length > 0) {
|
|
343
|
+
log.info(`[fastagent] schedules: ${schedules.map((s) => s.name).join(", ")}${options.externalClock ? " (external clock — no resident cron timers)" : ""}`);
|
|
344
|
+
}
|
|
245
345
|
const stop = () => scheduler.stop();
|
|
246
346
|
process.once("SIGINT", stop);
|
|
247
347
|
process.once("SIGTERM", stop);
|
|
348
|
+
return schedules;
|
|
248
349
|
}
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
|
|
4
|
+
* labels drift out of alignment the moment a longer one appears — which is exactly what happened when
|
|
5
|
+
* `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
|
|
6
|
+
* stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
|
|
7
|
+
* policy (pad, never hand-space), not a constant.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reportLine(label: string, value: string): void;
|
|
10
|
+
/** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
|
|
11
|
+
* you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
|
|
12
|
+
* as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
|
|
13
|
+
export declare function reportWorkspaceHint(hint: string | undefined): void;
|
|
2
14
|
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
3
15
|
export declare function isInteractive(): boolean;
|
|
4
16
|
/**
|
|
@@ -8,6 +20,12 @@ export declare function isInteractive(): boolean;
|
|
|
8
20
|
* bad `PORT` env is broken runtime configuration (1).
|
|
9
21
|
*/
|
|
10
22
|
export declare function parsePort(value: string | undefined, source: string, from: "flag" | "env"): number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Parse a `--bind` address: empty/whitespace is "not set" → undefined (the `??` chain falls through to
|
|
25
|
+
* config, then all interfaces). An unbindable string is a USAGE error (2) — caught here rather than as
|
|
26
|
+
* a node bind failure, or worse, as a "the interface you bound" diagnostic downstream.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseBind(value: string | undefined): string | undefined;
|
|
11
29
|
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
|
|
12
30
|
export declare function reportAuth(modelSpec: string, authPath: string): Promise<void>;
|
|
13
31
|
/**
|
|
@@ -19,9 +37,10 @@ export declare function reportAuth(modelSpec: string, authPath: string): Promise
|
|
|
19
37
|
* already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
|
|
20
38
|
* it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
|
|
21
39
|
* model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
|
|
22
|
-
* and best-effort written back to the config so the next run is quiet.
|
|
40
|
+
* and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
|
|
41
|
+
* AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
|
|
23
42
|
*/
|
|
24
|
-
export declare function resolveFirstRunModel(
|
|
43
|
+
export declare function resolveFirstRunModel(agentDir: string, options?: {
|
|
25
44
|
model?: string;
|
|
26
45
|
authPath?: string;
|
|
27
46
|
input?: boolean;
|
package/dist/cli/shared.js
CHANGED
|
@@ -8,14 +8,31 @@ import { relative } from "node:path";
|
|
|
8
8
|
import { autocomplete, isCancel, log as clackLog, password, select, text as clackText } from "@clack/prompts";
|
|
9
9
|
import { buildModelPickerOptions } from "./models-view.js";
|
|
10
10
|
import { fastagentCredentialStore } from "../engines/pi/auth.js";
|
|
11
|
-
import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec,
|
|
12
|
-
import { ensureStateRootSelfIgnored, isUnderDir } from "../engines/pi/definition.js";
|
|
11
|
+
import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resolveModel, resolveModelSpec, rewriteConfigModel, } from "../engines/pi/config.js";
|
|
13
12
|
import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
|
|
14
13
|
import { createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses } from "../engines/pi/models.js";
|
|
15
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
16
15
|
import { log } from "../log.js";
|
|
17
16
|
import { openExternalUrl } from "../open-url.js";
|
|
17
|
+
import { bindAddress, isBindAddress } from "../bind.js";
|
|
18
18
|
import { failStartup, failUsage } from "./fail.js";
|
|
19
|
+
/**
|
|
20
|
+
* The padded label writer for the STARTUP report (`dev`/`start`, stderr via the log level). Hand-spaced
|
|
21
|
+
* labels drift out of alignment the moment a longer one appears — which is exactly what happened when
|
|
22
|
+
* `workspace:` joined `config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
|
|
23
|
+
* stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
|
|
24
|
+
* policy (pad, never hand-space), not a constant.
|
|
25
|
+
*/
|
|
26
|
+
export function reportLine(label, value) {
|
|
27
|
+
log.info(`[fastagent] ${`${label}:`.padEnd(11)}${value}`);
|
|
28
|
+
}
|
|
29
|
+
/** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
|
|
30
|
+
* you pointed at the agent, and the project around it is probably what you meant. A hint, so it renders
|
|
31
|
+
* as one and is silent otherwise — `dev` and `start` both print the pair, so both ask for it. */
|
|
32
|
+
export function reportWorkspaceHint(hint) {
|
|
33
|
+
if (hint)
|
|
34
|
+
reportLine("hint", hint);
|
|
35
|
+
}
|
|
19
36
|
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
20
37
|
export function isInteractive() {
|
|
21
38
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -40,6 +57,19 @@ export function parsePort(value, source, from) {
|
|
|
40
57
|
}
|
|
41
58
|
return Number(trimmed);
|
|
42
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a `--bind` address: empty/whitespace is "not set" → undefined (the `??` chain falls through to
|
|
62
|
+
* config, then all interfaces). An unbindable string is a USAGE error (2) — caught here rather than as
|
|
63
|
+
* a node bind failure, or worse, as a "the interface you bound" diagnostic downstream.
|
|
64
|
+
*/
|
|
65
|
+
export function parseBind(value) {
|
|
66
|
+
const trimmed = value?.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (!isBindAddress(trimmed))
|
|
70
|
+
failUsage(`invalid --bind "${value}": must be an IP address or "localhost"`);
|
|
71
|
+
return bindAddress(trimmed); // a name never travels past this point — see bind.ts
|
|
72
|
+
}
|
|
43
73
|
/** Report which source provides the model's credentials, surfacing a remediation hint at startup. Non-blocking. */
|
|
44
74
|
export async function reportAuth(modelSpec, authPath) {
|
|
45
75
|
const provider = providerOf(modelSpec);
|
|
@@ -65,23 +95,24 @@ export async function reportAuth(modelSpec, authPath) {
|
|
|
65
95
|
* already set; on a non-TTY (CI, a piped stdin), with `--no-input`, on cancel, or on a failed login
|
|
66
96
|
* it stays quiet and lets the caller raise its own clear error (`missing model`, or deploy's
|
|
67
97
|
* model-travel gate). The pick is exported to FASTAGENT_MODEL so a spawned `dev` worker inherits it,
|
|
68
|
-
* and best-effort written back to the config so the next run is quiet.
|
|
98
|
+
* and best-effort written back to the config so the next run is quiet. `agentDir` is the resolved
|
|
99
|
+
* AGENT DIR (resolvePlacement().agentDir) — config and auth both live there.
|
|
69
100
|
*/
|
|
70
|
-
export async function resolveFirstRunModel(
|
|
71
|
-
const { config, path: configPath } = await loadConfig(
|
|
101
|
+
export async function resolveFirstRunModel(agentDir, options = {}) {
|
|
102
|
+
const { config, path: configPath } = await loadConfig(agentDir).catch(failStartup);
|
|
72
103
|
if (resolveModelSpec(options.model, config))
|
|
73
104
|
return; // already set (flag > FASTAGENT_MODEL > config)
|
|
74
105
|
if (options.input === false)
|
|
75
106
|
return; // --no-input: never prompt (clig) — the opener raises the clear error
|
|
76
107
|
if (!isInteractive())
|
|
77
108
|
return; // CI/deploy: the opener throws the actionable missing-model error
|
|
78
|
-
const authPath = resolveAuthPath(
|
|
109
|
+
const authPath = resolveAuthPath(agentDir, options.authPath);
|
|
79
110
|
const models = createPiModels({ authPath });
|
|
80
|
-
const chosen = await pickWithCredentials(
|
|
111
|
+
const chosen = await pickWithCredentials(models, authPath);
|
|
81
112
|
if (chosen === undefined)
|
|
82
113
|
return; // cancelled (or auth probe failed): the caller raises its clear missing-model error
|
|
83
114
|
process.env.FASTAGENT_MODEL = chosen; // this process + any spawned dev worker inherits it
|
|
84
|
-
await persistModelChoice(
|
|
115
|
+
await persistModelChoice(agentDir, configPath, chosen);
|
|
85
116
|
}
|
|
86
117
|
/**
|
|
87
118
|
* The credential-aware pick: full catalog annotated per provider, then the post-pick auth policy —
|
|
@@ -89,7 +120,7 @@ export async function resolveFirstRunModel(workspaceDir, options = {}) {
|
|
|
89
120
|
* independent of credentials), or the inline login for the rest. Returns the chosen spec, or
|
|
90
121
|
* undefined when the pick should be discarded (picker cancel, login cancel, a failed auth probe).
|
|
91
122
|
*/
|
|
92
|
-
async function pickWithCredentials(
|
|
123
|
+
async function pickWithCredentials(models, authPath) {
|
|
93
124
|
let statuses;
|
|
94
125
|
try {
|
|
95
126
|
statuses = await providerAuthStatuses(models);
|
|
@@ -124,11 +155,6 @@ async function pickWithCredentials(workspaceDir, models, authPath) {
|
|
|
124
155
|
}
|
|
125
156
|
return chosen;
|
|
126
157
|
}
|
|
127
|
-
// Inline login. Same leak guard as `login`: self-ignore the state root BEFORE a credential
|
|
128
|
-
// can land in-tree, so the secret is never untracked-but-committable.
|
|
129
|
-
const stateRoot = resolveStateRoot(workspaceDir);
|
|
130
|
-
if (isUnderDir(authPath, stateRoot))
|
|
131
|
-
await ensureStateRootSelfIgnored(workspaceDir, stateRoot);
|
|
132
158
|
try {
|
|
133
159
|
// Verified against the CHOSEN model — the exact request the agent is about to make; a rejected
|
|
134
160
|
// key re-prompts inside the loop, so reaching here means a usable (or at worst unverifiable) key.
|
|
@@ -224,10 +250,10 @@ function terminalLoginIO() {
|
|
|
224
250
|
/**
|
|
225
251
|
* Best-effort persist the picked model so the next run does not prompt. Rewrites the commented
|
|
226
252
|
* `model:` placeholder the scaffold writes / an existing `model:` line, or re-inserts the line into a
|
|
227
|
-
* scaffold-shaped config (the hand-deleted-to-reset case); anything else (
|
|
228
|
-
*
|
|
253
|
+
* scaffold-shaped config (the hand-deleted-to-reset case); anything else (a hand-shaped config) is
|
|
254
|
+
* left untouched with a printed hint. Never throws — persistence is a convenience.
|
|
229
255
|
*/
|
|
230
|
-
async function persistModelChoice(
|
|
256
|
+
async function persistModelChoice(agentDir, configPath, spec) {
|
|
231
257
|
const hint = () => console.error(
|
|
232
258
|
// No "using it for this run" promise: deploy's model-travel gate rightly ignores the un-persisted pick.
|
|
233
259
|
`[fastagent] picked ${spec} — set \`model: ${JSON.stringify(spec)}\` in your config to persist`);
|
|
@@ -238,7 +264,7 @@ async function persistModelChoice(workspaceDir, configPath, spec) {
|
|
|
238
264
|
if (!replaced)
|
|
239
265
|
return hint();
|
|
240
266
|
await writeFile(configPath, replaced);
|
|
241
|
-
console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(
|
|
267
|
+
console.error(`[fastagent] saved model ${JSON.stringify(spec)} to ${relative(agentDir, configPath)}`);
|
|
242
268
|
}
|
|
243
269
|
catch {
|
|
244
270
|
hint();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ChannelKind } from "../../scaffold/add-channel.ts";
|
|
2
|
+
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
3
|
+
/** The one schedule fact the plan needs (from loadSchedules) — name + cron + tz. */
|
|
4
|
+
export interface ScheduleFact {
|
|
5
|
+
name: string;
|
|
6
|
+
cron: string;
|
|
7
|
+
tz?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AgentcorePlanInput extends ContainerInput {
|
|
10
|
+
/** Base name (dir basename) — shapes the runtime name, stack name, ECR repo, session id. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
13
|
+
modelAuth: string | undefined;
|
|
14
|
+
/** Known first-party channels — each contributes its secret metadata + webhook step. */
|
|
15
|
+
channels: ChannelKind[];
|
|
16
|
+
/** ALL route-channel basenames (customs included) — any of them requires the forwarder. */
|
|
17
|
+
routeChannels: string[];
|
|
18
|
+
/** Extra secret env-var names (fastagent.config deploy.secrets). */
|
|
19
|
+
extraSecrets?: string[];
|
|
20
|
+
/** Static schedules — each becomes an EventBridge Scheduler rule targeting the forwarder. */
|
|
21
|
+
schedules: ScheduleFact[];
|
|
22
|
+
/** Wake tool enabled — DEGRADED here (fires only while a session happens to be awake); warned. */
|
|
23
|
+
selfSchedule: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface AgentcorePlan {
|
|
26
|
+
/** template + forwarder + Dockerfile/.dockerignore — written by the CLI (kept unless --force). */
|
|
27
|
+
artifacts: Artifact[];
|
|
28
|
+
/** The ordered, values-resolved deploy runbook — printed to stdout. */
|
|
29
|
+
runbook: string[];
|
|
30
|
+
/** Cron expressions EventBridge cannot express — surfaced as runbook warnings, not silent drops. */
|
|
31
|
+
untranslatableSchedules: {
|
|
32
|
+
name: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
/** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
|
|
37
|
+
* a fast LOCAL disk only: the platform wipes it on every runtime version update (= every deploy).
|
|
38
|
+
* Durability across deploys comes from the S3 snapshot (channels/agentcore-state.ts). */
|
|
39
|
+
export declare const MOUNT = "/mnt/state";
|
|
40
|
+
/**
|
|
41
|
+
* How long an idle session keeps its microVM. Memory is billed per second across the WHOLE session
|
|
42
|
+
* — idle included, at the peak level reached — so this tail is the standing cost of every burst of
|
|
43
|
+
* activity, while CPU stops billing the moment the agent stops working. 3 minutes rather than the
|
|
44
|
+
* platform's 15: the tail shrinks 5×, and the cost is a cold start (image + Node + snapshot restore)
|
|
45
|
+
* for anyone who returns after a longer gap. `/ping` reports HealthyBusy while work is in flight, so
|
|
46
|
+
* this timer only ever starts once the agent has genuinely settled — a long turn is never cut short.
|
|
47
|
+
* AWS accepts 60–28800.
|
|
48
|
+
*/
|
|
49
|
+
export declare const IDLE_TIMEOUT_SECONDS = 180;
|
|
50
|
+
/** The platform ceiling on one session's compute (8 h). The session ID outlives it: the next invoke
|
|
51
|
+
* simply gets fresh compute with the same storage. */
|
|
52
|
+
export declare const MAX_LIFETIME_SECONDS = 28800;
|
|
53
|
+
/** The state snapshot's object key in the deployment bucket (one object; see agentcore-state.ts). */
|
|
54
|
+
export declare const STATE_KEY = "state/snapshot.json.gz";
|
|
55
|
+
/** The forwarder artifact. Named `index.js` because it IS the Lambda deployment package's entry:
|
|
56
|
+
* zipping it as-is produces a valid package (`Handler: index.handler`), with nothing to rename. */
|
|
57
|
+
export declare const FORWARDER_FILE = "lambda/index.js";
|
|
58
|
+
/** The deployment bucket: forwarder code + the state snapshot. Account-suffixed for S3's GLOBAL
|
|
59
|
+
* namespace, and created OUTSIDE the stack (like the ECR repo) so a `delete-stack` cannot take the
|
|
60
|
+
* agent's memory with it. Bucket names cap at 63 chars; `name` is already gated to 40. */
|
|
61
|
+
export declare function stateBucketName(name: string, account: string): string;
|
|
62
|
+
/** AgentCore env values max 2048 chars — a real OAuth auth.json's base64 exceeds it, so the seed is
|
|
63
|
+
* CHUNKED across FASTAGENT_AUTH_SEED + _2… (collectAuthSeed reassembles at boot). 2000 keeps margin. */
|
|
64
|
+
export declare const AUTH_SEED_CHUNK_SIZE = 2000;
|
|
65
|
+
export declare const AUTH_SEED_MAX_CHUNKS = 4;
|
|
66
|
+
/** The generated template's filename (namespaced under the kit in the agentDir layout). */
|
|
67
|
+
export declare const TEMPLATE_FILE = "agentcore.template.yaml";
|
|
68
|
+
/** The generated template's first-line marker — the ONE source for both the generator and the
|
|
69
|
+
* "did fastagent generate this?" check (deploy's drift gate), so they cannot drift apart. */
|
|
70
|
+
export declare const GENERATED_TEMPLATE_MARKER = "# Generated by `fastagent deploy agentcore`";
|
|
71
|
+
/** Whether an on-disk template is fastagent-generated (vs hand-written — kept, never gated). */
|
|
72
|
+
export declare function isGeneratedAgentcoreTemplate(content: string): boolean;
|
|
73
|
+
/** Runtime name (`[a-zA-Z][a-zA-Z0-9_]{0,47}`) from a dir basename. */
|
|
74
|
+
export declare function toRuntimeName(basename: string): string;
|
|
75
|
+
/** The ONE fixed ingress session id (webhooks + schedule fires) — ≥ 33 chars (the API minimum),
|
|
76
|
+
* deterministic (the Lambda holds it in env), padded so any name clears the floor. */
|
|
77
|
+
export declare function ingressSessionId(name: string): string;
|
|
78
|
+
/** CFN parameter logical id for a secret env-var name: TELEGRAM_BOT_TOKEN → TelegramBotToken
|
|
79
|
+
* (parameter names must be alphanumeric). Deterministic — run.ts builds the same mapping. */
|
|
80
|
+
export declare function cfnParamName(envName: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Translate a 5-field cron into EventBridge Scheduler's `cron(m h dom mon dow *)`, or say why it
|
|
83
|
+
* can't be. The two dialects disagree exactly where silent translation would misfire:
|
|
84
|
+
* - EventBridge numbers day-of-week 1–7 (1 = Sunday); standard cron uses 0–6 (0/7 = Sunday) —
|
|
85
|
+
* numeric dow values and range endpoints are remapped ({@link mapDowField}); steps and names
|
|
86
|
+
* pass through; a range that wraps under renumbering is refused.
|
|
87
|
+
* - EventBridge requires `?` in dom or dow: a `*` on either side becomes `?`; BOTH restricted is
|
|
88
|
+
* standard cron's OR semantics, which EventBridge cannot express — refused, never approximated.
|
|
89
|
+
* - A 6-field (seconds) expression and L/# day-of-week forms are refused for the same reason.
|
|
90
|
+
*/
|
|
91
|
+
export declare function toEventBridgeCron(cron: string): {
|
|
92
|
+
expression: string;
|
|
93
|
+
} | {
|
|
94
|
+
error: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The forwarder Lambda source — the ONLY string both the template's inline ZipFile and the readable
|
|
98
|
+
* `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
|
|
99
|
+
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: CloudFormation inline code always
|
|
100
|
+
* lands as `index.js`, where ESM `import` is a syntax error (found by the first real deploy). Two
|
|
101
|
+
* event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
|
|
102
|
+
* channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
|
|
103
|
+
* Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
|
|
104
|
+
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key). MUST stay
|
|
105
|
+
* under CloudFormation's 4096-byte inline-code cap.
|
|
106
|
+
*/
|
|
107
|
+
export declare function forwarderSource(): string;
|
|
108
|
+
/**
|
|
109
|
+
* The EventBridge physical name for a schedule. A schedule's local name is an arbitrary MODULE FILE
|
|
110
|
+
* NAME (`schedules/晨报.ts`, `schedules/deploy check.ts`), while AWS requires `[0-9A-Za-z-_.]+` within
|
|
111
|
+
* 64 chars — and the `fa-<agent>-` prefix already eats up to 44 of them. So: sanitize, bound the
|
|
112
|
+
* readable part, and end with a hash of the ORIGINAL name, which keeps distinct schedules distinct
|
|
113
|
+
* where sanitizing or truncation would have merged them (one rule silently firing for two).
|
|
114
|
+
*/
|
|
115
|
+
export declare function scheduleResourceName(agent: string, schedule: string): string;
|
|
116
|
+
/** Compute the AgentCore deploy plan from the resolved definition. */
|
|
117
|
+
export declare function planAgentcoreDeploy(input: AgentcorePlanInput): AgentcorePlan;
|