@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
|
@@ -2,29 +2,24 @@
|
|
|
2
2
|
* `fastagent start [dir]`: run the agent in production posture — the SAME assembly as dev (your
|
|
3
3
|
* directory is the agent), just no file-watching. No build step: start reads the definition directly.
|
|
4
4
|
*/
|
|
5
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
5
|
import { dirname, resolve } from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import { writeFileAtomic } from "../../atomic-write.js";
|
|
7
|
+
import { authSeedBytes, collectAuthSeed } from "../../deploy/secrets.js";
|
|
8
8
|
import { loadDotEnv } from "../../env.js";
|
|
9
9
|
import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
|
|
10
|
-
import { resolveSecretsDir,
|
|
11
|
-
import {
|
|
12
|
-
import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
|
|
13
|
-
import { reportModuleLoadFailures } from "../../log.js";
|
|
14
|
-
import { CODING_TOOL_NAMES } from "../../engines/pi/create.js";
|
|
10
|
+
import { SECRET_FILE_MODE, ensureSecretsDir, resolveSecretsDir, isUnderDir, exists } from "../../paths.js";
|
|
11
|
+
import { log, setLogLevel } from "../../log.js";
|
|
15
12
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
16
13
|
import { mountAgentService } from "../../service.js";
|
|
17
|
-
import { log, setLogLevel } from "../../log.js";
|
|
18
14
|
import { logAgentLoop } from "../../observe.js";
|
|
19
15
|
import { installProxyFetch } from "../../proxy.js";
|
|
20
|
-
import { exists } from "../../paths.js";
|
|
21
16
|
import { bindAddress } from "../../bind.js";
|
|
22
17
|
import { isAgentcoreRuntime, mountAgentcoreService } from "../../channels/agentcore-service.js";
|
|
23
|
-
import { createWakeAlarmSink
|
|
18
|
+
import { createWakeAlarmSink } from "../../schedule/wake-alarm.js";
|
|
24
19
|
import { setWakeupsSink } from "../../schedule/wakeups.js";
|
|
25
20
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
26
21
|
import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
|
|
27
|
-
import { parseBind, parsePort,
|
|
22
|
+
import { parseBind, parsePort, reportAssembly, resolveFirstRunModel } from "../shared.js";
|
|
28
23
|
export async function runStart(dirArg, opts) {
|
|
29
24
|
const dir = resolve(dirArg);
|
|
30
25
|
// Flag validation first: a bad --port is a USAGE error (exit 2), and reporting it must not depend on
|
|
@@ -43,26 +38,15 @@ export async function runStart(dirArg, opts) {
|
|
|
43
38
|
// The same opener dev uses (single assembly source), just no watch.
|
|
44
39
|
const sessionsDirOverride = resolveSessionsDirOverride(opts.sessionsDir);
|
|
45
40
|
const opened = await openStartDir(dir, opts, sessionsDirOverride);
|
|
46
|
-
const { agent,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
reportLine("skills", definition.skills.map((s) => s.name).join(", ") || "(none)");
|
|
56
|
-
reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
|
|
57
|
-
if (toolNames.length > 0)
|
|
58
|
-
reportLine("tools", toolNames.join(", "));
|
|
59
|
-
if (deferredToolNames.length > 0) {
|
|
60
|
-
reportLine("deferred", `${deferredToolNames.join(", ")} (activated via search_tools)`);
|
|
61
|
-
}
|
|
62
|
-
reportToolCollisions(toolCollisions);
|
|
63
|
-
reportModuleLoadFailures(toolFailures);
|
|
64
|
-
reportLine("state", stateRoot);
|
|
65
|
-
reportLine("sessions", sessionsDir);
|
|
41
|
+
const { agent, agentDir, config, stateRoot, sessionsDir } = opened;
|
|
42
|
+
// The same report `dev` prints; `state:`/`sessions:` are start's own extras, and the persistence
|
|
43
|
+
// notes below are why (see reportAssembly on the asymmetry).
|
|
44
|
+
await reportAssembly(opened, {
|
|
45
|
+
afterTools: [
|
|
46
|
+
["state", stateRoot],
|
|
47
|
+
["sessions", sessionsDir],
|
|
48
|
+
],
|
|
49
|
+
});
|
|
66
50
|
// State defaults under the agent dir, which a redeploy may replace wholesale. Gate on where the
|
|
67
51
|
// state root ACTUALLY resolved (inside the agent dir?), not on the raw env var: an empty
|
|
68
52
|
// `FASTAGENT_STATE_DIR=""` reads as unset (resolveStateRoot) and still lands in-agent, so a raw
|
|
@@ -81,7 +65,6 @@ export async function runStart(dirArg, opts) {
|
|
|
81
65
|
log.info(`[fastagent] note: secrets (.env, rotated auth.json) live under the definition dir; point ` +
|
|
82
66
|
`FASTAGENT_SECRETS_DIR at a persistent volume so a redeploy that replaces the dir does not wipe them.`);
|
|
83
67
|
}
|
|
84
|
-
reportFindingsIfChanged(definition.dir, definition);
|
|
85
68
|
// Same debug turn trace as dev; gated out here by the info level (see dev.ts serveOnce).
|
|
86
69
|
const traced = logAgentLoop(agent);
|
|
87
70
|
// `http.host` enters here the way the flag enters `parseBind` — through `bindAddress`, so a
|
|
@@ -119,12 +102,6 @@ export async function runStart(dirArg, opts) {
|
|
|
119
102
|
// and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
|
|
120
103
|
// channels have no such layer, so their in-flight turns are still lost (the asker re-invokes).
|
|
121
104
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) into the
|
|
124
|
-
* writable secrets dir ONCE — only when the seed is set AND the auth file is absent, so a refreshed
|
|
125
|
-
* volume copy is never clobbered by the stale seed. Lets a deploy carry the operator's local
|
|
126
|
-
* OAuth/API credential so the box runs on the SAME subscription. No-op locally (the seed is unset).
|
|
127
|
-
*/
|
|
128
105
|
/** The opener, kept as its own call so `opened` can also feed the shared assembly below. */
|
|
129
106
|
function openStartDir(dir, opts, sessionsDir) {
|
|
130
107
|
return createPiAgentFromDir(dir, {
|
|
@@ -134,14 +111,25 @@ function openStartDir(dir, opts, sessionsDir) {
|
|
|
134
111
|
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
135
112
|
}).catch(failStartup);
|
|
136
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) into the
|
|
116
|
+
* writable secrets dir ONCE — only when the seed is set AND the auth file is absent, so a refreshed
|
|
117
|
+
* volume copy is never clobbered by the stale seed. Lets a deploy carry the operator's local
|
|
118
|
+
* OAuth/API credential so the box runs on the SAME subscription. No-op locally (the seed is unset).
|
|
119
|
+
*/
|
|
137
120
|
async function maybeSeedAuth(authPath) {
|
|
138
121
|
// collectAuthSeed: the seed may arrive CHUNKED (FASTAGENT_AUTH_SEED + _2…) on hosts with a small
|
|
139
122
|
// env-value max length (AgentCore); single-var hosts are unchanged.
|
|
140
123
|
const bytes = authSeedBytes(collectAuthSeed(process.env), await exists(authPath));
|
|
141
124
|
if (!bytes)
|
|
142
125
|
return;
|
|
143
|
-
await
|
|
144
|
-
|
|
126
|
+
await ensureSecretsDir(dirname(authPath));
|
|
127
|
+
// Same spelling as the credential store: the mode is applied before the content is reachable, so
|
|
128
|
+
// the seed can never be published at some other file's looser permissions. It does NOT close the
|
|
129
|
+
// gap between the `exists` check and the write — the rename would overwrite a file created in
|
|
130
|
+
// between, against this function's absent-only promise. That window needs two `start` processes on
|
|
131
|
+
// one secrets dir, which no supported deployment runs (one container, one volume).
|
|
132
|
+
writeFileAtomic(authPath, bytes, SECRET_FILE_MODE);
|
|
145
133
|
log.info(`[fastagent] seeded ${authPath} from FASTAGENT_AUTH_SEED (first boot)`);
|
|
146
134
|
}
|
|
147
135
|
/**
|
|
@@ -160,5 +148,7 @@ function armWakeAlarms(stateRoot) {
|
|
|
160
148
|
const sink = createWakeAlarmSink({ secret });
|
|
161
149
|
setWakeupsSink(sink);
|
|
162
150
|
log.info(`[fastagent] wake alarms: EventBridge-backed via the forwarder`);
|
|
163
|
-
|
|
151
|
+
// Boot reconcile: pending wake-ups may exist while their alarms were lost (a deploy replaced the
|
|
152
|
+
// forwarder, a sink call failed). The sink re-reads the store, so a bare notification is enough.
|
|
153
|
+
return () => sink(stateRoot);
|
|
164
154
|
}
|
package/dist/cli/serve.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import type { ChannelHandler } from "../channel.ts";
|
|
2
2
|
import type { AgentService } from "../service.ts";
|
|
3
|
-
/**
|
|
4
|
-
* Mount the session control plane (`/control/*`) when the agent enabled it
|
|
5
|
-
* (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
|
|
6
|
-
* writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
|
|
7
|
-
* the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
|
|
8
|
-
* its trust boundary, and each boot overwrites it with a fresh per-boot token. A user channel
|
|
9
|
-
* colliding on `/control/*` fails startup — the same disposition as a channel-channel collision
|
|
10
|
-
* (routesFor): `sessionControl` is an explicit opt-in, so declaring both is a configuration error,
|
|
11
|
-
* and silently shadowing either side would serve a surface the author didn't write.
|
|
12
|
-
*/
|
|
13
3
|
/**
|
|
14
4
|
* Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
|
|
15
5
|
* (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
|
|
@@ -20,6 +10,12 @@ import type { AgentService } from "../service.ts";
|
|
|
20
10
|
* came from config is broken runtime configuration (1).
|
|
21
11
|
*/
|
|
22
12
|
export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
|
|
13
|
+
/**
|
|
14
|
+
* The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
|
|
15
|
+
* what mounted. One function because both commands must say the same thing at the same moment —
|
|
16
|
+
* after readiness, never at socket bind.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reportServing(service: AgentService, host: string | undefined, boundPort: number): void;
|
|
23
19
|
/**
|
|
24
20
|
* The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
|
|
25
21
|
* ONE function because they are one message — they were two, and `--bind` updated the first while the
|
|
@@ -29,21 +25,16 @@ export declare function assertTunnelBindable(host: string | undefined, tunnel: b
|
|
|
29
25
|
* A wildcard bind is every interface, and naming one address there would understate it — but the curl
|
|
30
26
|
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
31
27
|
*/
|
|
32
|
-
/**
|
|
33
|
-
* The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
|
|
34
|
-
* what mounted. One function because both commands must say the same thing at the same moment —
|
|
35
|
-
* after readiness, never at socket bind.
|
|
36
|
-
*/
|
|
37
|
-
export declare function reportServing(service: AgentService, host: string | undefined, boundPort: number): void;
|
|
38
28
|
export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
|
|
39
|
-
/**
|
|
40
|
-
* Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
|
|
41
|
-
* adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
|
|
42
|
-
* the sole clean-shutdown command. `host` unset binds all interfaces.
|
|
43
|
-
*/
|
|
44
29
|
/** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
|
|
45
30
|
* a forced exit before the service answers would report a clean shutdown over a stuck channel. */
|
|
46
31
|
export declare const SHUTDOWN_GRACE_MS = 800;
|
|
32
|
+
/**
|
|
33
|
+
* Bind HTTP and report ready — but only once the SERVICE is, which is not the same moment: a bound
|
|
34
|
+
* socket is not a serving agent while a declared long-connection channel is still dialling, so this
|
|
35
|
+
* awaits `hooks.ready` (mountAgentService owns the connections themselves) before announcing
|
|
36
|
+
* anything. Signals are the sole clean-shutdown command; `host` unset binds all interfaces.
|
|
37
|
+
*/
|
|
47
38
|
export declare function serve(handler: ChannelHandler, bind: {
|
|
48
39
|
port: number;
|
|
49
40
|
host?: string;
|
package/dist/cli/serve.js
CHANGED
|
@@ -10,18 +10,9 @@ import { answersLocalhost, bindLabel, classifyBind, clientHost } from "../bind.j
|
|
|
10
10
|
import { serveNode } from "../channels/serve.js";
|
|
11
11
|
import { log } from "../log.js";
|
|
12
12
|
import { openExternalUrl } from "../open-url.js";
|
|
13
|
+
import { declaredChannels } from "../channels/discover.js";
|
|
13
14
|
import { announceWebhooks, startCloudflareTunnel } from "../tunnel.js";
|
|
14
15
|
import { failStartup, failUsage } from "./fail.js";
|
|
15
|
-
/**
|
|
16
|
-
* Mount the session control plane (`/control/*`) when the agent enabled it
|
|
17
|
-
* (`config.sessionControl`): merge the bearer-authenticated routes and return an announcer that
|
|
18
|
-
* writes `<stateRoot>/control.json` — `{ url, token }`, 0600 — once the port is known. The file is
|
|
19
|
-
* the LOCAL discovery channel (`fastagent attach`, a local desktop app); filesystem permissions are
|
|
20
|
-
* its trust boundary, and each boot overwrites it with a fresh per-boot token. A user channel
|
|
21
|
-
* colliding on `/control/*` fails startup — the same disposition as a channel-channel collision
|
|
22
|
-
* (routesFor): `sessionControl` is an explicit opt-in, so declaring both is a configuration error,
|
|
23
|
-
* and silently shadowing either side would serve a surface the author didn't write.
|
|
24
|
-
*/
|
|
25
16
|
/**
|
|
26
17
|
* Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
|
|
27
18
|
* (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
|
|
@@ -44,15 +35,6 @@ export function assertTunnelBindable(host, tunnel, source) {
|
|
|
44
35
|
failUsage(message);
|
|
45
36
|
failStartup(new Error(message));
|
|
46
37
|
}
|
|
47
|
-
/**
|
|
48
|
-
* The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
|
|
49
|
-
* ONE function because they are one message — they were two, and `--bind` updated the first while the
|
|
50
|
-
* second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
|
|
51
|
-
* neither can be changed without the other in view, and the address has a single derivation.
|
|
52
|
-
*
|
|
53
|
-
* A wildcard bind is every interface, and naming one address there would understate it — but the curl
|
|
54
|
-
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
55
|
-
*/
|
|
56
38
|
/**
|
|
57
39
|
* The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
|
|
58
40
|
* what mounted. One function because both commands must say the same thing at the same moment —
|
|
@@ -67,6 +49,15 @@ export function reportServing(service, host, boundPort) {
|
|
|
67
49
|
log.info(`[fastagent] long connections: ${service.channels.longConnections.join(", ")}`);
|
|
68
50
|
}
|
|
69
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The startup lines that name WHERE the serve is: the bind report, and the curl the reader copies.
|
|
54
|
+
* ONE function because they are one message — they were two, and `--bind` updated the first while the
|
|
55
|
+
* second went on dialing `localhost`, which is precisely what a non-wildcard bind stops answering. Now
|
|
56
|
+
* neither can be changed without the other in view, and the address has a single derivation.
|
|
57
|
+
*
|
|
58
|
+
* A wildcard bind is every interface, and naming one address there would understate it — but the curl
|
|
59
|
+
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
60
|
+
*/
|
|
70
61
|
export function readyAddressLines(host, boundPort, builtinInvoke) {
|
|
71
62
|
const dial = `${clientHost(host)}:${boundPort}`;
|
|
72
63
|
const lines = [
|
|
@@ -77,15 +68,16 @@ export function readyAddressLines(host, boundPort, builtinInvoke) {
|
|
|
77
68
|
}
|
|
78
69
|
return lines;
|
|
79
70
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Bind HTTP, open long-connection channels, and report ready only when both forms are usable. Each
|
|
82
|
-
* adapter owns reconnects; a terminal close rejects `closed` and fails the process visibly. Abort is
|
|
83
|
-
* the sole clean-shutdown command. `host` unset binds all interfaces.
|
|
84
|
-
*/
|
|
85
71
|
/** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
|
|
86
72
|
* a forced exit before the service answers would report a clean shutdown over a stuck channel. */
|
|
87
73
|
export const SHUTDOWN_GRACE_MS = 800;
|
|
88
74
|
const FORCED_EXIT_MS = 1_500;
|
|
75
|
+
/**
|
|
76
|
+
* Bind HTTP and report ready — but only once the SERVICE is, which is not the same moment: a bound
|
|
77
|
+
* socket is not a serving agent while a declared long-connection channel is still dialling, so this
|
|
78
|
+
* awaits `hooks.ready` (mountAgentService owns the connections themselves) before announcing
|
|
79
|
+
* anything. Signals are the sole clean-shutdown command; `host` unset binds all interfaces.
|
|
80
|
+
*/
|
|
89
81
|
export function serve(handler, bind, hooks = {}) {
|
|
90
82
|
const { port, host } = bind;
|
|
91
83
|
const hosted = serveNode(handler, { port, host });
|
|
@@ -155,7 +147,10 @@ export function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoo
|
|
|
155
147
|
void startCloudflareTunnel(boundPort).then((instance) => {
|
|
156
148
|
if (!instance)
|
|
157
149
|
return;
|
|
158
|
-
void announceWebhooks(agentDir, instance.url,
|
|
150
|
+
void announceWebhooks(agentDir, instance.url, declaredChannels(routeChannels), {
|
|
151
|
+
openUrl: openExternalUrl,
|
|
152
|
+
stateRoot,
|
|
153
|
+
});
|
|
159
154
|
const cleanup = () => instance.close();
|
|
160
155
|
process.once("SIGINT", cleanup);
|
|
161
156
|
process.once("SIGTERM", cleanup);
|
package/dist/cli/shared.d.ts
CHANGED
|
@@ -1,16 +1,46 @@
|
|
|
1
1
|
import { type LoginIO, type LoginMethod, type LoginResult } from "../engines/pi/login.ts";
|
|
2
|
+
import type { LoadedDefinition } from "../engines/pi/definition.ts";
|
|
3
|
+
import type { ModuleLoadFailure } from "../loader.ts";
|
|
4
|
+
import type { ToolCollision } from "../engines/pi/tool.ts";
|
|
5
|
+
/** What the startup report reads off an opened directory — a structural subset of the opener's return,
|
|
6
|
+
* spelled out so this module does not depend on that function's whole shape. */
|
|
7
|
+
export interface ReportableAssembly {
|
|
8
|
+
agentDir: string;
|
|
9
|
+
workspace: string;
|
|
10
|
+
modelSpec: string;
|
|
11
|
+
authPath: string;
|
|
12
|
+
config: {
|
|
13
|
+
thinkingLevel?: string;
|
|
14
|
+
};
|
|
15
|
+
definition: LoadedDefinition;
|
|
16
|
+
toolNames: string[];
|
|
17
|
+
deferredToolNames: string[];
|
|
18
|
+
toolCollisions: ToolCollision[];
|
|
19
|
+
toolFailures: ModuleLoadFailure[];
|
|
20
|
+
}
|
|
2
21
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
22
|
+
* What `dev` and `start` say about the directory they just opened, in the order they say it.
|
|
23
|
+
*
|
|
24
|
+
* ONE function because it is one report: the two commands wrote it out line by line, and the copies
|
|
25
|
+
* had already diverged over which lines exist at all — `dev` naming the config file, `start` naming
|
|
26
|
+
* state and sessions. A line added to one of two hand-written copies is invisible in the other.
|
|
27
|
+
*
|
|
28
|
+
* The divergence itself is PRESERVED, not resolved: `start`'s `state:`/`sessions:` pair introduces the
|
|
29
|
+
* persistence warnings that follow it in production posture, and `dev`'s `config:` has no counterpart
|
|
30
|
+
* there. Both are passed as explicit extras by the caller, so the asymmetry is visible at the call
|
|
31
|
+
* site rather than buried in two copies of a list. Whether it is RIGHT is a separate question from
|
|
32
|
+
* whether it has one owner.
|
|
33
|
+
*
|
|
34
|
+
* Findings (skill collisions, definition diagnostics) CLOSE the report — they are about the assembly
|
|
35
|
+
* just printed. What a command says next is its own posture talk, not report: `start`'s persistence
|
|
36
|
+
* notes now follow them rather than precede them.
|
|
8
37
|
*/
|
|
9
|
-
export declare function
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
38
|
+
export declare function reportAssembly(a: ReportableAssembly, extras?: {
|
|
39
|
+
/** Printed between `workspace:`/`hint:` and `model:` (`dev` names the config file here). */
|
|
40
|
+
beforeModel?: [label: string, value: string][];
|
|
41
|
+
/** Printed after the tool lines, before findings (`start` names state + sessions here). */
|
|
42
|
+
afterTools?: [label: string, value: string][];
|
|
43
|
+
}): Promise<void>;
|
|
14
44
|
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
15
45
|
export declare function isInteractive(): boolean;
|
|
16
46
|
/**
|
package/dist/cli/shared.js
CHANGED
|
@@ -12,7 +12,10 @@ import { isValidPort, listModels, loadConfig, providerOf, resolveAuthPath, resol
|
|
|
12
12
|
import { LoginCancelled, loginFlow } from "../engines/pi/login.js";
|
|
13
13
|
import { createPiModelRuntime, createPiModels, probeApiKey, probeAuthSource, providerAuthStatuses, } from "../engines/pi/models.js";
|
|
14
14
|
import { formatAuthReport } from "./auth-view.js";
|
|
15
|
-
import {
|
|
15
|
+
import { CODING_TOOL_NAMES } from "../engines/pi/create.js";
|
|
16
|
+
import { reportFindingsIfChanged, reportToolCollisions } from "../engines/pi/report.js";
|
|
17
|
+
import { workspaceHint } from "../paths.js";
|
|
18
|
+
import { log, reportModuleLoadFailures } from "../log.js";
|
|
16
19
|
import { openExternalUrl } from "../open-url.js";
|
|
17
20
|
import { bindAddress, isBindAddress } from "../bind.js";
|
|
18
21
|
import { failStartup, failUsage } from "./fail.js";
|
|
@@ -22,17 +25,61 @@ import { failStartup, failUsage } from "./fail.js";
|
|
|
22
25
|
* `codingTools:` joined `workspace:`/`config:`/`model:`/`state:`. `info` keeps its own writer on purpose: its report is
|
|
23
26
|
* stdout DATA (pipeable, its own label set, its own width), not a log line — the shared thing is the
|
|
24
27
|
* policy (pad, never hand-space), not a constant.
|
|
28
|
+
*
|
|
29
|
+
* Private to this module: {@link reportAssembly} is the report, and a command reaching past it for a
|
|
30
|
+
* line of its own is how the two copies of that report came to differ.
|
|
25
31
|
*/
|
|
26
|
-
|
|
32
|
+
function reportLine(label, value) {
|
|
27
33
|
log.info(`[fastagent] ${`${label}:`.padEnd(13)}${value}`);
|
|
28
34
|
}
|
|
29
35
|
/** The workspace hint under the `agent:`/`workspace:` pair, when there is one ({@link workspaceHint}):
|
|
30
36
|
* 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
|
|
32
|
-
|
|
37
|
+
* as one and is silent otherwise. */
|
|
38
|
+
function reportWorkspaceHint(hint) {
|
|
33
39
|
if (hint)
|
|
34
40
|
reportLine("hint", hint);
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* What `dev` and `start` say about the directory they just opened, in the order they say it.
|
|
44
|
+
*
|
|
45
|
+
* ONE function because it is one report: the two commands wrote it out line by line, and the copies
|
|
46
|
+
* had already diverged over which lines exist at all — `dev` naming the config file, `start` naming
|
|
47
|
+
* state and sessions. A line added to one of two hand-written copies is invisible in the other.
|
|
48
|
+
*
|
|
49
|
+
* The divergence itself is PRESERVED, not resolved: `start`'s `state:`/`sessions:` pair introduces the
|
|
50
|
+
* persistence warnings that follow it in production posture, and `dev`'s `config:` has no counterpart
|
|
51
|
+
* there. Both are passed as explicit extras by the caller, so the asymmetry is visible at the call
|
|
52
|
+
* site rather than buried in two copies of a list. Whether it is RIGHT is a separate question from
|
|
53
|
+
* whether it has one owner.
|
|
54
|
+
*
|
|
55
|
+
* Findings (skill collisions, definition diagnostics) CLOSE the report — they are about the assembly
|
|
56
|
+
* just printed. What a command says next is its own posture talk, not report: `start`'s persistence
|
|
57
|
+
* notes now follow them rather than precede them.
|
|
58
|
+
*/
|
|
59
|
+
export async function reportAssembly(a, extras = {}) {
|
|
60
|
+
reportLine("agent", a.agentDir);
|
|
61
|
+
reportLine("workspace", a.workspace);
|
|
62
|
+
reportWorkspaceHint(workspaceHint(a));
|
|
63
|
+
for (const [label, value] of extras.beforeModel ?? [])
|
|
64
|
+
reportLine(label, value);
|
|
65
|
+
reportLine("model", `${a.modelSpec}${a.config.thinkingLevel ? ` (thinking: ${a.config.thinkingLevel})` : ""}`);
|
|
66
|
+
await reportAuth(a.agentDir, a.modelSpec, a.authPath);
|
|
67
|
+
reportLine("context", a.definition.contextFiles.map((f) => f.path).join(", ") || "(none)");
|
|
68
|
+
if (a.definition.persona)
|
|
69
|
+
reportLine("persona", "persona.md");
|
|
70
|
+
reportLine("skills", a.definition.skills.map((s) => s.name).join(", ") || "(none)");
|
|
71
|
+
reportLine("codingTools", CODING_TOOL_NAMES.join(", "));
|
|
72
|
+
if (a.toolNames.length > 0)
|
|
73
|
+
reportLine("tools", a.toolNames.join(", "));
|
|
74
|
+
if (a.deferredToolNames.length > 0) {
|
|
75
|
+
reportLine("deferred", `${a.deferredToolNames.join(", ")} (activated via search_tools)`);
|
|
76
|
+
}
|
|
77
|
+
reportToolCollisions(a.toolCollisions);
|
|
78
|
+
reportModuleLoadFailures(a.toolFailures);
|
|
79
|
+
for (const [label, value] of extras.afterTools ?? [])
|
|
80
|
+
reportLine(label, value);
|
|
81
|
+
reportFindingsIfChanged(a.definition.dir, a.definition);
|
|
82
|
+
}
|
|
36
83
|
/** Both stdin and stdout are a terminal — the precondition for an interactive prompt. */
|
|
37
84
|
export function isInteractive() {
|
|
38
85
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -29,7 +29,7 @@ export type AgentcoreLogsOutcome = {
|
|
|
29
29
|
};
|
|
30
30
|
/**
|
|
31
31
|
* Find and tail one AgentCore log source. Discovery is dynamic rather than spelling `-DEFAULT`:
|
|
32
|
-
* endpoint naming belongs to AWS, and an edited stack may use a different endpoint.
|
|
33
|
-
*
|
|
32
|
+
* endpoint naming belongs to AWS, and an edited stack may use a different endpoint. The whole group
|
|
33
|
+
* is tailed — see the file header for why a stream filter cannot narrow it and must not be added.
|
|
34
34
|
*/
|
|
35
35
|
export declare function tailAgentcoreLogs(plan: AgentcoreLogsPlan, aws: CliRunner, announce?: (message: string) => void): Promise<AgentcoreLogsOutcome>;
|
|
@@ -17,8 +17,8 @@ function parseLogGroupNames(stdout) {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Find and tail one AgentCore log source. Discovery is dynamic rather than spelling `-DEFAULT`:
|
|
20
|
-
* endpoint naming belongs to AWS, and an edited stack may use a different endpoint.
|
|
21
|
-
*
|
|
20
|
+
* endpoint naming belongs to AWS, and an edited stack may use a different endpoint. The whole group
|
|
21
|
+
* is tailed — see the file header for why a stream filter cannot narrow it and must not be added.
|
|
22
22
|
*/
|
|
23
23
|
export async function tailAgentcoreLogs(plan, aws, announce = () => { }) {
|
|
24
24
|
const stack = `fastagent-${plan.name}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DeclaredChannel } from "../../channels/discover.ts";
|
|
2
2
|
import { type Artifact, type ContainerInput } from "../container.ts";
|
|
3
3
|
/** The one schedule fact the plan needs (from loadSchedules) — name + cron + tz. */
|
|
4
4
|
export interface ScheduleFact {
|
|
@@ -11,10 +11,9 @@ export interface AgentcorePlanInput extends ContainerInput {
|
|
|
11
11
|
name: string;
|
|
12
12
|
/** What satisfies model auth locally: an env-var name, an OAuth/stored label, or undefined. */
|
|
13
13
|
modelAuth: string | undefined;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
routeChannels: string[];
|
|
14
|
+
/** Every declared channel and its ingress — the source of the secret list, the webhook steps, and
|
|
15
|
+
* whether the forwarder is needed at all (ANY webhook channel requires it, customs included). */
|
|
16
|
+
channels: readonly DeclaredChannel[];
|
|
18
17
|
/** Extra secret env-var names (fastagent.config deploy.secrets). */
|
|
19
18
|
extraSecrets?: string[];
|
|
20
19
|
/** Static schedules — each becomes an EventBridge Scheduler rule targeting the forwarder. */
|
|
@@ -118,15 +117,18 @@ export declare function toEventBridgeCron(cron: string): {
|
|
|
118
117
|
error: string;
|
|
119
118
|
};
|
|
120
119
|
/**
|
|
121
|
-
* The forwarder Lambda source — the ONLY string both the
|
|
120
|
+
* The forwarder Lambda source — the ONLY string both the deployment package and the readable
|
|
122
121
|
* `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
|
|
123
|
-
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE:
|
|
124
|
-
*
|
|
122
|
+
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
|
|
123
|
+
* `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
|
|
125
124
|
* event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
|
|
126
125
|
* channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
|
|
127
126
|
* Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
|
|
128
|
-
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
129
|
-
*
|
|
127
|
+
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
128
|
+
*
|
|
129
|
+
* It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
|
|
130
|
+
* constraint this comment asserted for long after the template stopped inlining, which is the kind
|
|
131
|
+
* of stale rule that stops the next person making a correct change.
|
|
130
132
|
*/
|
|
131
133
|
export declare function forwarderSource(): string;
|
|
132
134
|
/**
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
31
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
32
32
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
33
|
+
import { webhookKinds, webhookRunbook } from "../channel-ingress.js";
|
|
33
34
|
import { containerArtifacts } from "../container.js";
|
|
34
35
|
import { deploymentSecrets, isEnvKey } from "../secrets.js";
|
|
35
36
|
/** SessionStorage mount = FASTAGENT_STATE_DIR (AgentCore requires exactly `/mnt/<one-level>`). It is
|
|
@@ -215,15 +216,18 @@ function yamlSingleQuote(value) {
|
|
|
215
216
|
return `'${value.replace(/'/g, "''")}'`;
|
|
216
217
|
}
|
|
217
218
|
/**
|
|
218
|
-
* The forwarder Lambda source — the ONLY string both the
|
|
219
|
+
* The forwarder Lambda source — the ONLY string both the deployment package and the readable
|
|
219
220
|
* `lambda/forwarder.js` artifact are generated from (one source, no drift). Zero-dependency: the
|
|
220
|
-
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE:
|
|
221
|
-
*
|
|
221
|
+
* Lambda Node runtime bundles AWS SDK v3. CommonJS ON PURPOSE: the package's entry lands as
|
|
222
|
+
* `index.js` ({@link zipSingleFile}), where ESM `import` is a syntax error. Two
|
|
222
223
|
* event shapes: a Function URL webhook (reconstructed verbatim into a `webhook` envelope; the
|
|
223
224
|
* channel's REAL response rides back inside the transport reply and is re-emitted byte-exact —
|
|
224
225
|
* Feishu's URL-verification challenge depends on it), and an EventBridge Scheduler fire
|
|
225
|
-
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
226
|
-
*
|
|
226
|
+
* (`{ scheduleFire }`, slot = the scheduled instant — the container's idempotency key).
|
|
227
|
+
*
|
|
228
|
+
* It ships as an S3 object rather than inline `ZipFile`, so there is no 4096-byte ceiling on it — a
|
|
229
|
+
* constraint this comment asserted for long after the template stopped inlining, which is the kind
|
|
230
|
+
* of stale rule that stops the next person making a correct change.
|
|
227
231
|
*/
|
|
228
232
|
export function forwarderSource() {
|
|
229
233
|
return `// Generated by \`fastagent deploy agentcore\` — the deployment's only ingress.
|
|
@@ -250,6 +254,15 @@ let ownUrl; // self-resolved once per cold start; rides on every envelope for th
|
|
|
250
254
|
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => \`%\${c.charCodeAt(0).toString(16).toUpperCase()}\`);
|
|
251
255
|
const hmac = (key, data) => crypto.createHmac("sha256", key).update(data).digest();
|
|
252
256
|
|
|
257
|
+
// Every shared-secret gate on this public URL goes through here. Constant-time, and an unset
|
|
258
|
+
// expected secret NEVER matches — otherwise a topology that did not configure one would accept a
|
|
259
|
+
// request that sent none. Non-string input is coerced to "" first: Buffer.from(8) allocates eight
|
|
260
|
+
// bytes rather than throwing, which would compare a number against a secret by length alone.
|
|
261
|
+
const secretEq = (given, expected) => {
|
|
262
|
+
const a = Buffer.from(typeof given === "string" ? given : ""), b = Buffer.from(expected || "");
|
|
263
|
+
return b.length > 0 && a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
264
|
+
};
|
|
265
|
+
|
|
253
266
|
function presign(method, seconds) {
|
|
254
267
|
const bucket = process.env.STATE_BUCKET, key = process.env.STATE_KEY, region = process.env.AWS_REGION;
|
|
255
268
|
const host = \`\${bucket}.s3.\${region}.amazonaws.com\`;
|
|
@@ -375,11 +388,7 @@ exports.handler = async (event, ctx) => {
|
|
|
375
388
|
// The container may settle long after the webhook Lambda (and its credentials) expired.
|
|
376
389
|
if (event.rawPath === "/__fastagent/state-urls") {
|
|
377
390
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
378
|
-
|
|
379
|
-
const expected = Buffer.from(process.env.STATE_REFRESH_SECRET || "");
|
|
380
|
-
if (!expected.length || actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
381
|
-
return { statusCode: 403, body: "forbidden\\n" };
|
|
382
|
-
}
|
|
391
|
+
if (!secretEq(req.auth, process.env.STATE_REFRESH_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
383
392
|
return {
|
|
384
393
|
statusCode: 200,
|
|
385
394
|
headers: { "content-type": "application/json" },
|
|
@@ -389,7 +398,7 @@ exports.handler = async (event, ctx) => {
|
|
|
389
398
|
// The container's wake-alarm callback (reserved path, shared secret) — handled HERE, never forwarded.
|
|
390
399
|
if (event.rawPath === "/__fastagent/wake-alarm") {
|
|
391
400
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
392
|
-
if (!
|
|
401
|
+
if (!secretEq(req.secret, process.env.WAKE_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
393
402
|
const failed = await syncAlarms(req.alarms || [], ctx);
|
|
394
403
|
// Partial failure IS failure: the container retries the whole (idempotent) set until every
|
|
395
404
|
// pending wake really has its alarm.
|
|
@@ -405,7 +414,7 @@ exports.handler = async (event, ctx) => {
|
|
|
405
414
|
// public traffic) are probeable too.
|
|
406
415
|
if (event.rawPath === "/__fastagent/probe") {
|
|
407
416
|
const req = JSON.parse(event.isBase64Encoded ? Buffer.from(event.body, "base64").toString() : event.body || "{}");
|
|
408
|
-
if (!
|
|
417
|
+
if (!secretEq(req.auth, process.env.INGRESS_SECRET)) return { statusCode: 403, body: "forbidden\\n" };
|
|
409
418
|
const r = await invoke({ kind: "probe" });
|
|
410
419
|
if (r.status !== 200) {
|
|
411
420
|
console.log(\`probe transport error \${r.status}: \${r.body}\`);
|
|
@@ -465,7 +474,7 @@ function template(input, translated) {
|
|
|
465
474
|
// a schedule turn can outlive both its original presigned URL and the Lambda credentials that signed
|
|
466
475
|
// it. Schedule-only URLs reject every non-reserved HTTP path before invoking AgentCore, so they do
|
|
467
476
|
// not expose a webhook/data plane (and start never mounts the builtin /invoke under AgentCore).
|
|
468
|
-
const needsForwarder = input.
|
|
477
|
+
const needsForwarder = input.channels.some((channel) => channel.ingress === "webhook") || translated.length > 0 || input.selfSchedule;
|
|
469
478
|
const needsFunctionUrl = needsForwarder;
|
|
470
479
|
const secrets = deploymentSecrets(input.modelAuth, input.channels, input.extraSecrets);
|
|
471
480
|
const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
|
|
@@ -604,7 +613,7 @@ function template(input, translated) {
|
|
|
604
613
|
` Action: lambda:GetFunctionUrlConfig`,
|
|
605
614
|
` Resource: ${forwarderFnArn}`,
|
|
606
615
|
]
|
|
607
|
-
: []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsFunctionUrl ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(input.
|
|
616
|
+
: []), ``, ` Forwarder:`, ` Type: AWS::Lambda::Function`, ` Properties:`, ` FunctionName: fastagent-${input.name}-forwarder`, ` Runtime: nodejs22.x`, ` Handler: index.handler`, ` # Webhook ACKs are fast, but schedule-fire holds the connection for the WHOLE agent turn`, ` # (claim-before-run means a timeout never double-fires; the turn also continues and is`, ` # audited container-side). EventBridge→Lambda is async, so the long timeout costs nothing.`, ` Timeout: 900`, ` MemorySize: 256`, ` Role: !GetAtt ForwarderRole.Arn`, ` Environment:`, ` Variables:`, ` RUNTIME_ARN: !GetAtt Runtime.AgentRuntimeArn`, ` INGRESS_SESSION_ID: ${ingressSessionId(input.name)}`, ...(needsFunctionUrl ? [` STATE_REFRESH_SECRET: !Ref FastagentIngressSecret`] : []), ...(input.channels.some((channel) => channel.ingress === "webhook") ? [` WEBHOOKS_ENABLED: "1"`] : []), ...(input.selfSchedule
|
|
608
617
|
? [
|
|
609
618
|
` WAKE_SECRET: !Ref FastagentWakeSecret`,
|
|
610
619
|
` WAKE_ROLE_ARN: !GetAtt WakeSchedulerRole.Arn`,
|
|
@@ -671,7 +680,7 @@ export function planAgentcoreDeploy(input) {
|
|
|
671
680
|
}
|
|
672
681
|
// Every forwarder needs its authenticated Function URL to refresh S3 snapshot capabilities during
|
|
673
682
|
// a long turn. Without route channels, ordinary HTTP paths are rejected before AgentCore is invoked.
|
|
674
|
-
const needsForwarder = input.
|
|
683
|
+
const needsForwarder = input.channels.some((channel) => channel.ingress === "webhook") || translated.length > 0 || input.selfSchedule;
|
|
675
684
|
const needsFunctionUrl = needsForwarder;
|
|
676
685
|
const artifacts = [
|
|
677
686
|
{ path: `${prefix}${TEMPLATE_FILE}`, content: template(input, translated) },
|
|
@@ -743,22 +752,21 @@ export function planAgentcoreDeploy(input) {
|
|
|
743
752
|
? `# Model auth: none found at the local auth path — pass --auth-path <file>, or \`--run\` carries it`
|
|
744
753
|
: `# Model auth: your local auth is "${input.modelAuth}" — the plan can't read its value; \`--run\` carries it`, `# as the FastagentAuthSeed parameter (base64 of auth.json), materialized on first boot.`);
|
|
745
754
|
}
|
|
746
|
-
// Post-deploy webhook registration —
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
post.push(`# Set the ${kind === "feishu" ? "Feishu" : "Lark"} event Request URL (developer console → Events & Callbacks):`, `# Request URL = <ForwarderUrl>/${kind} (the stack must be deployed when you save — the console`, `# sends a challenge, which rides through the forwarder to the channel and back verbatim).`);
|
|
755
|
+
// Post-deploy webhook registration — the shared channel-ingress steps, pointed at the forwarder's
|
|
756
|
+
// Function URL (read from the stack outputs). `--run` REFUSES a long-connection channel here, but
|
|
757
|
+
// generate-only only warns and still prints this runbook, so the steps are filtered on ingress like
|
|
758
|
+
// every other host's rather than on the CLI having gated.
|
|
759
|
+
const post = webhookRunbook(`<ForwarderUrl>`, channels);
|
|
760
|
+
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and
|
|
761
|
+
// nothing else has a compute ceiling.
|
|
762
|
+
// Named rather than positional: the asides land after ALL the steps, so "the console" has to say
|
|
763
|
+
// WHICH console. One line for both kinds — an agent that declares feishu AND lark reads the same
|
|
764
|
+
// fact twice otherwise and looks for a second step that does not exist.
|
|
765
|
+
if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
|
|
766
|
+
post.push(`# NOTE: the Feishu/Lark console's challenge rides through the forwarder to the channel and back`, `# verbatim, so the stack must be deployed when you save the Request URL.`);
|
|
767
|
+
}
|
|
768
|
+
if (channels.some((channel) => channel.name === "github")) {
|
|
769
|
+
post.push(`# NOTE: github turns are fire-and-forget with no replay — a compute reclaimed mid-review drops it`, `# (the ping's HealthyBusy + time_of_last_update holds the session while turns run, but the 8 h compute ceiling is hard).`);
|
|
762
770
|
}
|
|
763
771
|
if (post.length > 0)
|
|
764
772
|
runbook.push(``, ...post);
|