@fastagent-sh/fastagent 0.29.0 → 0.30.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/README.md +9 -4
- package/dist/atomic-write.d.ts +4 -5
- package/dist/atomic-write.js +4 -5
- package/dist/authoring.d.ts +32 -15
- package/dist/authoring.js +147 -127
- package/dist/channels/agentcore-protocol.d.ts +8 -3
- package/dist/channels/agentcore-protocol.js +3 -1
- package/dist/channels/agentcore-service.d.ts +1 -0
- package/dist/channels/agentcore-service.js +14 -4
- package/dist/channels/agentcore.d.ts +9 -0
- package/dist/channels/agentcore.js +29 -4
- package/dist/channels/context.d.ts +2 -0
- package/dist/channels/context.js +5 -1
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.js +3 -5
- package/dist/channels/feishu/scaffold/channel.ts +32 -34
- package/dist/channels/feishu/scaffold/feishu-send.ts +5 -5
- package/dist/channels/feishu/scaffold/feishu-threads.ts +3 -3
- package/dist/channels/feishu/setup-mode.d.ts +2 -12
- package/dist/channels/feishu/setup-mode.js +2 -22
- package/dist/channels/feishu/setup.d.ts +12 -0
- package/dist/channels/feishu/setup.js +197 -0
- package/dist/channels/kit/abortable.d.ts +8 -0
- package/dist/channels/kit/abortable.js +22 -0
- package/dist/channels/lark/lark.d.ts +0 -3
- package/dist/channels/lark/lark.js +0 -5
- package/dist/channels/lark/scaffold/channel.ts +33 -35
- package/dist/channels/lark/scaffold/lark-send.ts +5 -5
- package/dist/channels/lark/scaffold/lark-threads.ts +3 -3
- package/dist/channels/lark/setup.d.ts +9 -0
- package/dist/channels/lark/setup.js +57 -0
- package/dist/channels/setup.d.ts +112 -0
- package/dist/channels/setup.js +31 -0
- package/dist/channels/slack/history.js +1 -1
- package/dist/channels/slack/manifest.d.ts +17 -7
- package/dist/channels/slack/manifest.js +12 -16
- package/dist/channels/slack/scaffold/channel.ts +44 -30
- package/dist/channels/slack/scaffold/slack-send.ts +8 -8
- package/dist/channels/slack/scaffold/slack-threads.ts +4 -4
- package/dist/channels/slack/setup.d.ts +7 -0
- package/dist/channels/slack/setup.js +119 -0
- package/dist/channels/slack/slack-api.d.ts +2 -0
- package/dist/channels/slack/slack-api.js +4 -0
- package/dist/channels/slack/slack.d.ts +21 -2
- package/dist/channels/slack/slack.js +398 -338
- package/dist/channels/slack/socket.d.ts +57 -0
- package/dist/channels/slack/socket.js +305 -0
- package/dist/channels/telegram/polling.d.ts +27 -0
- package/dist/channels/telegram/polling.js +105 -0
- package/dist/channels/telegram/scaffold/channel.ts +52 -32
- package/dist/channels/telegram/scaffold/telegram-send.ts +9 -9
- package/dist/channels/telegram/setup.d.ts +2 -0
- package/dist/channels/telegram/setup.js +42 -0
- package/dist/channels/telegram/telegram-api.d.ts +27 -1
- package/dist/channels/telegram/telegram-api.js +7 -6
- package/dist/channels/telegram/telegram.d.ts +11 -3
- package/dist/channels/telegram/telegram.js +184 -157
- package/dist/channels/webhook-evidence.d.ts +11 -0
- package/dist/channels/webhook-evidence.js +71 -0
- package/dist/channels/webhooks.d.ts +37 -0
- package/dist/channels/webhooks.js +70 -0
- package/dist/child-env.d.ts +16 -0
- package/dist/child-env.js +30 -0
- package/dist/cli/box-login.js +4 -1
- package/dist/cli/channel-app.d.ts +3 -0
- package/dist/cli/channel-app.js +35 -0
- package/dist/cli/channel-setup.d.ts +38 -0
- package/dist/cli/channel-setup.js +195 -0
- package/dist/cli/commands/add.d.ts +6 -14
- package/dist/cli/commands/add.js +45 -303
- package/dist/cli/commands/auth.js +8 -2
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +16 -0
- package/dist/cli/commands/channels.d.ts +19 -0
- package/dist/cli/commands/channels.js +230 -0
- package/dist/cli/commands/content.d.ts +7 -3
- package/dist/cli/commands/content.js +16 -10
- package/dist/cli/commands/deploy/agentcore.js +55 -23
- package/dist/cli/commands/deploy/channel-apps.d.ts +17 -0
- package/dist/cli/commands/deploy/channel-apps.js +30 -0
- package/dist/cli/commands/deploy/docker.js +32 -65
- package/dist/cli/commands/deploy/fly.js +28 -12
- package/dist/cli/commands/deploy/railway.js +38 -13
- package/dist/cli/commands/deploy/secrets-step.d.ts +19 -0
- package/dist/cli/commands/deploy/secrets-step.js +66 -0
- package/dist/cli/commands/deploy/shared.d.ts +16 -5
- package/dist/cli/commands/deploy/shared.js +25 -8
- package/dist/cli/commands/deploy.js +53 -60
- package/dist/cli/commands/dev.d.ts +2 -0
- package/dist/cli/commands/dev.js +15 -3
- package/dist/cli/commands/env.js +1 -1
- package/dist/cli/commands/info.d.ts +2 -0
- package/dist/cli/commands/info.js +60 -25
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.js +20 -7
- package/dist/cli/commands/login.d.ts +0 -7
- package/dist/cli/commands/login.js +22 -10
- package/dist/cli/commands/logout.js +1 -1
- package/dist/cli/commands/mcp.js +14 -6
- package/dist/cli/commands/models.js +3 -2
- package/dist/cli/commands/schedules.d.ts +4 -2
- package/dist/cli/commands/schedules.js +14 -10
- package/dist/cli/commands/secrets.d.ts +14 -0
- package/dist/cli/commands/secrets.js +213 -0
- package/dist/cli/commands/start.d.ts +14 -14
- package/dist/cli/commands/start.js +46 -60
- package/dist/cli/content-view.d.ts +4 -6
- package/dist/cli/content-view.js +5 -11
- package/dist/cli/first-run-channels.d.ts +3 -0
- package/dist/cli/first-run-channels.js +63 -0
- package/dist/cli/instance.d.ts +41 -0
- package/dist/cli/instance.js +155 -0
- package/dist/cli/invoke-stream.d.ts +5 -2
- package/dist/cli/invoke-stream.js +7 -2
- package/dist/cli/program.js +282 -110
- package/dist/cli/serve.d.ts +4 -5
- package/dist/cli/serve.js +4 -7
- package/dist/cli/shared.d.ts +11 -2
- package/dist/cli/shared.js +32 -19
- package/dist/cli/ticket.js +50 -6
- package/dist/content/declare.d.ts +31 -37
- package/dist/content/declare.js +59 -60
- package/dist/content/file.d.ts +23 -12
- package/dist/content/file.js +48 -35
- package/dist/content/git.d.ts +0 -10
- package/dist/content/git.js +5 -39
- package/dist/content/resolve.d.ts +18 -22
- package/dist/content/resolve.js +32 -66
- package/dist/content/source.d.ts +7 -8
- package/dist/content/source.js +10 -28
- package/dist/credentials-page.d.ts +26 -0
- package/dist/credentials-page.js +185 -0
- package/dist/declared-secrets.d.ts +10 -3
- package/dist/declared-secrets.js +8 -4
- package/dist/deploy/agentcore/destroy.d.ts +4 -1
- package/dist/deploy/agentcore/destroy.js +25 -1
- package/dist/deploy/agentcore/forwarder.js +1 -1
- package/dist/deploy/agentcore/plan.d.ts +7 -21
- package/dist/deploy/agentcore/plan.js +47 -49
- package/dist/deploy/agentcore/run.d.ts +18 -10
- package/dist/deploy/agentcore/run.js +93 -70
- package/dist/deploy/agentcore/runtime-secrets.d.ts +7 -0
- package/dist/deploy/agentcore/runtime-secrets.js +108 -0
- package/dist/deploy/agentcore/shell.js +12 -19
- package/dist/deploy/agentcore/sigv4.d.ts +7 -0
- package/dist/deploy/agentcore/sigv4.js +15 -0
- package/dist/deploy/agentcore/slr.d.ts +18 -0
- package/dist/deploy/agentcore/slr.js +27 -0
- package/dist/deploy/agentcore/store.d.ts +7 -0
- package/dist/deploy/agentcore/store.js +115 -0
- package/dist/deploy/api.d.ts +14 -14
- package/dist/deploy/api.js +26 -24
- package/dist/deploy/box-shell.d.ts +3 -1
- package/dist/deploy/build-context.d.ts +4 -4
- package/dist/deploy/build-context.js +11 -15
- package/dist/deploy/channel-ingress.d.ts +35 -9
- package/dist/deploy/channel-ingress.js +119 -22
- package/dist/deploy/container.d.ts +35 -10
- package/dist/deploy/container.js +79 -37
- package/dist/deploy/delivered-text.d.ts +13 -0
- package/dist/deploy/delivered-text.js +86 -0
- package/dist/deploy/docker/plan.d.ts +7 -8
- package/dist/deploy/docker/plan.js +31 -52
- package/dist/deploy/docker/run.d.ts +5 -43
- package/dist/deploy/docker/run.js +9 -84
- package/dist/deploy/file-store.d.ts +2 -0
- package/dist/deploy/file-store.js +17 -0
- package/dist/deploy/fly/plan.d.ts +3 -2
- package/dist/deploy/fly/plan.js +20 -9
- package/dist/deploy/fly/run.d.ts +18 -8
- package/dist/deploy/fly/run.js +24 -42
- package/dist/deploy/fly/store.d.ts +12 -0
- package/dist/deploy/fly/store.js +83 -0
- package/dist/deploy/hosts.d.ts +1 -2
- package/dist/deploy/hosts.js +1 -2
- package/dist/deploy/preflight.d.ts +18 -5
- package/dist/deploy/preflight.js +135 -63
- package/dist/deploy/railway/ignore.d.ts +8 -0
- package/dist/deploy/railway/ignore.js +57 -0
- package/dist/deploy/railway/plan.d.ts +1 -2
- package/dist/deploy/railway/plan.js +8 -6
- package/dist/deploy/railway/run.d.ts +21 -15
- package/dist/deploy/railway/run.js +32 -78
- package/dist/deploy/railway/store.d.ts +29 -0
- package/dist/deploy/railway/store.js +143 -0
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +4 -3
- package/dist/deploy/secret-store.d.ts +63 -0
- package/dist/deploy/secret-store.js +98 -0
- package/dist/deploy/secrets.d.ts +29 -46
- package/dist/deploy/secrets.js +49 -122
- package/dist/deploy/workspace.d.ts +6 -12
- package/dist/deploy/workspace.js +18 -94
- package/dist/dev-supervisor.js +6 -8
- package/dist/environment/declare.js +3 -2
- package/dist/environment/mise.d.ts +3 -1
- package/dist/environment/mise.js +4 -2
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/harnesses/models/catalog.d.ts +1 -0
- package/dist/harnesses/models/catalog.js +1 -2
- package/dist/harnesses/models/environment.d.ts +7 -2
- package/dist/harnesses/models/environment.js +9 -5
- package/dist/harnesses/models/host.d.ts +0 -7
- package/dist/harnesses/models/host.js +10 -1
- package/dist/harnesses/models/login.d.ts +0 -8
- package/dist/harnesses/models/login.js +5 -4
- package/dist/harnesses/models/models.d.ts +7 -2
- package/dist/harnesses/models/models.js +28 -4
- package/dist/harnesses/pi-durable/agent.js +59 -38
- package/dist/harnesses/pi-durable/build.d.ts +12 -0
- package/dist/harnesses/pi-durable/build.js +270 -0
- package/dist/harnesses/pi-durable/built.d.ts +24 -0
- package/dist/harnesses/pi-durable/built.js +136 -0
- package/dist/harnesses/pi-durable/config.d.ts +81 -0
- package/dist/harnesses/pi-durable/config.js +266 -0
- package/dist/harnesses/pi-durable/define.d.ts +63 -13
- package/dist/harnesses/pi-durable/define.js +23 -2
- package/dist/harnesses/pi-durable/definition.d.ts +72 -0
- package/dist/harnesses/pi-durable/definition.js +143 -0
- package/dist/harnesses/pi-durable/events.d.ts +4 -3
- package/dist/harnesses/pi-durable/events.js +179 -136
- package/dist/harnesses/pi-durable/execution-env.d.ts +17 -0
- package/dist/harnesses/pi-durable/execution-env.js +11 -0
- package/dist/harnesses/pi-durable/mcp.js +1 -3
- package/dist/harnesses/pi-durable/open.d.ts +35 -37
- package/dist/harnesses/pi-durable/open.js +91 -153
- package/dist/harnesses/pi-durable/reads.d.ts +0 -1
- package/dist/harnesses/pi-durable/reads.js +1 -3
- package/dist/harnesses/pi-durable/registry.d.ts +4 -6
- package/dist/harnesses/pi-durable/registry.js +29 -26
- package/dist/harnesses/pi-durable/release.d.ts +2 -2
- package/dist/harnesses/pi-durable/release.js +10 -7
- package/dist/harnesses/pi-durable/serve.d.ts +91 -15
- package/dist/harnesses/pi-durable/serve.js +109 -47
- package/dist/harnesses/pi-durable/sessions.js +15 -12
- package/dist/harnesses/pi-durable/text-sources.d.ts +30 -0
- package/dist/harnesses/pi-durable/text-sources.js +55 -0
- package/dist/harnesses/pi-durable/text.d.ts +16 -4
- package/dist/harnesses/pi-durable/text.js +115 -59
- package/dist/harnesses/pi-durable/wake.js +8 -8
- package/dist/http/client.js +9 -3
- package/dist/http/contract.d.ts +3 -3
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +4 -1
- package/dist/instance-name.d.ts +7 -0
- package/dist/instance-name.js +26 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/mcp/client.js +11 -2
- package/dist/mcp/config.d.ts +70 -18
- package/dist/mcp/config.js +136 -53
- package/dist/mcp/host.d.ts +5 -2
- package/dist/mcp/host.js +14 -6
- package/dist/mcp/sign-in.js +2 -2
- package/dist/mcp-api.d.ts +14 -0
- package/dist/paths.d.ts +22 -8
- package/dist/paths.js +44 -11
- package/dist/runtime-websocket.d.ts +14 -0
- package/dist/runtime-websocket.js +39 -0
- package/dist/scaffold/add-channel.d.ts +9 -16
- package/dist/scaffold/add-channel.js +13 -83
- package/dist/scaffold/templates/env.example +4 -5
- package/dist/scaffold/templates/fastagent.config.ts +20 -14
- package/dist/scaffold/templates/gitignore +9 -6
- package/dist/scaffold/templates.d.ts +6 -2
- package/dist/scaffold/templates.js +7 -2
- package/dist/scaffold/vendor-skill.js +4 -2
- package/dist/service.d.ts +44 -6
- package/dist/service.js +110 -10
- package/dist/serving-command.d.ts +10 -0
- package/dist/serving-command.js +1 -0
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +1 -1
- package/dist/strip-edges.d.ts +10 -0
- package/dist/strip-edges.js +22 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +0 -1
- package/dist/tunnel.js +2 -8
- package/dist/value-file.d.ts +11 -6
- package/dist/value-file.js +21 -9
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +6 -5
- package/dist/channels/lark/credentials-page.d.ts +0 -25
- package/dist/channels/lark/credentials-page.js +0 -206
- package/dist/channels/lark/onboard.d.ts +0 -52
- package/dist/channels/lark/onboard.js +0 -86
- package/dist/channels/slack/config-api.d.ts +0 -69
- package/dist/channels/slack/config-api.js +0 -162
- package/dist/channels/slack/onboard.d.ts +0 -40
- package/dist/channels/slack/onboard.js +0 -136
- package/dist/channels/slack/onboarding-state.d.ts +0 -31
- package/dist/channels/slack/onboarding-state.js +0 -64
- package/dist/channels/slack/register-webhook.d.ts +0 -12
- package/dist/channels/slack/register-webhook.js +0 -67
- package/dist/channels/slack/setup-server.d.ts +0 -32
- package/dist/channels/slack/setup-server.js +0 -246
- package/dist/cli/add-feishu.d.ts +0 -41
- package/dist/cli/add-feishu.js +0 -391
- package/dist/cli/add-slack.d.ts +0 -18
- package/dist/cli/add-slack.js +0 -216
- package/dist/harnesses/pi-durable/parked.d.ts +0 -18
- package/dist/harnesses/pi-durable/parked.js +0 -27
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page where a person pastes the values a channel's setup needs, for a setup that has no terminal (a coding agent's
|
|
3
|
+
* `--json`): loopback only, one use, and each value goes from the browser to this process and nowhere else (see
|
|
4
|
+
* setup-page.ts for why it is never the agent's conversation). Each value is checked as it is pasted and handed to
|
|
5
|
+
* `accept` at once, so a step that passed stays accepted when another is refused: the form then asks only for what is left.
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { SetupRefused } from "./channels/setup.js";
|
|
10
|
+
import { field, formCard, openButton, privacy, prose, refusal, sendPage, setupRequestUrl, statusCard, step, submit, } from "./setup-page.js";
|
|
11
|
+
function readBody(request) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let body = "";
|
|
14
|
+
let tooLarge = false;
|
|
15
|
+
request.on("data", (chunk) => {
|
|
16
|
+
if (tooLarge)
|
|
17
|
+
return;
|
|
18
|
+
body += String(chunk);
|
|
19
|
+
if (Buffer.byteLength(body) > 16 * 1024)
|
|
20
|
+
tooLarge = true;
|
|
21
|
+
});
|
|
22
|
+
request.on("end", () => resolve(tooLarge ? undefined : body));
|
|
23
|
+
request.on("error", () => resolve(undefined));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// A browser can leave during validation, before either response event can be subscribed to.
|
|
27
|
+
function afterResponse(response, complete) {
|
|
28
|
+
if (response.destroyed || response.writableFinished)
|
|
29
|
+
complete();
|
|
30
|
+
else
|
|
31
|
+
response.once("close", complete);
|
|
32
|
+
}
|
|
33
|
+
export async function startCredentialsPage(input) {
|
|
34
|
+
const path = `/setup/${randomBytes(24).toString("hex")}`;
|
|
35
|
+
const { promise: done, resolve, reject } = Promise.withResolvers();
|
|
36
|
+
const accepted = new Set();
|
|
37
|
+
let checking = false;
|
|
38
|
+
let used = false;
|
|
39
|
+
let port = 0;
|
|
40
|
+
const form = (problem) => formCard(input.title, input.steps
|
|
41
|
+
.map(({ label, guide, fields }, index) => {
|
|
42
|
+
if (accepted.has(index))
|
|
43
|
+
return step(index + 1, label, prose(`Saved.`));
|
|
44
|
+
const here = problem?.step === index ? problem.refusal : undefined;
|
|
45
|
+
return step(index + 1, label, prose(guide.instructions) +
|
|
46
|
+
(guide.url === undefined ? "" : openButton("Open", guide.url)) +
|
|
47
|
+
(here === undefined
|
|
48
|
+
? ""
|
|
49
|
+
: refusal(here.message, here.fix === undefined ? undefined : ` ${here.fix}`, here.why)) +
|
|
50
|
+
fields
|
|
51
|
+
.map((one, position) => field({
|
|
52
|
+
id: one.name,
|
|
53
|
+
label: one.label,
|
|
54
|
+
secret: one.secret ?? true,
|
|
55
|
+
invalid: here !== undefined,
|
|
56
|
+
autofocus: here !== undefined && position === 0,
|
|
57
|
+
}))
|
|
58
|
+
.join(""));
|
|
59
|
+
})
|
|
60
|
+
.join("") +
|
|
61
|
+
submit("Save and check") +
|
|
62
|
+
privacy());
|
|
63
|
+
const server = createServer(async (request, response) => {
|
|
64
|
+
const url = setupRequestUrl(request, response);
|
|
65
|
+
if (url === undefined)
|
|
66
|
+
return;
|
|
67
|
+
const host = request.headers.host ?? "";
|
|
68
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
|
|
69
|
+
response.writeHead(421).end("misdirected request\n");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (url.pathname !== path || (request.method !== "GET" && request.method !== "POST")) {
|
|
73
|
+
response.writeHead(404).end("not found\n");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const expired = () => sendPage(response, 404, {
|
|
77
|
+
title: "FastAgent setup",
|
|
78
|
+
card: statusCard({
|
|
79
|
+
state: "failed",
|
|
80
|
+
heading: "This page is no longer in use",
|
|
81
|
+
next: "Go back to your coding agent.",
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
if (used) {
|
|
85
|
+
expired();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (request.method === "GET") {
|
|
89
|
+
sendPage(response, 200, { title: `${input.title} — FastAgent`, card: form() });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const site = request.headers["sec-fetch-site"];
|
|
93
|
+
if (site !== undefined && site !== "same-origin") {
|
|
94
|
+
response.writeHead(403).end("forbidden\n");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const body = await readBody(request);
|
|
98
|
+
if (body === undefined) {
|
|
99
|
+
response.writeHead(413).end("payload too large\n");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (used) {
|
|
103
|
+
expired();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (checking) {
|
|
107
|
+
response.writeHead(409).end("another submission is being checked\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
checking = true;
|
|
111
|
+
const pasted = new URLSearchParams(body);
|
|
112
|
+
try {
|
|
113
|
+
for (const [index, { fields }] of input.steps.entries()) {
|
|
114
|
+
if (accepted.has(index))
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const values = {};
|
|
118
|
+
for (const { name, label } of fields) {
|
|
119
|
+
const value = (pasted.get(name) ?? "").trim();
|
|
120
|
+
if (value === "")
|
|
121
|
+
throw new SetupRefused(`${label} is needed.`);
|
|
122
|
+
values[name] = value;
|
|
123
|
+
}
|
|
124
|
+
await input.accept(index, values);
|
|
125
|
+
accepted.add(index);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// Only a refusal stays on the form; infrastructure and persistence errors end the setup.
|
|
129
|
+
if (!(error instanceof SetupRefused))
|
|
130
|
+
throw error;
|
|
131
|
+
checking = false;
|
|
132
|
+
sendPage(response, 200, { title: `${input.title} — FastAgent`, card: form({ step: index, refusal: error }) });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
checking = false;
|
|
139
|
+
used = true;
|
|
140
|
+
afterResponse(response, () => reject(error));
|
|
141
|
+
sendPage(response, 200, {
|
|
142
|
+
title: "Setup stopped — FastAgent",
|
|
143
|
+
card: statusCard({
|
|
144
|
+
state: "failed",
|
|
145
|
+
heading: "Setup stopped",
|
|
146
|
+
lede: error instanceof Error ? error.message : String(error),
|
|
147
|
+
next: "Go back to your coding agent.",
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
used = true;
|
|
153
|
+
afterResponse(response, resolve);
|
|
154
|
+
sendPage(response, 200, {
|
|
155
|
+
title: `${input.doneHeading} — FastAgent`,
|
|
156
|
+
card: statusCard({
|
|
157
|
+
state: "done",
|
|
158
|
+
heading: input.doneHeading,
|
|
159
|
+
lede: "Your coding agent will say if anything is left to do.",
|
|
160
|
+
next: "Go back to your coding agent.",
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
await new Promise((resolveListening, rejectListening) => {
|
|
165
|
+
server.once("error", rejectListening);
|
|
166
|
+
server.listen(0, "127.0.0.1", () => {
|
|
167
|
+
server.off("error", rejectListening);
|
|
168
|
+
resolveListening();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
const address = server.address();
|
|
172
|
+
if (!address || typeof address === "string") {
|
|
173
|
+
server.close();
|
|
174
|
+
throw new Error("the credentials page did not receive a local TCP port");
|
|
175
|
+
}
|
|
176
|
+
port = address.port;
|
|
177
|
+
return {
|
|
178
|
+
url: `http://127.0.0.1:${port}${path}`,
|
|
179
|
+
done,
|
|
180
|
+
close: () => new Promise((resolveClosed) => {
|
|
181
|
+
server.close(() => resolveClosed());
|
|
182
|
+
server.closeAllConnections();
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -19,19 +19,26 @@ export interface DeclaredSecret {
|
|
|
19
19
|
name: string;
|
|
20
20
|
/** Where it was declared: "src/tools/x-post.ts", "fastagent.config.ts providers[0]", "context.json" — printed in
|
|
21
21
|
* runbooks and failures, so it must name a place the author can open. */
|
|
22
|
-
|
|
22
|
+
declaredIn: string;
|
|
23
23
|
/** What it is for, when the declaration says (`context.json`'s variables do): printed beside it in runbooks. */
|
|
24
24
|
description?: string;
|
|
25
25
|
/** The code runs without it: no start is refused for its absence. */
|
|
26
|
-
optional
|
|
26
|
+
optional: boolean;
|
|
27
|
+
/** Declared by one way of a channel that receives either way (`byIngress`): read only when it receives that way. */
|
|
28
|
+
when?: {
|
|
29
|
+
channel: string;
|
|
30
|
+
ingress: "webhook" | "connection";
|
|
31
|
+
};
|
|
27
32
|
}
|
|
28
33
|
/** Whether `name` has the shape of an env-var name ({@link ENV_NAME}). */
|
|
29
34
|
export declare function isEnvName(name: string): boolean;
|
|
30
35
|
/** Declared names as a stable list, first declaration winning — the form a carrier iterates. */
|
|
31
36
|
export declare function dedupeSecrets(declared: readonly DeclaredSecret[]): DeclaredSecret[];
|
|
37
|
+
/** The values of `secrets` that are set: what an environment is made of, since a name given no value is not one. */
|
|
38
|
+
export declare function definedValues(secrets: Readonly<Record<string, string | undefined>>): Record<string, string>;
|
|
32
39
|
/** Required names with no value in `env` — empty string counts as missing (an unset `.env` line). A name some
|
|
33
40
|
* declaration requires is required, whatever another says. */
|
|
34
|
-
export declare function missingSecrets(declared: readonly DeclaredSecret[], env
|
|
41
|
+
export declare function missingSecrets(declared: readonly DeclaredSecret[], env: NodeJS.ProcessEnv): DeclaredSecret[];
|
|
35
42
|
/** "X_API_KEY, X_API_SECRET (src/tools/x-post.ts); SLACK_TOKEN (src/channels/slack.ts)" — grouped by the
|
|
36
43
|
* file to open, since that is the unit the author fixes. */
|
|
37
44
|
export declare function describeSecrets(declared: readonly DeclaredSecret[]): string;
|
package/dist/declared-secrets.js
CHANGED
|
@@ -30,17 +30,21 @@ export function dedupeSecrets(declared) {
|
|
|
30
30
|
const seen = new Set();
|
|
31
31
|
return declared.filter((s) => !seen.has(s.name) && seen.add(s.name));
|
|
32
32
|
}
|
|
33
|
+
/** The values of `secrets` that are set: what an environment is made of, since a name given no value is not one. */
|
|
34
|
+
export function definedValues(secrets) {
|
|
35
|
+
return Object.fromEntries(Object.entries(secrets).filter((entry) => entry[1] !== undefined));
|
|
36
|
+
}
|
|
33
37
|
/** Required names with no value in `env` — empty string counts as missing (an unset `.env` line). A name some
|
|
34
38
|
* declaration requires is required, whatever another says. */
|
|
35
|
-
export function missingSecrets(declared, env
|
|
36
|
-
return dedupeSecrets(declared.filter((s) => s.optional
|
|
39
|
+
export function missingSecrets(declared, env) {
|
|
40
|
+
return dedupeSecrets(declared.filter((s) => !s.optional)).filter((s) => !env[s.name]);
|
|
37
41
|
}
|
|
38
42
|
/** "X_API_KEY, X_API_SECRET (src/tools/x-post.ts); SLACK_TOKEN (src/channels/slack.ts)" — grouped by the
|
|
39
43
|
* file to open, since that is the unit the author fixes. */
|
|
40
44
|
export function describeSecrets(declared) {
|
|
41
45
|
const bySource = new Map();
|
|
42
|
-
for (const { name,
|
|
43
|
-
bySource.set(
|
|
46
|
+
for (const { name, declaredIn } of dedupeSecrets(declared)) {
|
|
47
|
+
bySource.set(declaredIn, [...(bySource.get(declaredIn) ?? []), name]);
|
|
44
48
|
}
|
|
45
49
|
return [...bySource].map(([source, names]) => `${names.join(", ")} (${source})`).join("; ");
|
|
46
50
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `destroy agentcore`: removing what a deploy created, INCLUDING the parts the stack does not own.
|
|
3
3
|
*
|
|
4
|
-
* `aws cloudformation delete-stack` is the obvious move and it leaves
|
|
4
|
+
* `aws cloudformation delete-stack` is the obvious move and it leaves most of them behind, each for its own
|
|
5
5
|
* reason:
|
|
6
6
|
*
|
|
7
7
|
* - the S3 artifact bucket and the ECR repository must exist BEFORE the stack (it reads the forwarder's zip and
|
|
8
8
|
* the runtime's image from them), so they cannot be stack resources;
|
|
9
|
+
* - the Secrets Manager secret holds the deployment's values, which must exist before the first deploy and survive a
|
|
10
|
+
* stack update, so it is no stack resource either; it is deleted without a recovery window, since a window would
|
|
11
|
+
* keep its name from the next deployment of the agent (its values are pushed again from their source);
|
|
9
12
|
* - the forwarder's log group is created by AWS on the Lambda's first WRITE, so no template ever mentions it —
|
|
10
13
|
* and the deploy put a 14-day retention on it, i.e. it holds data;
|
|
11
14
|
* - the EventBridge schedules are minted at RUNTIME by the container: a recurring one per `schedules/` file, which
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { awsCli, awsJson, parseLogGroupNames } from "./aws-cli.js";
|
|
2
|
-
import { agentcoreRepoName, agentcoreStackName, deploymentBucketName, forwarderLogGroup, runtimeLogGroupPrefix, recurringSchedulePrefix, toRuntimeName, wakeAlarmPrefix, } from "./plan.js";
|
|
2
|
+
import { agentcoreRepoName, agentcoreSecretName, agentcoreStackName, deploymentBucketName, forwarderLogGroup, runtimeLogGroupPrefix, recurringSchedulePrefix, toRuntimeName, wakeAlarmPrefix, } from "./plan.js";
|
|
3
3
|
/** S3 deletes at most 1000 keys per call. */
|
|
4
4
|
const DELETE_BATCH = 1000;
|
|
5
5
|
/**
|
|
@@ -109,6 +109,19 @@ export async function destroyAgentcoreDeployment(plan, runner, announce = () =>
|
|
|
109
109
|
return gate(`could not tell whether repository ${repo} exists in ${region}: ${repoRead.unreadable}`);
|
|
110
110
|
}
|
|
111
111
|
const repoExists = "ok" in repoRead;
|
|
112
|
+
const secretName = agentcoreSecretName(plan.name);
|
|
113
|
+
const secretRead = await aws.present([
|
|
114
|
+
"secretsmanager",
|
|
115
|
+
"describe-secret",
|
|
116
|
+
"--secret-id",
|
|
117
|
+
secretName,
|
|
118
|
+
"--output",
|
|
119
|
+
"json",
|
|
120
|
+
]);
|
|
121
|
+
if ("unreadable" in secretRead) {
|
|
122
|
+
return gate(`could not tell whether secret ${secretName} exists in ${region}: ${secretRead.unreadable}`);
|
|
123
|
+
}
|
|
124
|
+
const secretExists = "ok" in secretRead;
|
|
112
125
|
// BOTH LOG GROUPS. AWS creates each on the first WRITE, so neither is a stack resource — same reason, twice —
|
|
113
126
|
// and the runtime's holds the agent's own stdout/stderr, i.e. what it said in every conversation. `deploy`'s
|
|
114
127
|
// runbook already knows there are two: it sets a retention on each.
|
|
@@ -173,6 +186,8 @@ export async function destroyAgentcoreDeployment(plan, runner, announce = () =>
|
|
|
173
186
|
found.push(`bucket ${bucket} (${keys.length} object version(s))`);
|
|
174
187
|
if (repoExists)
|
|
175
188
|
found.push(`repository ${repo}`);
|
|
189
|
+
if (secretExists)
|
|
190
|
+
found.push(`secret ${secretName} (its values are deleted for good, with no recovery window)`);
|
|
176
191
|
for (const name of listedGroups)
|
|
177
192
|
found.push(`log group ${name}`);
|
|
178
193
|
// The one thing this command refuses to delete.
|
|
@@ -245,6 +260,15 @@ export async function destroyAgentcoreDeployment(plan, runner, announce = () =>
|
|
|
245
260
|
if (repoExists) {
|
|
246
261
|
await attempt(`repository ${repo}`, ["ecr", "delete-repository", "--repository-name", repo, "--force"]);
|
|
247
262
|
}
|
|
263
|
+
if (secretExists) {
|
|
264
|
+
await attempt(`secret ${secretName}`, [
|
|
265
|
+
"secretsmanager",
|
|
266
|
+
"delete-secret",
|
|
267
|
+
"--secret-id",
|
|
268
|
+
secretName,
|
|
269
|
+
"--force-delete-without-recovery",
|
|
270
|
+
]);
|
|
271
|
+
}
|
|
248
272
|
for (const name of logGroups) {
|
|
249
273
|
await attempt(`log group ${name}`, ["logs", "delete-log-group", "--log-group-name", name]);
|
|
250
274
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by `fastagent deploy agentcore` — the deployment's only ingress.
|
|
2
2
|
// (Inside fastagent this file IS the source; the sentence below applies to the copy it is emitted as,
|
|
3
|
-
// the agent directory's `
|
|
3
|
+
// the agent directory's `agentcore/forwarder/index.js`.)
|
|
4
4
|
// REGENERATED ON EVERY DEPLOY; edits here are overwritten and never deployed. `--run` builds the
|
|
5
5
|
// Lambda package from fastagent's own copy of this source, so this file is the readable MIRROR of
|
|
6
6
|
// what runs (and what the manual runbook zips) — not an input you can change.
|
|
@@ -10,8 +10,7 @@ export interface AgentcorePlanInput extends ContainerInput {
|
|
|
10
10
|
*/
|
|
11
11
|
channels: readonly DeclaredChannel[];
|
|
12
12
|
/**
|
|
13
|
-
* The runbook's variable list (`deploymentSecrets`):
|
|
14
|
-
* carries.
|
|
13
|
+
* The runbook's variable list (`deploymentSecrets`): the declared names, each hinted by the file that declared it.
|
|
15
14
|
*/
|
|
16
15
|
secrets?: readonly DeploymentSecret[];
|
|
17
16
|
/**
|
|
@@ -51,7 +50,7 @@ export declare const SECRETS_DIR = "/mnt/data/.secrets";
|
|
|
51
50
|
export declare const DEFAULT_IDLE_TIMEOUT_SECONDS = 180;
|
|
52
51
|
/** One session's compute lifetime — AWS's own default. Both lifecycle fields cap at 1209600 (14 d). */
|
|
53
52
|
export declare const MAX_LIFETIME_SECONDS = 28800;
|
|
54
|
-
export declare const FORWARDER_FILE = "
|
|
53
|
+
export declare const FORWARDER_FILE = "agentcore/forwarder/index.js";
|
|
55
54
|
/**
|
|
56
55
|
* Where the forwarder puts the one-shot schedule for a wake-up. The id inside the name is minted in the
|
|
57
56
|
* container, so this prefix is the only handle anything outside has on the alarms a deployment has pending.
|
|
@@ -64,23 +63,10 @@ export declare function wakeAlarmPrefix(name: string): string;
|
|
|
64
63
|
export declare function recurringSchedulePrefix(name: string): string;
|
|
65
64
|
/** Account-suffixed artifact bucket for S3's global namespace. */
|
|
66
65
|
export declare function deploymentBucketName(name: string, account: string): string;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export declare const CARRIER_CHUNK_SIZE = 2000;
|
|
72
|
-
export declare const CARRIER_MAX_CHUNKS = 4;
|
|
73
|
-
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
74
|
-
export declare const CARRIERS: readonly [{
|
|
75
|
-
readonly param: "FastagentEnv";
|
|
76
|
-
readonly env: "FASTAGENT_ENV";
|
|
77
|
-
readonly what: "base64 JSON of the value file's variables";
|
|
78
|
-
}];
|
|
79
|
-
/** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
|
|
80
|
-
export declare function carrierChunk(carrier: (typeof CARRIERS)[number], i: number): {
|
|
81
|
-
param: string;
|
|
82
|
-
env: string;
|
|
83
|
-
};
|
|
66
|
+
/** The Secrets Manager secret holding a deployment's values: one JSON object of NAME → value (agent-artifact.md §6.2). */
|
|
67
|
+
export declare function agentcoreSecretName(name: string): string;
|
|
68
|
+
/** The environment variable that tells the container which secret to read at its first invocation. */
|
|
69
|
+
export declare const SECRET_ARN_ENV = "FASTAGENT_SECRET_ARN";
|
|
84
70
|
/** The generated template's filename (namespaced under the kit in the agentDir layout). */
|
|
85
71
|
export declare const TEMPLATE_FILE = "agentcore.template.yaml";
|
|
86
72
|
/** The generated template's first-line marker. */
|
|
@@ -122,7 +108,7 @@ export declare const PROBE_PAYLOAD: string;
|
|
|
122
108
|
export declare function ingressSessionId(name: string): string;
|
|
123
109
|
/**
|
|
124
110
|
* The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment package and the
|
|
125
|
-
* readable `
|
|
111
|
+
* readable `agentcore/forwarder/index.js` artifact are generated from.
|
|
126
112
|
*/
|
|
127
113
|
export declare function forwarderSource(): string;
|
|
128
114
|
/** Compute the AgentCore deploy plan from the resolved definition. */
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { MAX_WEBHOOK_BODY_BYTES } from "../../channels/agentcore-limits.js";
|
|
4
4
|
import { SECRETS_DIRNAME } from "../../paths.js";
|
|
5
|
-
import { webhookKinds
|
|
5
|
+
import { webhookKinds } from "../../channels/webhooks.js";
|
|
6
|
+
import { webhookRunbook } from "../channel-ingress.js";
|
|
6
7
|
import { containerArtifacts } from "../container.js";
|
|
7
8
|
import { deploymentLoginCommand } from "../box-shell.js";
|
|
9
|
+
import { stripEdges } from "../../strip-edges.js";
|
|
8
10
|
function agentcoreTopology(input) {
|
|
9
11
|
return { webhooks: input.channels.some((channel) => channel.ingress === "webhook") };
|
|
10
12
|
}
|
|
@@ -19,7 +21,7 @@ export const SECRETS_DIR = `${MOUNT}/${SECRETS_DIRNAME}`;
|
|
|
19
21
|
export const DEFAULT_IDLE_TIMEOUT_SECONDS = 180;
|
|
20
22
|
/** One session's compute lifetime — AWS's own default. Both lifecycle fields cap at 1209600 (14 d). */
|
|
21
23
|
export const MAX_LIFETIME_SECONDS = 28800;
|
|
22
|
-
export const FORWARDER_FILE = "
|
|
24
|
+
export const FORWARDER_FILE = "agentcore/forwarder/index.js";
|
|
23
25
|
/**
|
|
24
26
|
* Where the forwarder puts the one-shot schedule for a wake-up. The id inside the name is minted in the
|
|
25
27
|
* container, so this prefix is the only handle anything outside has on the alarms a deployment has pending.
|
|
@@ -38,22 +40,12 @@ export function recurringSchedulePrefix(name) {
|
|
|
38
40
|
export function deploymentBucketName(name, account) {
|
|
39
41
|
return `fa-${name}-${account}`;
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
export const CARRIER_CHUNK_SIZE = 2000;
|
|
46
|
-
export const CARRIER_MAX_CHUNKS = 4;
|
|
47
|
-
/** The template's chunked carrier: CloudFormation parameter prefix → container env-var name. */
|
|
48
|
-
export const CARRIERS = [
|
|
49
|
-
{ param: "FastagentEnv", env: "FASTAGENT_ENV", what: "base64 JSON of the value file's variables" },
|
|
50
|
-
];
|
|
51
|
-
/** Chunk `i` (0-based) of a carrier: its parameter and env-var names. */
|
|
52
|
-
export function carrierChunk(carrier, i) {
|
|
53
|
-
return i === 0
|
|
54
|
-
? { param: carrier.param, env: carrier.env }
|
|
55
|
-
: { param: `${carrier.param}${i + 1}`, env: `${carrier.env}_${i + 1}` };
|
|
43
|
+
/** The Secrets Manager secret holding a deployment's values: one JSON object of NAME → value (agent-artifact.md §6.2). */
|
|
44
|
+
export function agentcoreSecretName(name) {
|
|
45
|
+
return `fastagent/${name}`;
|
|
56
46
|
}
|
|
47
|
+
/** The environment variable that tells the container which secret to read at its first invocation. */
|
|
48
|
+
export const SECRET_ARN_ENV = "FASTAGENT_SECRET_ARN";
|
|
57
49
|
/** The generated template's filename (namespaced under the kit in the agentDir layout). */
|
|
58
50
|
export const TEMPLATE_FILE = "agentcore.template.yaml";
|
|
59
51
|
/** The generated template's first-line marker. */
|
|
@@ -64,14 +56,11 @@ export function isGeneratedAgentcoreTemplate(content) {
|
|
|
64
56
|
}
|
|
65
57
|
/** Deployment base name from the agent directory's name — the ONE mapping used to find its stack later. */
|
|
66
58
|
export function agentcoreName(agentDirName) {
|
|
67
|
-
return (agentDirName
|
|
68
|
-
.toLowerCase()
|
|
69
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
70
|
-
.replace(/^-+|-+$/g, "") || "agent");
|
|
59
|
+
return stripEdges(agentDirName.toLowerCase().replace(/[^a-z0-9-]+/g, "-"), (char) => char === "-") || "agent";
|
|
71
60
|
}
|
|
72
61
|
/** Runtime name (`[a-zA-Z][a-zA-Z0-9_]{0,47}`) from a dir basename. */
|
|
73
62
|
export function toRuntimeName(basename) {
|
|
74
|
-
const slug = basename.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
63
|
+
const slug = stripEdges(basename.replace(/[^a-zA-Z0-9]+/g, "_"), (char) => char === "_");
|
|
75
64
|
return (/^[a-zA-Z]/.test(slug) ? slug : `agent_${slug || "fastagent"}`).slice(0, 48);
|
|
76
65
|
}
|
|
77
66
|
/** The forwarder Lambda's name — and therefore its log group, which AWS derives from it. */
|
|
@@ -119,7 +108,7 @@ export function ingressSessionId(name) {
|
|
|
119
108
|
}
|
|
120
109
|
/**
|
|
121
110
|
* The forwarder Lambda source — `forwarder.js` beside this file, the ONE text both the deployment package and the
|
|
122
|
-
* readable `
|
|
111
|
+
* readable `agentcore/forwarder/index.js` artifact are generated from.
|
|
123
112
|
*/
|
|
124
113
|
export function forwarderSource() {
|
|
125
114
|
return readFileSync(new URL("./forwarder.js", import.meta.url), "utf8");
|
|
@@ -129,30 +118,20 @@ function template(input, topology) {
|
|
|
129
118
|
const runtimeName = toRuntimeName(input.name);
|
|
130
119
|
const idleTimeout = input.idleTimeoutSeconds ?? DEFAULT_IDLE_TIMEOUT_SECONDS;
|
|
131
120
|
const forwarderFnArn = `!Sub arn:aws:lambda:\${AWS::Region}:\${AWS::AccountId}:function:fastagent-${input.name}-forwarder`;
|
|
132
|
-
// Secret env vars ride CFN NoEcho parameters.
|
|
133
121
|
const params = [
|
|
134
122
|
` ImageUri:`,
|
|
135
123
|
` Type: String`,
|
|
136
124
|
` Description: ECR image URI (linux/arm64) — <account>.dkr.ecr.<region>.amazonaws.com/<repo>:<tag>`,
|
|
137
125
|
];
|
|
138
|
-
params.push(` ForwarderBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: content-hashed key of the forwarder .zip in ForwarderBucket`);
|
|
126
|
+
params.push(` SecretArn:`, ` Type: String`, ` Description: ARN of the Secrets Manager secret holding the deployment's values (created outside this stack by \`fastagent secrets\`; the runtime reads it on its first invocation)`, ` ForwarderBucket:`, ` Type: String`, ` Description: S3 bucket holding the forwarder deployment package (created outside this stack)`, ` ForwarderS3Key:`, ` Type: String`, ` Description: content-hashed key of the forwarder .zip in ForwarderBucket`);
|
|
139
127
|
const envLines = [
|
|
140
128
|
` PORT: "8080"`, // the Runtime service contract's fixed port (config.http.port does not apply here)
|
|
141
129
|
` FASTAGENT_AGENTCORE: "1"`, // serve mounts /invocations + /ping, arms no resident cron
|
|
142
|
-
`
|
|
130
|
+
` FASTAGENT_ROOT: ${MOUNT}`,
|
|
143
131
|
` FASTAGENT_STATE_DIR: ${MOUNT}/.state`,
|
|
144
132
|
` FASTAGENT_SECRETS_DIR: ${SECRETS_DIR}`,
|
|
133
|
+
` ${SECRET_ARN_ENV}: !Ref SecretArn`,
|
|
145
134
|
];
|
|
146
|
-
// The carrier is chunked (env values max 2048 chars — see CARRIER_CHUNK_SIZE): N parameters, each riding its
|
|
147
|
-
// own env var; `start` reassembles them. ONE parameter set for every variable, so the template does not change
|
|
148
|
-
// when the value file gains a name.
|
|
149
|
-
for (const carrier of CARRIERS) {
|
|
150
|
-
for (let i = 0; i < CARRIER_MAX_CHUNKS; i++) {
|
|
151
|
-
const { param, env } = carrierChunk(carrier, i);
|
|
152
|
-
params.push(` ${param}:`, ` Type: String`, ` Default: ""`, ` NoEcho: true`, ` Description: ${carrier.what} carried by --run, chunk ${i + 1}/${CARRIER_MAX_CHUNKS} (env values cap at 2048 chars); empty = unused`);
|
|
153
|
-
envLines.push(` ${env}: !Ref ${param}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
135
|
// IAM invocation permission does not prove forwarder origin. Internal envelopes need a secret.
|
|
157
136
|
params.push(` FastagentIngressSecret:`, ` Type: String`, ` NoEcho: true`, ` Description: shared secret authenticating forwarder→runtime envelopes (any random string; --run mints one)`);
|
|
158
137
|
envLines.push(` FASTAGENT_INGRESS_SECRET: !Ref FastagentIngressSecret`);
|
|
@@ -199,6 +178,9 @@ function template(input, topology) {
|
|
|
199
178
|
` - Effect: Allow # AgentCore workload identity (the platform mints one per runtime)`,
|
|
200
179
|
` Action: [bedrock-agentcore:GetWorkloadAccessToken]`,
|
|
201
180
|
` Resource: "*"`,
|
|
181
|
+
` - Effect: Allow # the deployment's values: this one secret, read on the first invocation`,
|
|
182
|
+
` Action: [secretsmanager:GetSecretValue]`,
|
|
183
|
+
` Resource: !Ref SecretArn`,
|
|
202
184
|
``,
|
|
203
185
|
` Runtime:`,
|
|
204
186
|
` Type: AWS::BedrockAgentCore::Runtime`,
|
|
@@ -251,7 +233,12 @@ export function planAgentcoreDeploy(input) {
|
|
|
251
233
|
`# Prereqs: AWS CLI v2 with credentials + a region where AgentCore is available, and Docker with buildx`,
|
|
252
234
|
`# (the image MUST be linux/arm64 — the one host whose build runs on YOUR machine, not remotely).`,
|
|
253
235
|
``,
|
|
254
|
-
`#
|
|
236
|
+
`# 0. First use of AgentCore in this account only: the runtime needs its service-linked role, and without it`,
|
|
237
|
+
`# CloudFormation fails at the runtime, after the build, with a bare AccessDenied. Skip if \`aws iam get-role`,
|
|
238
|
+
`# --role-name AWSServiceRoleForBedrockAgentCoreRuntimeIdentity\` finds it.`,
|
|
239
|
+
`aws iam create-service-linked-role --aws-service-name runtime-identity.bedrock-agentcore.amazonaws.com`,
|
|
240
|
+
``,
|
|
241
|
+
`# 1. ECR repository, the forwarder artifact bucket and the deployment's values secret (one-time; skip what exists).`,
|
|
255
242
|
`aws ecr create-repository --repository-name ${repo}`,
|
|
256
243
|
`aws s3api create-bucket --bucket ${bucketHint} --region us-east-1 # us-east-1 ONLY`,
|
|
257
244
|
`aws s3api create-bucket --bucket ${bucketHint} --region <region> \\ # every OTHER region`,
|
|
@@ -259,10 +246,16 @@ export function planAgentcoreDeploy(input) {
|
|
|
259
246
|
`aws s3api put-public-access-block --bucket ${bucketHint} \\`,
|
|
260
247
|
` --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true`,
|
|
261
248
|
``,
|
|
249
|
+
`aws secretsmanager create-secret --name ${agentcoreSecretName(name)} --secret-string '{}'`,
|
|
250
|
+
...(secrets.length > 0
|
|
251
|
+
? [`# Its values, one command each (or \`fastagent secrets push agentcore\` for a file or the environment):`]
|
|
252
|
+
: []),
|
|
253
|
+
...secrets.map((s) => `# fastagent secrets set agentcore ${s.name} # ${s.hint}`),
|
|
254
|
+
``,
|
|
262
255
|
`# 1b. Package the forwarder and upload it. Name the object by its CONTENT (a hash/date):`,
|
|
263
256
|
`# CloudFormation rolls the function only when the ForwarderS3Key VALUE changes.`,
|
|
264
|
-
`(cd
|
|
265
|
-
`aws s3 cp
|
|
257
|
+
`(cd agentcore/forwarder && zip -q forwarder.zip index.js)`,
|
|
258
|
+
`aws s3 cp agentcore/forwarder/forwarder.zip s3://${bucketHint}/forwarder/<hash>.zip`,
|
|
266
259
|
``,
|
|
267
260
|
`# 2. Build (linux/arm64) + push. Use a UNIQUE tag per deploy (a git sha / date): CloudFormation only`,
|
|
268
261
|
`# rolls the runtime when the ImageUri VALUE changes — re-pushing the same tag deploys nothing.`,
|
|
@@ -271,19 +264,17 @@ export function planAgentcoreDeploy(input) {
|
|
|
271
264
|
``,
|
|
272
265
|
`# 3. Deploy the stack (runtime + ingress in one template).`,
|
|
273
266
|
];
|
|
274
|
-
|
|
275
|
-
runbook.push(`# Variables ride ONE NoEcho parameter, FastagentEnv: base64 of a JSON object {"NAME": "value", …},`, `# split every ${CARRIER_CHUNK_SIZE} chars across FastagentEnv, FastagentEnv2 … (\`--run\` builds it):`, ...secrets.map((s) => `# ${s.name}: ${s.hint}`));
|
|
276
|
-
}
|
|
267
|
+
runbook.push(`# SecretArn: the ARN of the secret made in step 1 (aws secretsmanager describe-secret --secret-id ${agentcoreSecretName(name)} --query ARN)`);
|
|
277
268
|
const wakeSecretHint = " FastagentWakeSecret=<any random string> FastagentIngressSecret=<another random string>";
|
|
278
269
|
runbook.push(`# FastagentWakeSecret: the wake-alarm shared secret — any random string (\`--run\` mints one)`);
|
|
279
270
|
runbook.push(`# FastagentIngressSecret: what proves an envelope came from the forwarder — another random string, kept for the activation below`);
|
|
280
|
-
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${
|
|
271
|
+
runbook.push(`aws cloudformation deploy --stack-name ${stack} --template-file ${TEMPLATE_FILE} \\`, ` --capabilities CAPABILITY_IAM \\`, ` --parameter-overrides ImageUri=${image} SecretArn=<arn> ForwarderBucket=${bucketHint} ForwarderS3Key=forwarder/<hash>.zip${wakeSecretHint}`, ``, `# 4. Read the outputs (the runtime ARN + callback URL; it serves webhooks only when configured):`, `aws cloudformation describe-stacks --stack-name ${stack} --query "Stacks[0].Outputs"`, ``, `# 5. Tail the Runtime's application stdout/stderr (same fastagent messages + log level as locally).`, `# Discovery resolves the per-endpoint log group from the stack's RuntimeArn:`, `fastagent logs agentcore --follow`, `# The ingress transport is a separate Lambda and therefore a separate log source:`, `fastagent logs agentcore --source forwarder --follow`, ``, `# 6. Set a log retention period on EVERY group this stack writes to. Each is created by the service that`, `# writes it, not by this template, and CloudWatch keeps log data indefinitely by default — which is`, `# forever-billed storage, and the logs are where WHY a scheduled turn failed is recorded. Logs are the`, `# ONE state path this host does not reclaim on its own.`, `aws logs put-retention-policy --log-group-name <the group \`fastagent logs agentcore\` resolves> --retention-in-days 14`, `# The forwarder is a separate Lambda, so its group is separate and defaults the same way:`, `aws logs put-retention-policy --log-group-name ${forwarderLogGroup(input.name)} --retention-in-days 14`);
|
|
281
272
|
if (input.boxLogin) {
|
|
282
|
-
runbook.push(``, `# Model auth: after EVERY deploy and after 14 idle days (each storage reset wipes the last login),`, `# log the runtime in to ${input.boxLogin} from the agent directory
|
|
273
|
+
runbook.push(``, `# Model auth: after EVERY deploy and after 14 idle days (each storage reset wipes the last login),`, `# log the runtime in to ${input.boxLogin} from the agent directory, once the probe below has opened it (a message`, `# that reaches the runtime before the login fails):`, `${deploymentLoginCommand("agentcore", input.boxLogin)}`);
|
|
283
274
|
}
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
const post = webhookRunbook(`<ForwarderUrl>`, channels);
|
|
275
|
+
// The runtime points the channels its own credentials can at the forwarder's Function URL as the probe below wakes
|
|
276
|
+
// it; only Slack's URL, and Lark's where its config API answers 404, is a person's step.
|
|
277
|
+
const post = webhookRunbook(`<ForwarderUrl>`, channels.filter((channel) => channel.name === "slack"));
|
|
287
278
|
// AgentCore asides the shared steps cannot carry: nothing else routes through a forwarder, and nothing else has a
|
|
288
279
|
// compute ceiling.
|
|
289
280
|
if (webhookKinds(channels).some((kind) => kind === "feishu" || kind === "lark")) {
|
|
@@ -291,8 +282,15 @@ export function planAgentcoreDeploy(input) {
|
|
|
291
282
|
}
|
|
292
283
|
if (post.length > 0)
|
|
293
284
|
runbook.push(``, ...post);
|
|
294
|
-
|
|
285
|
+
const own = webhookKinds(channels).filter((kind) => kind !== "slack");
|
|
286
|
+
runbook.push(``, `# Schedules and wake-ups are EventBridge-backed: the container mirrors (via the forwarder, authenticated by`, `# FastagentWakeSecret) each schedule into a recurring EventBridge schedule (${recurringSchedulePrefix(name)}*) and each pending`, `# wake-up into a self-deleting one-shot (${wakeAlarmPrefix(name)}*), which wake it even when the compute is reclaimed.`, `# So a schedule written or edited on the runtime takes effect without a deploy. The container sets them only`, `# once an envelope has reached it THROUGH THE FORWARDER, which tells it where the forwarder is; an`, `# invoke-agent-runtime call does not (that door is IAM's, and does not carry it). \`--run\` does this with its`, `# probe. After a manual deploy, do it once, or no schedule fires until a webhook arrives:`, `curl -fsS -X POST "<ForwarderUrl>/__fastagent/probe" -d '{"auth":"<FastagentIngressSecret>"}'`, ...(own.length > 0
|
|
287
|
+
? [
|
|
288
|
+
`# The same envelope makes the runtime point ${own.join(", ")} at <ForwarderUrl> (or the configuration's`,
|
|
289
|
+
`# http.publicUrl) itself, on every start; the answer's "webhooks" says how each one went, "pending" until it ends`,
|
|
290
|
+
`# (a step only a person can take is in the runtime's log). Repeat the call to read it again.`,
|
|
291
|
+
]
|
|
292
|
+
: []));
|
|
295
293
|
runbook.push(``, `# Check that the deployed agent opens ({"ok":true}); it is reached through its channels, not this call:`, `aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}" \\`, ` --payload '${PROBE_PAYLOAD}' --cli-binary-format raw-in-base64-out /dev/stdout`);
|
|
296
|
-
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage. It keeps
|
|
294
|
+
runbook.push(``, `# Stop the fixed runtime session after redeploy so the next call uses the new image.`, `# \`--run\` does this automatically. In-flight turns are interrupted.`, `aws bedrock-agentcore stop-runtime-session --agent-runtime-arn <RuntimeArn> \\`, ` --runtime-session-id "${ingressSessionId(name)}"`, ``, `# Redeploy = \`fastagent deploy agentcore\` (a new definition needs a fresh release manifest) + step 1b`, `# (new forwarder key, if its code changed) + step 2 with a NEW tag + step 3.`, `# STATE: ${MOUNT} is managed SessionStorage, the agent's working directory. It keeps what the agent wrote there, .state/`, `# and .secrets/ across compute stop/resume — an idle-reclaimed agent resumes with its memory. AWS`, `# RESETS it on every runtime version update (i.e. every deploy) and after 14 idle days, so a deploy`, `# replaces the state along with the image: sessions, channel state and pending wake-ups start blank.`, `# A provider API key in the deployment's secret survives every deploy; a login on the runtime survives neither reset.`, `# Cross-deploy memory needs EFS or S3 Files, which are VPC-only (a NAT gateway for model/channel`, `# egress, ~$33/mo standing): use \`deploy fly\` or \`deploy railway\` for a real volume instead.`, `# Keep one runtime writer per agent; use the fixed runtime session id printed above for every entry point.`);
|
|
297
295
|
return { artifacts, runbook, topology };
|
|
298
296
|
}
|