@fastagent-sh/fastagent 0.19.0 → 0.21.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 +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
package/dist/tunnel.js
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
* Process orchestration, not assembly — lives outside the engine, beside dev-supervisor.ts.
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { readdirSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
10
|
import { registerFeishuWebhook } from "./channels/feishu/register-webhook.js";
|
|
13
11
|
import { registerSlackWebhook } from "./channels/slack/register-webhook.js";
|
|
14
12
|
import { registerTelegramWebhook } from "./channels/telegram/register-webhook.js";
|
|
13
|
+
import { pointChannelsAt } from "./deploy/channel-ingress.js";
|
|
15
14
|
import { dotEnvPath, loadDotEnv } from "./env.js";
|
|
16
15
|
import { resolveStateRoot } from "./paths.js";
|
|
17
16
|
import { log } from "./log.js";
|
|
@@ -24,6 +23,28 @@ const TUNNEL_URL_RE = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/i;
|
|
|
24
23
|
export function parseTunnelUrl(chunk) {
|
|
25
24
|
return chunk.match(TUNNEL_URL_RE)?.[0];
|
|
26
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* cloudflared's line for an established edge connection. THIS, not the URL, is when the hostname
|
|
28
|
+
* begins to exist: `*.trycloudflare.com` is no wildcard, and a quick tunnel's record is published
|
|
29
|
+
* once the tunnel registers a connection — so the URL is printed while the name is still NXDOMAIN.
|
|
30
|
+
* A platform told about it inside that window answers "Failed to resolve host" and goes on answering
|
|
31
|
+
* it far longer than any registrar's retry budget (#435); the record going live seconds later does
|
|
32
|
+
* not undo the answer it already gave, which is why more retries were never the fix.
|
|
33
|
+
*
|
|
34
|
+
* Measured over 7 quick tunnels: the URL at +0s, this line at +1.1-1.9s, the record 0.5-3.9s later.
|
|
35
|
+
*/
|
|
36
|
+
const TUNNEL_CONNECTED_RE = /Registered tunnel connection/i;
|
|
37
|
+
/** Whether cloudflared's output so far reports an edge connection — see {@link TUNNEL_CONNECTED_RE}. */
|
|
38
|
+
export function hasTunnelConnection(output) {
|
|
39
|
+
return TUNNEL_CONNECTED_RE.test(output);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How long after that connection to hand the URL over. The one number here that is not observed: 5s
|
|
43
|
+
* covers the widest of the 7 measured gaps (3.9s), all taken over cloudflared's http2 transport
|
|
44
|
+
* because this network blocks its QUIC. Costs a beat of `dev --tunnel` start-up; buys the first
|
|
45
|
+
* registration attempt landing on a name that resolves.
|
|
46
|
+
*/
|
|
47
|
+
export const TUNNEL_DNS_LAG_MS = 5000;
|
|
27
48
|
/** Global timer (rather than timers/promises) so timeout/retry behavior is deterministic under fake timers. */
|
|
28
49
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
50
|
const TUNNEL_ATTEMPTS = 3;
|
|
@@ -32,12 +53,13 @@ const TUNNEL_RETRY_MS = 2000;
|
|
|
32
53
|
const TUNNEL_START_TIMEOUT_MS = 30_000;
|
|
33
54
|
const spawnCloudflared = (port) => spawn("cloudflared", ["tunnel", "--url", `http://localhost:${port}`], { stdio: ["ignore", "pipe", "pipe"] });
|
|
34
55
|
/**
|
|
35
|
-
* Start a Cloudflare quick tunnel to localhost:`port`, resolving once its public URL
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
56
|
+
* Start a Cloudflare quick tunnel to localhost:`port`, resolving once its public URL is assigned AND
|
|
57
|
+
* the tunnel has an edge connection — the URL is printed first and is not usable yet
|
|
58
|
+
* ({@link hasTunnelConnection}). cloudflared sometimes exits before printing a URL (a transient
|
|
59
|
+
* trycloudflare API error), so retry a few times. ALWAYS resolves to undefined WITH an operator log
|
|
60
|
+
* saying why — missing binary, the exit reason, or "gave up after retries" — never silently; serving
|
|
61
|
+
* continues without a tunnel either way. What remains after this is the PLATFORM's own warm-up, which
|
|
62
|
+
* each registrar absorbs by retrying while the platform reports it cannot yet verify the URL.
|
|
41
63
|
*/
|
|
42
64
|
export async function startCloudflareTunnel(port, spawnFn = spawnCloudflared, attemptTimeoutMs = TUNNEL_START_TIMEOUT_MS) {
|
|
43
65
|
for (let attempt = 1; attempt <= TUNNEL_ATTEMPTS; attempt++) {
|
|
@@ -56,26 +78,35 @@ export async function startCloudflareTunnel(port, spawnFn = spawnCloudflared, at
|
|
|
56
78
|
}
|
|
57
79
|
return undefined;
|
|
58
80
|
}
|
|
59
|
-
/** One cloudflared launch: a Tunnel
|
|
81
|
+
/** One cloudflared launch: a Tunnel once its URL is assigned AND the edge connection is up, or a
|
|
82
|
+
* failure (missing binary / exit before a URL). */
|
|
60
83
|
function spawnTunnelOnce(port, spawnFn, timeoutMs) {
|
|
61
84
|
return new Promise((resolve) => {
|
|
62
85
|
const child = spawnFn(port);
|
|
63
86
|
let settled = false;
|
|
64
87
|
let timer;
|
|
88
|
+
let handOver;
|
|
65
89
|
let tail = ""; // recent output, surfaced as the failure reason
|
|
90
|
+
let assigned; // printed well before the tunnel can carry anything
|
|
66
91
|
const finish = (result) => {
|
|
67
92
|
if (settled)
|
|
68
93
|
return;
|
|
69
94
|
settled = true;
|
|
70
95
|
if (timer)
|
|
71
96
|
clearTimeout(timer);
|
|
97
|
+
if (handOver)
|
|
98
|
+
clearTimeout(handOver);
|
|
72
99
|
resolve(result);
|
|
73
100
|
};
|
|
101
|
+
const handOverNow = (url) => finish({ tunnel: { url, close: () => child.kill("SIGTERM") } });
|
|
74
102
|
const onChunk = (buf) => {
|
|
75
103
|
tail = (tail + String(buf)).slice(-600);
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
|
|
104
|
+
assigned ??= parseTunnelUrl(String(buf));
|
|
105
|
+
if (!assigned || handOver || !hasTunnelConnection(tail))
|
|
106
|
+
return;
|
|
107
|
+
const url = assigned;
|
|
108
|
+
handOver = setTimeout(() => handOverNow(url), TUNNEL_DNS_LAG_MS);
|
|
109
|
+
handOver.unref();
|
|
79
110
|
};
|
|
80
111
|
child.stdout?.on("data", onChunk);
|
|
81
112
|
child.stderr?.on("data", onChunk); // cloudflared prints the URL (and its errors) on stderr
|
|
@@ -92,6 +123,15 @@ function spawnTunnelOnce(port, spawnFn, timeoutMs) {
|
|
|
92
123
|
});
|
|
93
124
|
child.on("exit", () => finish({ fatal: false, detail: lastErrorLine(tail) }));
|
|
94
125
|
timer = setTimeout(() => {
|
|
126
|
+
// A URL whose tunnel never connected is not worth a fresh one — the next attempt meets the same
|
|
127
|
+
// network. Serve it and name what is wrong, rather than retrying into the same wall.
|
|
128
|
+
if (assigned) {
|
|
129
|
+
log.warn(`[fastagent] --tunnel: cloudflared never reported an edge connection for ${assigned} within ` +
|
|
130
|
+
`${Math.round(timeoutMs / 1000)}s — serving it anyway. Nothing reaches a tunnel that has not ` +
|
|
131
|
+
"connected, so a webhook registration that cannot resolve the host is this, not the platform.");
|
|
132
|
+
handOverNow(assigned);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
95
135
|
child.kill("SIGTERM");
|
|
96
136
|
finish({ fatal: false, detail: `timed out after ${Math.round(timeoutMs / 1000)}s waiting for a public URL` });
|
|
97
137
|
}, timeoutMs);
|
|
@@ -106,23 +146,23 @@ function lastErrorLine(tail) {
|
|
|
106
146
|
.filter(Boolean);
|
|
107
147
|
return ([...lines].reverse().find((l) => /err|error|failed/i.test(l)) ?? lines.at(-1) ?? "").slice(0, 200);
|
|
108
148
|
}
|
|
109
|
-
/** Channel basenames present in `<dir>/channels/`. */
|
|
110
|
-
function channelBasenames(dir) {
|
|
111
|
-
try {
|
|
112
|
-
return readdirSync(join(dir, "channels"))
|
|
113
|
-
.filter((n) => /\.(ts|js|mjs)$/.test(n) && !n.endsWith(".d.ts"))
|
|
114
|
-
.map((n) => n.replace(/\.(ts|js|mjs)$/, ""));
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return [];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
149
|
/**
|
|
121
150
|
* Print the public URL and wire up first-party webhook channels found under `dir` (the agent
|
|
122
151
|
* ROOT): Telegram and Feishu/Lark use runtime credentials; onboarded Slack uses its owner-local config
|
|
123
152
|
* token; GitHub and a manually scaffolded Slack app receive explicit console URLs.
|
|
153
|
+
*
|
|
154
|
+
* Returns what each registrar ANSWERED, because two kinds of caller need different things from a
|
|
155
|
+
* failure. `dev`/`start` are long-running: a webhook that did not register is a logged problem, not
|
|
156
|
+
* a reason to stop serving, and they void this. `deploy … --run` is a command that exits, and an
|
|
157
|
+
* exit 0 there tells its caller the deployment is reachable — so it feeds these through
|
|
158
|
+
* `registrationGate`, exactly as the fly/railway/agentcore runners feed their own registrar calls
|
|
159
|
+
* (docker used to be the one host that could not, because this returned nothing).
|
|
124
160
|
*/
|
|
125
|
-
export async function announceWebhooks(dir, baseUrl,
|
|
161
|
+
export async function announceWebhooks(dir, baseUrl,
|
|
162
|
+
/** Every declared channel with its ingress. Not a pre-filtered list: this used to accept "the route
|
|
163
|
+
* channels" and default to every basename in `channels/`, which pointed a webhook at a
|
|
164
|
+
* long-connection channel whenever a caller forgot to filter. {@link pointChannelsAt} filters. */
|
|
165
|
+
channels, opts = {}) {
|
|
126
166
|
log.info(`[fastagent] public URL: ${baseUrl}`);
|
|
127
167
|
try {
|
|
128
168
|
loadDotEnv(dir); // webhook registrars read channel credentials from .env
|
|
@@ -135,33 +175,26 @@ export async function announceWebhooks(dir, baseUrl, opts = {}) {
|
|
|
135
175
|
// missing-credential guidance. loadDotEnv keeps throwing for the synchronous command callers.
|
|
136
176
|
log.warn(`[fastagent] could not read ${dotEnvPath(dir)}: ${error.message} — continuing without it`);
|
|
137
177
|
}
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
// its origin connects, and the automatic registrars poll /health before configuring the platform (the
|
|
145
|
-
// same wait the deploy runners rely on). GitHub needs no wait — the operator adds that webhook by hand.
|
|
146
|
-
if (routeChannels.includes("telegram"))
|
|
147
|
-
await registerTelegramWebhook(baseUrl);
|
|
148
|
-
if (routeChannels.includes("github")) {
|
|
149
|
-
log.info(`[fastagent] github: add a webhook in your repo (Settings → Webhooks): Payload URL = ${baseUrl}/webhook, content type application/json, secret = GITHUB_WEBHOOK_SECRET`);
|
|
150
|
-
}
|
|
151
|
-
if (routeChannels.includes("slack")) {
|
|
152
|
-
await registerSlackWebhook(baseUrl, {
|
|
153
|
-
stateRoot: opts.stateRoot ?? resolveStateRoot(dir),
|
|
154
|
-
log: (message) => log.info(message),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
// feishu/lark register programmatically too (application-v7 config PATCH — telegram-setWebhook
|
|
158
|
-
// parity), once per mounted kind (each kind is its own app with its own credentials); the registrar
|
|
159
|
-
// owns its own health wait and degrades to the manual console instruction.
|
|
178
|
+
// Readiness is the registrar's job: a fresh quick tunnel returns Cloudflare 530 for ~20-30s before its
|
|
179
|
+
// origin connects, and each registrar absorbs that by retrying the platform call whose own URL
|
|
180
|
+
// verification reports it. GitHub needs no wait — the operator adds that webhook by hand.
|
|
181
|
+
//
|
|
182
|
+
// The registrars differ from the deploy path's in what they carry, not in which channels they answer
|
|
183
|
+
// for: this one narrates to the log and opens the console for a manual Feishu step.
|
|
160
184
|
const feishuOptions = {
|
|
161
185
|
onManualRegistration: ({ consoleUrl }) => opts.openUrl?.(consoleUrl),
|
|
162
186
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
return pointChannelsAt({
|
|
188
|
+
baseUrl,
|
|
189
|
+
channels,
|
|
190
|
+
log: (message) => log.info(`[fastagent] ${message}`),
|
|
191
|
+
registrars: {
|
|
192
|
+
telegram: (url) => registerTelegramWebhook(url),
|
|
193
|
+
slack: (url) => registerSlackWebhook(url, {
|
|
194
|
+
stateRoot: opts.stateRoot ?? resolveStateRoot(dir),
|
|
195
|
+
log: (message) => log.info(message),
|
|
196
|
+
}),
|
|
197
|
+
feishu: (url, kind) => registerFeishuWebhook(url, kind, feishuOptions),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
167
200
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/fastagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on GitHub, Telegram, Slack, or behind any channel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"build": "tsc -p tsconfig.build.json",
|
|
95
95
|
"prepack": "npm run build",
|
|
96
96
|
"test": "vitest --run",
|
|
97
|
+
"test:live": "vitest --run --config vitest.live.config.ts",
|
|
97
98
|
"typecheck": "tsc --noEmit",
|
|
98
99
|
"lint": "biome check src test && node scripts/check-doc-links.mjs && knip",
|
|
99
100
|
"format": "biome check --write src test",
|
|
@@ -105,9 +106,9 @@
|
|
|
105
106
|
},
|
|
106
107
|
"dependencies": {
|
|
107
108
|
"@clack/prompts": "^1.6.0",
|
|
108
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
109
|
-
"@earendil-works/pi-ai": "^0.
|
|
110
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
109
|
+
"@earendil-works/pi-agent-core": "^0.85.1",
|
|
110
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
111
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
111
112
|
"@hono/node-server": "^2.1.1",
|
|
112
113
|
"@larksuiteoapi/node-sdk": "^1.71.1",
|
|
113
114
|
"@octokit/webhooks-methods": "^6.0.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface SlackBotTokenProviderOptions {
|
|
2
|
-
statePath: string;
|
|
3
|
-
botToken: string;
|
|
4
|
-
botRefreshToken?: string;
|
|
5
|
-
clientId?: string;
|
|
6
|
-
clientSecret?: string;
|
|
7
|
-
botTokenExpiresAt?: number;
|
|
8
|
-
apiBaseUrl?: string;
|
|
9
|
-
fetch?: typeof fetch;
|
|
10
|
-
}
|
|
11
|
-
/** Latest local rotating credentials for `deploy --run`. The remote runtime still prefers its own
|
|
12
|
-
* durable volume; this overlay prevents a locally consumed refresh token from being redeployed. */
|
|
13
|
-
export declare function readSlackBotAuthEnv(statePath: string): Record<string, string>;
|
|
14
|
-
/** Resolve the current bot token, refreshing once per process when it approaches expiry. */
|
|
15
|
-
export declare function createSlackBotTokenProvider(options: SlackBotTokenProviderOptions): () => Promise<string>;
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/** Rotating Slack bot-token provider backed by owner-only channel state. */
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { writeFileAtomic } from "../../atomic-write.js";
|
|
4
|
-
const REFRESH_EARLY_MS = 5 * 60_000;
|
|
5
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
6
|
-
function isState(value) {
|
|
7
|
-
const state = value;
|
|
8
|
-
return (typeof state === "object" &&
|
|
9
|
-
state !== null &&
|
|
10
|
-
state.version === 1 &&
|
|
11
|
-
typeof state.accessToken === "string" &&
|
|
12
|
-
typeof state.refreshToken === "string" &&
|
|
13
|
-
typeof state.expiresAt === "number");
|
|
14
|
-
}
|
|
15
|
-
function load(path) {
|
|
16
|
-
let raw;
|
|
17
|
-
try {
|
|
18
|
-
raw = readFileSync(path, "utf8");
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
if (error.code === "ENOENT")
|
|
22
|
-
return undefined;
|
|
23
|
-
throw new Error(`Slack bot auth state ${path} is unreadable: ${String(error)}`, { cause: error });
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const value = JSON.parse(raw);
|
|
27
|
-
if (!isState(value))
|
|
28
|
-
throw new Error("unexpected shape/version");
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
// A stale env refresh token may already have been consumed. Never hide corrupt rotating state by
|
|
33
|
-
// falling back to it: recovery requires restoring the durable file or reinstalling the app.
|
|
34
|
-
throw new Error(`Slack bot auth state ${path} is invalid: ${String(error)}`, { cause: error });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function save(path, state) {
|
|
38
|
-
writeFileAtomic(path, JSON.stringify(state), 0o600);
|
|
39
|
-
}
|
|
40
|
-
async function refreshSlackBotToken(input) {
|
|
41
|
-
const authorization = Buffer.from(`${input.clientId}:${input.clientSecret}`, "utf8").toString("base64");
|
|
42
|
-
let lastError;
|
|
43
|
-
// Slack keeps a short grace window for a just-consumed refresh token. One retry recovers the
|
|
44
|
-
// committed-but-response-lost boundary without creating an unbounded refresh loop.
|
|
45
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
46
|
-
try {
|
|
47
|
-
const response = await input.fetch(`${input.apiBaseUrl}/oauth.v2.access`, {
|
|
48
|
-
method: "POST",
|
|
49
|
-
headers: {
|
|
50
|
-
authorization: `Basic ${authorization}`,
|
|
51
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
52
|
-
},
|
|
53
|
-
body: new URLSearchParams({
|
|
54
|
-
grant_type: "refresh_token",
|
|
55
|
-
refresh_token: input.refreshToken,
|
|
56
|
-
}).toString(),
|
|
57
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
58
|
-
});
|
|
59
|
-
const data = (await response.json().catch(() => ({})));
|
|
60
|
-
if (!response.ok ||
|
|
61
|
-
data.ok !== true ||
|
|
62
|
-
!data.access_token ||
|
|
63
|
-
!data.refresh_token ||
|
|
64
|
-
typeof data.expires_in !== "number") {
|
|
65
|
-
throw new Error(`Slack rejected bot-token refresh (HTTP ${response.status})`);
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
version: 1,
|
|
69
|
-
accessToken: data.access_token,
|
|
70
|
-
refreshToken: data.refresh_token,
|
|
71
|
-
expiresAt: Date.now() + data.expires_in * 1_000,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
lastError = error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
throw new Error("Slack bot-token rotation failed; restore credentials or reinstall the app", { cause: lastError });
|
|
79
|
-
}
|
|
80
|
-
/** Latest local rotating credentials for `deploy --run`. The remote runtime still prefers its own
|
|
81
|
-
* durable volume; this overlay prevents a locally consumed refresh token from being redeployed. */
|
|
82
|
-
export function readSlackBotAuthEnv(statePath) {
|
|
83
|
-
const state = load(statePath);
|
|
84
|
-
return state
|
|
85
|
-
? {
|
|
86
|
-
SLACK_BOT_TOKEN: state.accessToken,
|
|
87
|
-
SLACK_BOT_REFRESH_TOKEN: state.refreshToken,
|
|
88
|
-
SLACK_BOT_TOKEN_EXPIRES_AT: String(state.expiresAt),
|
|
89
|
-
}
|
|
90
|
-
: {};
|
|
91
|
-
}
|
|
92
|
-
/** Resolve the current bot token, refreshing once per process when it approaches expiry. */
|
|
93
|
-
export function createSlackBotTokenProvider(options) {
|
|
94
|
-
const rotatingFields = [options.botRefreshToken, options.clientId, options.clientSecret, options.botTokenExpiresAt];
|
|
95
|
-
const rotating = rotatingFields.some((value) => value !== undefined);
|
|
96
|
-
if (rotating && rotatingFields.some((value) => value === undefined || value === "")) {
|
|
97
|
-
throw new Error("Slack token rotation requires botRefreshToken, clientId, clientSecret, and botTokenExpiresAt together");
|
|
98
|
-
}
|
|
99
|
-
if (rotating && (!Number.isFinite(options.botTokenExpiresAt) || options.botTokenExpiresAt <= 0)) {
|
|
100
|
-
throw new Error("Slack botTokenExpiresAt must be a positive epoch-millisecond value");
|
|
101
|
-
}
|
|
102
|
-
if (!rotating)
|
|
103
|
-
return async () => options.botToken;
|
|
104
|
-
const configured = {
|
|
105
|
-
version: 1,
|
|
106
|
-
accessToken: options.botToken,
|
|
107
|
-
refreshToken: options.botRefreshToken,
|
|
108
|
-
expiresAt: options.botTokenExpiresAt,
|
|
109
|
-
};
|
|
110
|
-
const persisted = load(options.statePath);
|
|
111
|
-
// Deploy may carry a newer pair from the owner machine onto an existing remote volume; ordinary
|
|
112
|
-
// restarts carry the original stale env and therefore keep the newer persisted pair.
|
|
113
|
-
let state = persisted && persisted.expiresAt >= configured.expiresAt ? persisted : configured;
|
|
114
|
-
let refreshing;
|
|
115
|
-
return async () => {
|
|
116
|
-
if (state.expiresAt > Date.now() + REFRESH_EARLY_MS)
|
|
117
|
-
return state.accessToken;
|
|
118
|
-
refreshing ??= refreshSlackBotToken({
|
|
119
|
-
refreshToken: state.refreshToken,
|
|
120
|
-
clientId: options.clientId,
|
|
121
|
-
clientSecret: options.clientSecret,
|
|
122
|
-
apiBaseUrl: (options.apiBaseUrl ?? "https://slack.com/api").replace(/\/$/, ""),
|
|
123
|
-
fetch: options.fetch ?? globalThis.fetch,
|
|
124
|
-
})
|
|
125
|
-
.then((next) => {
|
|
126
|
-
save(options.statePath, next);
|
|
127
|
-
state = next;
|
|
128
|
-
})
|
|
129
|
-
.finally(() => {
|
|
130
|
-
refreshing = undefined;
|
|
131
|
-
});
|
|
132
|
-
await refreshing;
|
|
133
|
-
return state.accessToken;
|
|
134
|
-
};
|
|
135
|
-
}
|