@fastagent-sh/fastagent 0.28.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 +10 -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 +3 -1
- package/dist/channels/feishu/feishu-api.js +4 -0
- package/dist/channels/feishu/feishu.d.ts +0 -3
- package/dist/channels/feishu/feishu.js +10 -10
- package/dist/channels/feishu/register-app.d.ts +2 -0
- package/dist/channels/feishu/register-app.js +4 -6
- 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/wait-health.js +1 -1
- 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 -310
- 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 -67
- 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 +63 -24
- 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.d.ts +12 -0
- package/dist/cli/commands/mcp.js +164 -0
- 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 +339 -108
- 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 +26 -2
- 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 -11
- 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 +80 -39
- 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 +18 -43
- package/dist/deploy/docker/run.js +57 -79
- 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 -22
- package/dist/deploy/fly/run.js +25 -44
- 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 -7
- 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.d.ts +3 -0
- package/dist/harnesses/pi-durable/mcp.js +32 -0
- package/dist/harnesses/pi-durable/open.d.ts +36 -38
- package/dist/harnesses/pi-durable/open.js +97 -150
- 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 +95 -15
- package/dist/harnesses/pi-durable/serve.js +151 -59
- 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/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +8 -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/locked-file.d.ts +2 -2
- package/dist/locked-file.js +8 -3
- package/dist/mcp/auth.d.ts +22 -0
- package/dist/mcp/auth.js +180 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +179 -0
- package/dist/mcp/config.d.ts +86 -0
- package/dist/mcp/config.js +225 -0
- package/dist/mcp/grants.d.ts +4 -0
- package/dist/mcp/grants.js +132 -0
- package/dist/mcp/host.d.ts +19 -0
- package/dist/mcp/host.js +128 -0
- package/dist/mcp/network.d.ts +12 -0
- package/dist/mcp/network.js +150 -0
- package/dist/mcp/sign-in.d.ts +2 -0
- package/dist/mcp/sign-in.js +204 -0
- package/dist/mcp-api.d.ts +107 -0
- package/dist/mcp-api.js +10 -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/setup-page.d.ts +51 -0
- package/dist/setup-page.js +149 -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 +12 -6
- package/dist/value-file.js +36 -25
- package/dist/version.d.ts +7 -1
- package/dist/version.js +9 -3
- package/package.json +8 -6
- package/dist/channels/lark/onboard.d.ts +0 -36
- package/dist/channels/lark/onboard.js +0 -56
- 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 -201
- package/dist/cli/add-feishu.d.ts +0 -50
- package/dist/cli/add-feishu.js +0 -367
- 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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentCore's values as a deployment's store: one Secrets Manager secret per deployment, a JSON object of NAME → value
|
|
3
|
+
* that the container reads on its first invocation (runtime-secrets.ts). It lives outside the stack, as the ECR
|
|
4
|
+
* repository and the artifact bucket do: its values must exist before the first deploy, and a stack update must not be
|
|
5
|
+
* what rewrites them.
|
|
6
|
+
*
|
|
7
|
+
* Reading goes around `awsCli().read`, whose unreadable answer quotes stdout: here stdout is every value.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { SECRET_FILE_MODE } from "../../paths.js";
|
|
13
|
+
import { awsCli, awsJson } from "./aws-cli.js";
|
|
14
|
+
const describe = (secretName, ...query) => [
|
|
15
|
+
"secretsmanager",
|
|
16
|
+
"describe-secret",
|
|
17
|
+
"--secret-id",
|
|
18
|
+
secretName,
|
|
19
|
+
...query,
|
|
20
|
+
];
|
|
21
|
+
/** The values the secret holds now. A secret that is not a JSON object of strings is refused, naming nothing in it. */
|
|
22
|
+
function parseValues(secretString) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(secretString);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("its value is not JSON");
|
|
29
|
+
}
|
|
30
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31
|
+
throw new Error("its value is not a JSON object of NAME → value");
|
|
32
|
+
}
|
|
33
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
34
|
+
if (typeof value !== "string")
|
|
35
|
+
throw new Error(`its value for ${name} is not a string`);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
export function agentcoreSecretStore(aws, secretName, log) {
|
|
40
|
+
const cli = awsCli(aws);
|
|
41
|
+
const current = async () => {
|
|
42
|
+
const { code, stdout, stderr } = await aws(["secretsmanager", "get-secret-value", "--secret-id", secretName, "--query", "SecretString", "--output", "text"], { capture: true, captureStderr: true });
|
|
43
|
+
if (code !== 0) {
|
|
44
|
+
throw new Error(`\`aws secretsmanager get-secret-value\` failed: ${(stderr ?? "").trim().slice(0, 300)}`);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return parseValues(stdout.trim());
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error(`the secret ${secretName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const store = {
|
|
54
|
+
async exists() {
|
|
55
|
+
const read = await cli.read(describe(secretName, "--output", "json"), awsJson((parsed) => parsed ?? undefined));
|
|
56
|
+
if ("unreadable" in read)
|
|
57
|
+
throw new Error(`could not read secret ${secretName}: ${read.unreadable}`);
|
|
58
|
+
if ("absent" in read)
|
|
59
|
+
return false;
|
|
60
|
+
if (read.ok.DeletedDate !== undefined) {
|
|
61
|
+
throw new Error(`secret ${secretName} is scheduled for deletion: \`aws secretsmanager restore-secret --secret-id ${secretName}\` ` +
|
|
62
|
+
`brings it back, or wait for the deletion and re-run`);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
async ensure() {
|
|
67
|
+
if (await store.exists())
|
|
68
|
+
return;
|
|
69
|
+
log(`creating secret ${secretName}…`);
|
|
70
|
+
const created = await cli.write([
|
|
71
|
+
"secretsmanager",
|
|
72
|
+
"create-secret",
|
|
73
|
+
"--name",
|
|
74
|
+
secretName,
|
|
75
|
+
"--description",
|
|
76
|
+
"FastAgent deployment values",
|
|
77
|
+
"--secret-string",
|
|
78
|
+
"{}",
|
|
79
|
+
]);
|
|
80
|
+
if (!("done" in created)) {
|
|
81
|
+
throw new Error(`\`aws secretsmanager create-secret\` failed: ${"refused" in created ? created.refused : ""}`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async names() {
|
|
85
|
+
return new Set(Object.keys(await current()));
|
|
86
|
+
},
|
|
87
|
+
async write(values) {
|
|
88
|
+
const names = Object.keys(values);
|
|
89
|
+
log(`setting ${names.length} value(s) in ${secretName}: ${names.join(", ")}`);
|
|
90
|
+
// Read, merge, write: one operator at a time, as every host's store assumes.
|
|
91
|
+
const merged = { ...(await current()), ...values };
|
|
92
|
+
// Over a file only this process can read, never argv.
|
|
93
|
+
const dir = await mkdtemp(join(tmpdir(), "fastagent-secret-"));
|
|
94
|
+
try {
|
|
95
|
+
const file = join(dir, "secret.json");
|
|
96
|
+
await writeFile(file, JSON.stringify(merged), { mode: SECRET_FILE_MODE });
|
|
97
|
+
const { code, stderr } = await aws(["secretsmanager", "put-secret-value", "--secret-id", secretName, "--secret-string", `file://${file}`], { capture: true, captureStderr: true });
|
|
98
|
+
if (code !== 0) {
|
|
99
|
+
throw new Error(`\`aws secretsmanager put-secret-value\` failed: ${(stderr ?? "").trim().slice(0, 300)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await rm(dir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
async arn() {
|
|
107
|
+
const read = await cli.read(describe(secretName, "--query", "ARN", "--output", "text"), (stdout) => stdout.trim() || undefined);
|
|
108
|
+
if (!("ok" in read)) {
|
|
109
|
+
throw new Error(`could not read the ARN of secret ${secretName}: ${"unreadable" in read ? read.unreadable : "absent"}`);
|
|
110
|
+
}
|
|
111
|
+
return read.ok;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
return store;
|
|
115
|
+
}
|
package/dist/deploy/api.d.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Whether the deployment serves the API
|
|
1
|
+
import type { EffectiveConfig } from "../harnesses/pi-durable/config.ts";
|
|
2
|
+
/** Whether the deployment serves the API, and why not when it does not. */
|
|
3
3
|
export type DeploymentApi = {
|
|
4
4
|
served: true;
|
|
5
|
-
generatesKey: boolean;
|
|
6
5
|
} | {
|
|
7
6
|
served: false;
|
|
8
7
|
why: string;
|
|
9
8
|
};
|
|
10
9
|
/**
|
|
11
|
-
* Decide the API for one deploy, writing nothing
|
|
12
|
-
*
|
|
13
|
-
* same setting.
|
|
10
|
+
* Decide the API for one deploy, writing nothing. Turned off, it is off on the deployment too, a key the target still
|
|
11
|
+
* holds included: the serve reads the same setting.
|
|
14
12
|
*/
|
|
15
13
|
export declare function deploymentApi(input: {
|
|
16
|
-
config:
|
|
14
|
+
config: EffectiveConfig;
|
|
17
15
|
/** The host serves the API at all: not AgentCore, reached only through `/invocations`. */
|
|
18
16
|
hostServes: boolean;
|
|
19
|
-
values: ReadonlyMap<string, string>;
|
|
20
17
|
}): DeploymentApi;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export declare function writeApiKey(agentDir: string): Promise<string | undefined>;
|
|
18
|
+
/** A key for the Agent API, made here: FastAgent mints it, so no person holds it before the deploy. */
|
|
19
|
+
export declare function mintApiKey(): {
|
|
20
|
+
name: string;
|
|
21
|
+
make(): string;
|
|
22
|
+
};
|
|
27
23
|
/**
|
|
28
24
|
* What a deploy says of the API, once the host has said where the deployment is reached (when it knows). The check
|
|
29
25
|
* names the agent's directory when the command runs elsewhere, since that is where `invoke --url` finds the key.
|
|
26
|
+
* `keyInFile`: this machine's production value file holds a key, which `invoke --url` then reads. `run`: the deploy
|
|
27
|
+
* has set the key (or found one) on the target; without it, nothing was set.
|
|
30
28
|
*/
|
|
31
29
|
export declare function apiReport(api: DeploymentApi, where: {
|
|
32
30
|
valueFile: string;
|
|
33
31
|
agentArg: string | undefined;
|
|
32
|
+
keyInFile: boolean;
|
|
33
|
+
run: boolean;
|
|
34
34
|
}, reachedAt: string | undefined): string[];
|
package/dist/deploy/api.js
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A deployment's Agent API (SPEC.md §9.5, §9.6): served under `/v1` on every host but AgentCore, unless the
|
|
3
|
-
* definition turns it off (`http.api: false`), behind a key
|
|
4
|
-
*
|
|
3
|
+
* definition turns it off (`http.api: false`), behind a key the deploy mints and sets on the target the first time
|
|
4
|
+
* neither the target nor the production value file has one. And what a deploy reports of it.
|
|
5
5
|
*/
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
import { API_PREFIX } from "../http/contract.js";
|
|
8
|
-
import { API_KEY_VARIABLE, API_TURNED_OFF,
|
|
9
|
-
import { writeValues } from "../value-file.js";
|
|
8
|
+
import { API_KEY_VARIABLE, API_TURNED_OFF, apiTurnedOff } from "../http/server.js";
|
|
10
9
|
/**
|
|
11
|
-
* Decide the API for one deploy, writing nothing
|
|
12
|
-
*
|
|
13
|
-
* same setting.
|
|
10
|
+
* Decide the API for one deploy, writing nothing. Turned off, it is off on the deployment too, a key the target still
|
|
11
|
+
* holds included: the serve reads the same setting.
|
|
14
12
|
*/
|
|
15
13
|
export function deploymentApi(input) {
|
|
16
|
-
const { config, hostServes
|
|
14
|
+
const { config, hostServes } = input;
|
|
17
15
|
if (!hostServes)
|
|
18
16
|
return { served: false, why: "this host reaches the agent only through its own invocations" };
|
|
19
17
|
if (apiTurnedOff(config))
|
|
20
18
|
return { served: false, why: API_TURNED_OFF };
|
|
21
|
-
return { served: true
|
|
19
|
+
return { served: true };
|
|
22
20
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* wrote into that git may track.
|
|
27
|
-
*/
|
|
28
|
-
export async function writeApiKey(agentDir) {
|
|
29
|
-
const key = randomBytes(32).toString("base64url");
|
|
30
|
-
// Named in `overwrite`: a line that holds no key (empty, or only commas) is replaced, not kept as set.
|
|
31
|
-
const { unprotectedSecretsDir } = await writeValues(agentDir, "Agent API", [{ name: API_KEY_VARIABLE, value: key }], [API_KEY_VARIABLE]);
|
|
32
|
-
return unprotectedSecretsDir;
|
|
21
|
+
/** A key for the Agent API, made here: FastAgent mints it, so no person holds it before the deploy. */
|
|
22
|
+
export function mintApiKey() {
|
|
23
|
+
return { name: API_KEY_VARIABLE, make: () => randomBytes(32).toString("base64url") };
|
|
33
24
|
}
|
|
34
25
|
/**
|
|
35
26
|
* What a deploy says of the API, once the host has said where the deployment is reached (when it knows). The check
|
|
36
27
|
* names the agent's directory when the command runs elsewhere, since that is where `invoke --url` finds the key.
|
|
28
|
+
* `keyInFile`: this machine's production value file holds a key, which `invoke --url` then reads. `run`: the deploy
|
|
29
|
+
* has set the key (or found one) on the target; without it, nothing was set.
|
|
37
30
|
*/
|
|
38
31
|
export function apiReport(api, where, reachedAt) {
|
|
39
32
|
if (!api.served)
|
|
40
33
|
return [`Agent API: not served — ${api.why}`];
|
|
41
|
-
const { valueFile, agentArg } = where;
|
|
34
|
+
const { valueFile, agentArg, keyInFile, run } = where;
|
|
42
35
|
const base = reachedAt ?? "<the deployment's URL>";
|
|
36
|
+
if (!keyInFile && !run) {
|
|
37
|
+
return [
|
|
38
|
+
`Agent API: ${base}${API_PREFIX}, behind ${API_KEY_VARIABLE}, which --run makes and sets on the target when ` +
|
|
39
|
+
`neither it nor ${valueFile} has one`,
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (!keyInFile) {
|
|
43
|
+
return [
|
|
44
|
+
`Agent API: ${base}${API_PREFIX}, behind ${API_KEY_VARIABLE} on the target; ${valueFile} holds none, so pass the key ` +
|
|
45
|
+
`to \`fastagent invoke --url\` with --key`,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
43
48
|
return [
|
|
44
|
-
|
|
45
|
-
? [`Agent API: generated ${API_KEY_VARIABLE} in ${valueFile}; it travels with the other values`]
|
|
46
|
-
: []),
|
|
47
|
-
`Agent API: ${base}${API_PREFIX}, behind ${API_KEY_VARIABLE} in ${valueFile}`,
|
|
49
|
+
`Agent API: ${base}${API_PREFIX}, behind ${API_KEY_VARIABLE} (in ${valueFile})`,
|
|
48
50
|
`check it: fastagent invoke --url ${base} "hello"${agentArg === undefined ? "" : ` ${agentArg}`}`,
|
|
49
51
|
];
|
|
50
52
|
}
|
|
@@ -21,9 +21,11 @@ export interface BoxShell {
|
|
|
21
21
|
wake?(): Promise<void>;
|
|
22
22
|
/**
|
|
23
23
|
* The box's storage root, for a shell that does not carry the server's environment (AgentCore's). Unset: the
|
|
24
|
-
* shell sees `
|
|
24
|
+
* shell sees `FASTAGENT_ROOT` as the server does, or the image's default.
|
|
25
25
|
*/
|
|
26
26
|
storage?: string;
|
|
27
|
+
/** Where the box's image has the agent. Unset: where a generated image has it. */
|
|
28
|
+
program?: string;
|
|
27
29
|
}
|
|
28
30
|
/** A host CLI that runs a command on the box itself: `bin ...args(command)`, run from `cwd`. */
|
|
29
31
|
export declare function processShell(bin: string, args: (command: string) => string[], cwd: string): BoxShell;
|
|
@@ -5,12 +5,12 @@ export interface BuildContextPaths {
|
|
|
5
5
|
machineryPaths: string[];
|
|
6
6
|
/** Credential files that exist right now — a kept ignore file that lets one through bakes it. */
|
|
7
7
|
leakCandidates: string[];
|
|
8
|
-
/**
|
|
8
|
+
/** A state root of the agent's own choosing inside the context, when it exists. */
|
|
9
9
|
stateShips?: string;
|
|
10
|
-
/**
|
|
10
|
+
/** `.fastagent/`, when it exists: the local instances' state and clones, and a build's staging. */
|
|
11
|
+
instancesShip?: string;
|
|
12
|
+
/** `content/`, when it exists: clones in the root, which a host refuses in a release. */
|
|
11
13
|
contentShips?: string;
|
|
12
|
-
/** `context.local.json`, when it exists: the directories of this machine the agent is bound to. */
|
|
13
|
-
bindingsShip?: string;
|
|
14
14
|
/** The agent's node_modules, when it exists. */
|
|
15
15
|
depDirs: string[];
|
|
16
16
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, relative, join, sep } from "node:path";
|
|
3
3
|
import ignore from "ignore";
|
|
4
|
-
import { AGENT_CONFIG_FILE, CONTENT_DIRNAME,
|
|
5
|
-
import { isGeneratedDockerignore } from "./container.js";
|
|
4
|
+
import { AGENT_CONFIG_FILE, CONTENT_DIRNAME, INSTANCES_DIRNAME, SECRETS_DIRNAME, exists, readTextIfExists, resolveSecretsDir, resolveStateRoot, } from "../paths.js";
|
|
5
|
+
import { DELIVERED_CONFIG_DIR, isGeneratedDockerignore } from "./container.js";
|
|
6
6
|
/**
|
|
7
7
|
* "Would docker's packer drop this path?" — built from a `.dockerignore`'s text via the `ignore` matcher (the same
|
|
8
8
|
* library the scaffolded ignore files use), so `!` negation and last-match-wins are the library's problem, not ours.
|
|
@@ -64,10 +64,11 @@ export async function buildContextPaths(agentDir, authPath) {
|
|
|
64
64
|
// State gets the same treatment (a custom in-tree FASTAGENT_STATE_DIR is invisible to the name-based `**/.state`),
|
|
65
65
|
// at warn level.
|
|
66
66
|
const stateRel = inContext(resolveStateRoot(agentDir));
|
|
67
|
+
const inInstances = stateRel !== undefined && (stateRel === INSTANCES_DIRNAME || stateRel.startsWith(`${INSTANCES_DIRNAME}/`));
|
|
67
68
|
// Existence gates the WARNING, never the generated exclude (same split as secretPaths vs leakCandidates).
|
|
68
|
-
const stateShips = stateRel !== undefined && (await exists(join(agentDir, stateRel))) ? stateRel : undefined;
|
|
69
|
+
const stateShips = stateRel !== undefined && !inInstances && (await exists(join(agentDir, stateRel))) ? stateRel : undefined;
|
|
70
|
+
const instancesShip = (await exists(join(agentDir, INSTANCES_DIRNAME))) ? INSTANCES_DIRNAME : undefined;
|
|
69
71
|
const contentShips = (await exists(join(agentDir, CONTENT_DIRNAME))) ? CONTENT_DIRNAME : undefined;
|
|
70
|
-
const bindingsShip = (await exists(join(agentDir, LOCAL_CONTEXT_FILE))) ? LOCAL_CONTEXT_FILE : undefined;
|
|
71
72
|
// The `.env` family at the level fastagent is RESPONSIBLE for: the agent dir's root.
|
|
72
73
|
const envFiles = (await entriesIfExists(agentDir))
|
|
73
74
|
.map((entry) => entry.name)
|
|
@@ -94,13 +95,13 @@ export async function buildContextPaths(agentDir, authPath) {
|
|
|
94
95
|
];
|
|
95
96
|
// Same existence rule: a node_modules that is not there cannot be uploaded.
|
|
96
97
|
const depDirs = await present(["node_modules"]);
|
|
97
|
-
const machineryPaths = [...secretPaths, ...(stateRel ? [stateRel] : [])];
|
|
98
|
-
return { machineryPaths, leakCandidates, stateShips,
|
|
98
|
+
const machineryPaths = [...secretPaths, ...(stateRel && !inInstances ? [stateRel] : [])];
|
|
99
|
+
return { machineryPaths, leakCandidates, stateShips, instancesShip, contentShips, depDirs };
|
|
99
100
|
}
|
|
100
101
|
/** Interrogate BOTH ignore files deploy emits, when the author kept them. */
|
|
101
102
|
export async function checkKeptIgnoreFiles(ctx, report) {
|
|
102
103
|
const { agentDir, force } = ctx;
|
|
103
|
-
const { leakCandidates, stateShips,
|
|
104
|
+
const { leakCandidates, stateShips, instancesShip, contentShips, depDirs } = ctx.paths;
|
|
104
105
|
for (const rel of [".dockerignore", "Dockerfile.dockerignore"]) {
|
|
105
106
|
const kept = await readTextIfExists(join(agentDir, rel));
|
|
106
107
|
if (kept === undefined)
|
|
@@ -130,14 +131,13 @@ export async function checkKeptIgnoreFiles(ctx, report) {
|
|
|
130
131
|
if (stateShips && !excluded(`${stateShips}/sessions`)) {
|
|
131
132
|
report.warn(`your ${rel} (kept) does not exclude \`${stateShips}\` — the build machine's sessions/channel state would ship in the image. ${remedy([`/${stateShips}`])}`);
|
|
132
133
|
}
|
|
134
|
+
if (instancesShip && !excluded(`${instancesShip}/dev/content/x`)) {
|
|
135
|
+
report.warn(`your ${rel} (kept) does not exclude \`${instancesShip}\` — the build machine's sessions, channel state and clones of the agent's content would ship in the image. ${remedy([`/${instancesShip}`, `!/${DELIVERED_CONFIG_DIR}`])}`);
|
|
136
|
+
}
|
|
133
137
|
// An issue, not a warning: a release holding content/ is refused on the host, which links its own there.
|
|
134
138
|
if (contentShips && !excluded(`${contentShips}/x`)) {
|
|
135
139
|
report.issue(`your ${rel} (kept) does not exclude \`${contentShips}\` — this machine's clones of the agent's content would ship in the image. ${remedy([`/${contentShips}`])}`);
|
|
136
140
|
}
|
|
137
|
-
// A warning: the host reads it, and the paths it names (this machine's, the user's name in them) are not there.
|
|
138
|
-
if (bindingsShip && !excluded(bindingsShip)) {
|
|
139
|
-
report.warn(`your ${rel} (kept) does not exclude \`${bindingsShip}\` — the directories of this machine the agent is bound to would ship in the image, and the host would look for them at every start. ${remedy([`/${bindingsShip}`])}`);
|
|
140
|
-
}
|
|
141
141
|
const unexcludedDeps = depDirs.filter((p) => !excluded(`${p}/.package-lock.json`));
|
|
142
142
|
if (unexcludedDeps.length > 0) {
|
|
143
143
|
report.warn(`your ${rel} does not exclude ${unexcludedDeps.map((p) => `\`${p}\``).join(" or ")} — the ` +
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** HOW A RUNNING CHANNEL IS REACHED — the URL-neutral half of "point this channel at the agent". */
|
|
2
2
|
import type { DeclaredChannel } from "../channels/discover.ts";
|
|
3
3
|
import type { RegistrationOutcome } from "../channels/registration.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { type WebhookKind, type WebhookRegistration } from "../channels/webhooks.ts";
|
|
5
5
|
export interface Registrars {
|
|
6
6
|
telegram: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
7
|
-
slack?: (baseUrl: string) => Promise<RegistrationOutcome>;
|
|
8
7
|
feishu?: (baseUrl: string, kind: "feishu" | "lark") => Promise<RegistrationOutcome>;
|
|
9
8
|
/**
|
|
10
9
|
* Where a channel's manual step goes beside the log, for a person who reads no log: a coding agent's user, through
|
|
@@ -12,11 +11,6 @@ export interface Registrars {
|
|
|
12
11
|
*/
|
|
13
12
|
leave?: (step: string) => void;
|
|
14
13
|
}
|
|
15
|
-
/**
|
|
16
|
-
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool knows how to
|
|
17
|
-
* instruct.
|
|
18
|
-
*/
|
|
19
|
-
export declare function webhookKinds(channels: readonly DeclaredChannel[]): ChannelKind[];
|
|
20
14
|
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
21
15
|
export declare function webhookPaths(channels: readonly DeclaredChannel[]): string[];
|
|
22
16
|
/**
|
|
@@ -31,7 +25,7 @@ export declare function pointChannelsAt(input: {
|
|
|
31
25
|
registrars: Registrars;
|
|
32
26
|
log: (msg: string) => void;
|
|
33
27
|
}): Promise<{
|
|
34
|
-
kind:
|
|
28
|
+
kind: WebhookKind;
|
|
35
29
|
outcome: RegistrationOutcome;
|
|
36
30
|
}[]>;
|
|
37
31
|
/** Poll a public `/health` until it answers 200; false on timeout. Injected by tests. */
|
|
@@ -70,7 +64,7 @@ export declare function loginGate(input: {
|
|
|
70
64
|
log: (msg: string) => void;
|
|
71
65
|
/** What THIS host does about its webhooks once the box is logged in — the only per-host words in the gate. */
|
|
72
66
|
afterLogin: string;
|
|
73
|
-
/** Where the channels point; unset when the host has
|
|
67
|
+
/** Where the channels point, for the lines that point them by hand; unset when the host has none to print. */
|
|
74
68
|
baseUrl?: string;
|
|
75
69
|
}): string;
|
|
76
70
|
/** {@link pointChannelsAt} plus the shared gate policy, for a command that EXITS. */
|
|
@@ -82,3 +76,35 @@ export declare function registerWebhooks(input: {
|
|
|
82
76
|
/** How THIS host retries — the only per-host words in the gate. */
|
|
83
77
|
retryHint: string;
|
|
84
78
|
}): Promise<string | undefined>;
|
|
79
|
+
/** A box's `/health` webhooks, or undefined while it cannot be read. Injected by tests. */
|
|
80
|
+
export type BoxWebhooksProbe = (healthUrl: string) => Promise<readonly {
|
|
81
|
+
channel: string;
|
|
82
|
+
outcome: WebhookRegistration["outcome"];
|
|
83
|
+
}[] | undefined>;
|
|
84
|
+
/**
|
|
85
|
+
* What a box that points its own channels made of them (channel-setup.md §5): read from its `/health` until no channel
|
|
86
|
+
* is `pending`, then gated as {@link registerWebhooks} gates. Slack's Request URL no credential on the box can set, so
|
|
87
|
+
* `registrars.slack` sets it from this machine, as before.
|
|
88
|
+
*/
|
|
89
|
+
export declare function confirmWebhooks(input: {
|
|
90
|
+
/** Where this machine reaches the box's `/health`: the host's own URL. */
|
|
91
|
+
baseUrl: string;
|
|
92
|
+
/** Where the platforms reach it, the delivered `http.publicUrl` (a custom domain, say): every step names this one. */
|
|
93
|
+
publicUrl: string;
|
|
94
|
+
channels: readonly DeclaredChannel[];
|
|
95
|
+
registrars: Pick<Registrars, "leave">;
|
|
96
|
+
log: (msg: string) => void;
|
|
97
|
+
retryHint: string;
|
|
98
|
+
probe?: BoxWebhooksProbe;
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
intervalMs?: number;
|
|
101
|
+
/** At a terminal with a `--config` file: record each URL a person set once the box confirms it. */
|
|
102
|
+
record?: UrlRecorder;
|
|
103
|
+
}): Promise<string | undefined>;
|
|
104
|
+
/** Where a deploy at a terminal records the URL a person set once the box confirms it (`channels.<name>.app.url`). */
|
|
105
|
+
export interface UrlRecorder {
|
|
106
|
+
/** How long to wait for the person to save the URL on the app's page. */
|
|
107
|
+
waitMs: number;
|
|
108
|
+
/** Write `url` into the binding of `kind` in the configuration `--config` names; resolves where it wrote. */
|
|
109
|
+
write(kind: WebhookKind, url: string): Promise<string>;
|
|
110
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { manualWebhookStep, webhookKinds, webhookPath, } from "../channels/webhooks.js";
|
|
1
2
|
import { registrationGate } from "./registration-gate.js";
|
|
2
3
|
import { waitForHealth } from "../channels/wait-health.js";
|
|
3
4
|
const feishuCloud = (kind, label) => ({
|
|
4
|
-
path: `/${kind}`,
|
|
5
5
|
// The subscription mode is the published version's: a switch from long connection waits for the publish.
|
|
6
6
|
delivers: "delivers to this deployment once its published version is in webhook mode (after a first deploy, publish the " +
|
|
7
7
|
"version it opened)",
|
|
8
8
|
register: (r, baseUrl) => r.feishu?.(baseUrl, kind),
|
|
9
|
-
manual: (baseUrl) => `${kind}: set the event Request URL in the developer console (Events & Callbacks) → ${baseUrl}/${kind} (the app must be running when you save)`,
|
|
10
9
|
runbook: (baseUrl) => [
|
|
11
10
|
`# Set the ${label} event Request URL (developer console → Events & Callbacks). Default route`,
|
|
12
11
|
`# POST /${kind}; the app must be RUNNING when you save (the console verifies with a challenge):`,
|
|
@@ -16,10 +15,8 @@ const feishuCloud = (kind, label) => ({
|
|
|
16
15
|
/** Declaration order — the order every runbook and every driver reports in. */
|
|
17
16
|
const INGRESS = {
|
|
18
17
|
telegram: {
|
|
19
|
-
path: "/telegram",
|
|
20
18
|
delivers: "now delivers to this deployment",
|
|
21
19
|
register: (r, baseUrl) => r.telegram(baseUrl),
|
|
22
|
-
manual: (baseUrl) => `telegram: set the webhook → ${baseUrl}/telegram (secret_token = TELEGRAM_SECRET_TOKEN)`,
|
|
23
20
|
runbook: (baseUrl) => [
|
|
24
21
|
`# Register the Telegram webhook. The path assumes the default route (POST /telegram); if you`,
|
|
25
22
|
`# remapped it in channels/telegram.ts, use your path. secret_token MUST equal TELEGRAM_SECRET_TOKEN:`,
|
|
@@ -28,30 +25,21 @@ const INGRESS = {
|
|
|
28
25
|
],
|
|
29
26
|
},
|
|
30
27
|
slack: {
|
|
31
|
-
path: "/slack",
|
|
32
28
|
delivers: "now delivers to this deployment",
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// The Request URL is the app's page, which a person sets (channel-setup.md §3): no credential here changes it.
|
|
30
|
+
register: () => undefined,
|
|
35
31
|
runbook: (baseUrl) => [
|
|
36
32
|
`# Set Slack Event Subscriptions → Request URL (default route POST /slack; the running service`,
|
|
37
|
-
`# answers Slack's challenge), and match scopes/subscriptions to the app \`
|
|
33
|
+
`# answers Slack's challenge), and match scopes/subscriptions to the app \`channels setup slack\` created:`,
|
|
38
34
|
`# Request URL = ${baseUrl}/slack`,
|
|
39
35
|
],
|
|
40
36
|
},
|
|
41
37
|
feishu: feishuCloud("feishu", "Feishu"),
|
|
42
38
|
lark: feishuCloud("lark", "Lark"),
|
|
43
39
|
};
|
|
44
|
-
/**
|
|
45
|
-
* The first-party channels this deployment must point at a URL: webhook ingress, and a kind this tool knows how to
|
|
46
|
-
* instruct.
|
|
47
|
-
*/
|
|
48
|
-
export function webhookKinds(channels) {
|
|
49
|
-
const declared = new Set(channels.filter((c) => c.ingress === "webhook").map((c) => c.name));
|
|
50
|
-
return Object.keys(INGRESS).filter((kind) => declared.has(kind));
|
|
51
|
-
}
|
|
52
40
|
/** Default route keys for {@link webhookKinds}, in the same order (the docker plan's ingress note). */
|
|
53
41
|
export function webhookPaths(channels) {
|
|
54
|
-
return webhookKinds(channels).map(
|
|
42
|
+
return webhookKinds(channels).map(webhookPath);
|
|
55
43
|
}
|
|
56
44
|
/**
|
|
57
45
|
* The runbook block for every channel that needs a URL set by hand, `baseUrl` spelled the host's way (a literal
|
|
@@ -68,7 +56,7 @@ export async function pointChannelsAt(input) {
|
|
|
68
56
|
// Calling IS the question: a channel whose registrar the caller did not wire returns undefined.
|
|
69
57
|
const running = ingress.register(input.registrars, input.baseUrl);
|
|
70
58
|
if (!running) {
|
|
71
|
-
input.log(
|
|
59
|
+
input.log(manualWebhookStep(kind, input.baseUrl));
|
|
72
60
|
outcomes.push({ kind, outcome: "manual" }); // a human's step — re-surfaced after registrar output
|
|
73
61
|
continue;
|
|
74
62
|
}
|
|
@@ -77,7 +65,7 @@ export async function pointChannelsAt(input) {
|
|
|
77
65
|
}
|
|
78
66
|
// What the registrar said is in the log; the person who reads none gets the one route to set.
|
|
79
67
|
for (const { kind } of outcomes.filter(({ outcome }) => outcome === "manual")) {
|
|
80
|
-
input.registrars.leave?.(
|
|
68
|
+
input.registrars.leave?.(manualWebhookStep(kind, input.baseUrl));
|
|
81
69
|
}
|
|
82
70
|
return outcomes;
|
|
83
71
|
}
|
|
@@ -134,9 +122,10 @@ export function loginGate(input) {
|
|
|
134
122
|
const byHand = input.baseUrl === undefined ? [] : webhookRunbook(input.baseUrl, input.channels);
|
|
135
123
|
for (const line of byHand)
|
|
136
124
|
input.log(line);
|
|
137
|
-
// "
|
|
138
|
-
return (`${input.notLoggedIn}. This deploy
|
|
139
|
-
`
|
|
125
|
+
// "Confirmed none" is about THIS run only: a channel the box or an earlier deploy pointed here still delivers.
|
|
126
|
+
return (`${input.notLoggedIn}. This deploy confirmed no webhook, and logging in confirms none; any already pointed here, ` +
|
|
127
|
+
`by the box itself or by an earlier deploy, still delivers, and every message it brings fails until the box is ` +
|
|
128
|
+
`logged in. Once it is, ` +
|
|
140
129
|
`${input.afterLogin}` +
|
|
141
130
|
(byHand.length > 0 ? `. The lines above point the channels by hand.` : ``));
|
|
142
131
|
}
|
|
@@ -168,3 +157,111 @@ export async function registerWebhooks(input) {
|
|
|
168
157
|
}
|
|
169
158
|
return reg.gate();
|
|
170
159
|
}
|
|
160
|
+
/** How long a deploy waits for a box to end its own registration: the box's registrars retry for about 190 s. */
|
|
161
|
+
const BOX_REGISTRATION_TIMEOUT_MS = 240_000;
|
|
162
|
+
const readBoxWebhooks = async (healthUrl) => {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
|
|
165
|
+
const body = (await res.json());
|
|
166
|
+
return Array.isArray(body.webhooks)
|
|
167
|
+
? body.webhooks
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined; // not routable, or not answering JSON yet: polled again until the deadline
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* What a box that points its own channels made of them (channel-setup.md §5): read from its `/health` until no channel
|
|
176
|
+
* is `pending`, then gated as {@link registerWebhooks} gates. Slack's Request URL no credential on the box can set, so
|
|
177
|
+
* `registrars.slack` sets it from this machine, as before.
|
|
178
|
+
*/
|
|
179
|
+
export async function confirmWebhooks(input) {
|
|
180
|
+
const kinds = webhookKinds(input.channels);
|
|
181
|
+
const reg = registrationGate(input.log, input.retryHint);
|
|
182
|
+
const outcomes = new Map();
|
|
183
|
+
if (kinds.includes("slack")) {
|
|
184
|
+
input.log(manualWebhookStep("slack", input.publicUrl));
|
|
185
|
+
outcomes.set("slack", "manual");
|
|
186
|
+
}
|
|
187
|
+
const own = kinds.filter((kind) => kind !== "slack");
|
|
188
|
+
if (own.length > 0) {
|
|
189
|
+
input.log(`waiting for the box to point ${own.join(", ")} at ${input.publicUrl}…`);
|
|
190
|
+
const probe = input.probe ?? readBoxWebhooks;
|
|
191
|
+
const deadline = Date.now() + (input.timeoutMs ?? BOX_REGISTRATION_TIMEOUT_MS);
|
|
192
|
+
for (;;) {
|
|
193
|
+
const read = await probe(`${input.baseUrl}/health`);
|
|
194
|
+
const ended = own.map((kind) => read?.find((entry) => entry.channel === kind));
|
|
195
|
+
if (ended.every((entry) => entry !== undefined && entry.outcome !== "pending")) {
|
|
196
|
+
for (const [i, kind] of own.entries())
|
|
197
|
+
outcomes.set(kind, ended[i]?.outcome);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
if (Date.now() >= deadline) {
|
|
201
|
+
for (const [i, kind] of own.entries()) {
|
|
202
|
+
if (ended[i] === undefined || ended[i]?.outcome === "pending") {
|
|
203
|
+
input.log(`${kind}: the box did not report its registration within the wait — see its log`);
|
|
204
|
+
outcomes.set(kind, "failed");
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
outcomes.set(kind, ended[i]?.outcome);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, input.intervalMs ?? 2_000));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const kind of kinds) {
|
|
216
|
+
if (outcomes.get(kind) === "registered" || kind === "slack")
|
|
217
|
+
continue;
|
|
218
|
+
// What the box said and why is in its log; the line a person acts on is said here.
|
|
219
|
+
input.log(manualWebhookStep(kind, input.publicUrl));
|
|
220
|
+
input.registrars.leave?.(manualWebhookStep(kind, input.publicUrl));
|
|
221
|
+
}
|
|
222
|
+
if (input.record !== undefined)
|
|
223
|
+
await recordConfirmed(input, kinds, outcomes, input.record);
|
|
224
|
+
for (const kind of kinds) {
|
|
225
|
+
const outcome = outcomes.get(kind);
|
|
226
|
+
reg.track(kind, outcome);
|
|
227
|
+
if (outcome === "registered")
|
|
228
|
+
input.log(`the selected production ${kind} app ${INGRESS[kind].delivers}`);
|
|
229
|
+
}
|
|
230
|
+
return reg.gate();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* For each channel whose URL only a person sets (Slack; Lark without its config API), wait for the box to report it
|
|
234
|
+
* reached at the delivered URL, then record that URL in the binding, so every later deploy, CI included, reports it
|
|
235
|
+
* `registered` from the start (channel-setup.md §5).
|
|
236
|
+
*/
|
|
237
|
+
async function recordConfirmed(input, kinds, outcomes, record) {
|
|
238
|
+
const probe = input.probe ?? readBoxWebhooks;
|
|
239
|
+
const waiting = new Set(kinds.filter((k) => (k === "slack" || k === "lark") && outcomes.get(k) !== "failed"));
|
|
240
|
+
if (waiting.size === 0)
|
|
241
|
+
return;
|
|
242
|
+
const later = (kind) => `\`fastagent channels setup ${kind} --config <file> --record-url\``;
|
|
243
|
+
const manual = [...waiting].filter((kind) => outcomes.get(kind) === "manual");
|
|
244
|
+
if (manual.length > 0) {
|
|
245
|
+
input.log(`waiting up to ${Math.round(record.waitMs / 60_000)} min for ` +
|
|
246
|
+
`${manual.map((kind) => `${input.publicUrl}${webhookPath(kind)}`).join(" and ")} to be saved on the app's ` +
|
|
247
|
+
`page (the platform verifies it with this box); stop it any time and record it later with ${later(manual[0])}`);
|
|
248
|
+
}
|
|
249
|
+
// One deadline for every channel still waiting: each is recorded as the box reports it reached.
|
|
250
|
+
const deadline = Date.now() + record.waitMs;
|
|
251
|
+
for (;;) {
|
|
252
|
+
const read = await probe(`${input.baseUrl}/health`);
|
|
253
|
+
for (const kind of [...waiting]) {
|
|
254
|
+
if (read?.find((entry) => entry.channel === kind)?.outcome !== "registered")
|
|
255
|
+
continue;
|
|
256
|
+
waiting.delete(kind);
|
|
257
|
+
outcomes.set(kind, "registered");
|
|
258
|
+
const url = `${input.publicUrl}${webhookPath(kind)}`;
|
|
259
|
+
// The deployment is done and the platform reaches it: a record not written is said, never the deploy's failure.
|
|
260
|
+
await record.write(kind, url).then((where) => input.log(`${kind}: the platform reached the box at ${url}; recorded it in ${where}`), (error) => input.log(`${kind}: the platform reached the box at ${url}, but it was not recorded (${error.message}); ` +
|
|
261
|
+
`record it with ${later(kind)}`));
|
|
262
|
+
}
|
|
263
|
+
if (waiting.size === 0 || Date.now() >= deadline)
|
|
264
|
+
return;
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, input.intervalMs ?? 2_000));
|
|
266
|
+
}
|
|
267
|
+
}
|