@fastagent-sh/fastagent 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +0 -4
- package/dist/channels/agentcore-service.js +1 -6
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +1 -67
- package/dist/channels/agentcore.js +90 -89
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +13 -62
- package/dist/channels/discover.d.ts +0 -1
- package/dist/channels/discover.js +1 -3
- 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 +2 -4
- package/dist/channels/feishu/feishu.js +56 -115
- 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/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- 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/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +41 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +34 -14
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -126
- 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 +81 -22
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +60 -99
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/telegram.js +47 -142
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +7 -20
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +25 -37
- 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 +17 -49
- package/dist/cli/commands/deploy.js +39 -618
- package/dist/cli/commands/dev.js +17 -46
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +17 -46
- 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 +36 -19
- package/dist/cli/serve.js +91 -7
- package/dist/cli/shared.d.ts +13 -18
- package/dist/cli/shared.js +20 -4
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -2
- package/dist/deploy/agentcore/plan.js +29 -244
- package/dist/deploy/agentcore/run.d.ts +4 -20
- package/dist/deploy/agentcore/run.js +22 -7
- package/dist/deploy/channel-ingress.js +1 -1
- package/dist/deploy/docker/run.d.ts +17 -3
- package/dist/deploy/docker/run.js +30 -8
- package/dist/deploy/fly/plan.js +7 -0
- package/dist/deploy/fly/run.d.ts +27 -0
- package/dist/deploy/fly/run.js +102 -20
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.js +3 -3
- package/dist/deploy/railway/plan.d.ts +5 -0
- package/dist/deploy/railway/plan.js +7 -0
- package/dist/deploy/railway/run.d.ts +2 -1
- package/dist/deploy/railway/run.js +5 -4
- package/dist/deploy/secrets.js +0 -17
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +109 -85
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +22 -39
- package/dist/engines/pi/create.js +58 -73
- package/dist/engines/pi/definition.js +8 -8
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +31 -41
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/session-builder.js +26 -113
- package/dist/engines/pi/session-control.d.ts +10 -18
- package/dist/engines/pi/session-control.js +47 -81
- package/dist/engines/pi/session-settings.d.ts +1 -1
- package/dist/engines/pi/session-settings.js +1 -1
- package/dist/engines/pi/session-store.d.ts +12 -17
- package/dist/engines/pi/session-store.js +18 -40
- package/dist/engines/pi/tool-context.d.ts +3 -3
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +1 -0
- 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 +2 -0
- package/dist/loader.js +5 -0
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +7 -3
- package/dist/paths.js +17 -4
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +48 -65
- package/dist/schedule/wake-alarm.d.ts +1 -12
- package/dist/schedule/wake-alarm.js +2 -3
- package/dist/service.d.ts +23 -22
- package/dist/service.js +7 -73
- package/dist/session-remote.d.ts +5 -5
- package/dist/session-remote.js +25 -28
- package/dist/session.d.ts +1 -1
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/tunnel.d.ts +16 -6
- package/dist/tunnel.js +53 -12
- 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/cli/commands/dev.js
CHANGED
|
@@ -3,34 +3,23 @@
|
|
|
3
3
|
* assemble + serve, restarting it on agent edits. A fresh process per reload means what is served
|
|
4
4
|
* is always the latest code, including modules a tool/config imports.
|
|
5
5
|
*/
|
|
6
|
-
import { resolve } from "node:path";
|
|
7
6
|
import { runDevSupervisor } from "../../dev-supervisor.js";
|
|
8
|
-
import { loadDotEnv } from "../../env.js";
|
|
9
7
|
import { setLogLevel } from "../../log.js";
|
|
10
8
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
11
9
|
import { mountAgentService } from "../../service.js";
|
|
12
10
|
import { logAgentLoop } from "../../observe.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { SHUTDOWN_GRACE_MS, assertTunnelBindable, maybeTunnel, reportServing, serve } from "../serve.js";
|
|
17
|
-
import { parseBind, parsePort, reportAssembly, resolveFirstRunModel } from "../shared.js";
|
|
11
|
+
import { failStartup } from "../fail.js";
|
|
12
|
+
import { assertTunnelBindable, cliMountOptions, resolveBindHost, serveService } from "../serve.js";
|
|
13
|
+
import { enterAgentCommand, parseBind, parsePort, reportAssembly } from "../shared.js";
|
|
18
14
|
export async function runDev(dirArg, opts) {
|
|
19
|
-
const dir = resolve(dirArg);
|
|
20
|
-
const placement = placementOrExit(dir);
|
|
21
15
|
setLogLevel("debug"); // dev posture: verbose, includes the debug turn trace (content) — supervisor and worker both
|
|
22
16
|
const isWorker = process.env.FASTAGENT_DEV_WORKER === "1";
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
if (!isWorker) {
|
|
28
|
-
loadDotEnv(placement.agentDir);
|
|
29
|
-
installProxyFetch();
|
|
30
|
-
await resolveFirstRunModel(placement.agentDir, opts);
|
|
31
|
-
}
|
|
17
|
+
// The model is picked ONCE, in the parent process (a TTY; watch and --no-watch both); a spawned
|
|
18
|
+
// worker inherits the choice through FASTAGENT_MODEL and must not prompt even when the pick was
|
|
19
|
+
// cancelled.
|
|
20
|
+
const placement = await enterAgentCommand(dirArg, { ...opts, input: isWorker ? false : opts.input });
|
|
32
21
|
if (isWorker || opts.watch === false) {
|
|
33
|
-
await serveOnce(
|
|
22
|
+
await serveOnce(placement, opts);
|
|
34
23
|
return;
|
|
35
24
|
}
|
|
36
25
|
parsePort(opts.port, "--port", "flag"); // flag-shape checks before spawning
|
|
@@ -40,41 +29,23 @@ export async function runDev(dirArg, opts) {
|
|
|
40
29
|
await runDevSupervisor(placement, { tunnel: opts.tunnel ?? false });
|
|
41
30
|
}
|
|
42
31
|
/** Assemble the agent and serve it once (the dev worker; also the --no-watch path). */
|
|
43
|
-
async function serveOnce(
|
|
32
|
+
async function serveOnce(placement, opts) {
|
|
44
33
|
const portFlag = parsePort(opts.port, "--port", "flag");
|
|
45
34
|
const bindFlag = parseBind(opts.bind);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const a = await createPiAgentFromDir(dir, {
|
|
35
|
+
const tunnel = opts.tunnel ?? false;
|
|
36
|
+
const a = await createPiAgentFromDir(placement.workspace, {
|
|
49
37
|
model: opts.model,
|
|
50
38
|
authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
|
|
51
39
|
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
52
40
|
}).catch(failStartup);
|
|
53
41
|
// The same report `start` prints; `config:` is dev's own extra (see reportAssembly on the asymmetry).
|
|
54
42
|
await reportAssembly(a, { beforeModel: [["config", a.configPath ?? "(none)"]] });
|
|
55
|
-
|
|
56
|
-
// configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
|
|
57
|
-
const configured = a.config.http?.host;
|
|
58
|
-
const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
|
|
59
|
-
assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
|
|
43
|
+
const host = resolveBindHost(bindFlag, a.config.http?.host, tunnel);
|
|
60
44
|
// The SAME assembly an embedder gets from `createAgentService` — channels, control plane,
|
|
61
45
|
// schedules, long connections. `dev` opens the directory itself only because its startup report
|
|
62
|
-
// prints the opened values before anything mounts.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
closeTimeoutMs: SHUTDOWN_GRACE_MS,
|
|
68
|
-
control: { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) },
|
|
69
|
-
onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
|
|
70
|
-
}).catch(failStartup);
|
|
71
|
-
serve(service.handler, { port: portFlag ?? a.config.http?.port ?? 8787, host }, {
|
|
72
|
-
ready: service.ready,
|
|
73
|
-
onListening: (p) => {
|
|
74
|
-
reportServing(service, host, p);
|
|
75
|
-
service.announce(p);
|
|
76
|
-
maybeTunnel(a.agentDir, service.channels.routes, p, opts.tunnel ?? false, a.stateRoot);
|
|
77
|
-
},
|
|
78
|
-
onShutdown: () => service.close(),
|
|
79
|
-
});
|
|
46
|
+
// prints the opened values before anything mounts. The turn trace (tool calls + reply) logs at
|
|
47
|
+
// debug level: shown here, gated out in start (level info), keeping end-user content out of
|
|
48
|
+
// production logs.
|
|
49
|
+
const service = await mountAgentService(a, cliMountOptions(logAgentLoop)).catch(failStartup);
|
|
50
|
+
serveService(service, { port: portFlag ?? a.config.http?.port ?? 8787, host }, { tunnel, agentDir: a.agentDir, stateRoot: a.stateRoot });
|
|
80
51
|
}
|
|
@@ -4,23 +4,17 @@
|
|
|
4
4
|
* the schedule's stable session (faithful to the served behavior). Does NOT advance the schedule's fire
|
|
5
5
|
* state — a test run must never make the scheduler skip the real next run.
|
|
6
6
|
*/
|
|
7
|
-
import { join
|
|
8
|
-
import { loadDotEnv } from "../../env.js";
|
|
7
|
+
import { join } from "node:path";
|
|
9
8
|
import { displayPath } from "../../paths.js";
|
|
10
|
-
import { reportModuleLoadFailures } from "../../
|
|
9
|
+
import { reportModuleLoadFailures } from "../../loader.js";
|
|
11
10
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
12
11
|
import { runInvokeStream } from "../invoke-stream.js";
|
|
13
|
-
import { installProxyFetch } from "../../proxy.js";
|
|
14
12
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
15
13
|
import { scheduleSession } from "../../schedule/scheduler.js";
|
|
16
|
-
import { failStartup
|
|
17
|
-
import {
|
|
14
|
+
import { failStartup } from "../fail.js";
|
|
15
|
+
import { enterAgentCommand, reportAuth } from "../shared.js";
|
|
18
16
|
export async function runFire(name, dirArg, opts) {
|
|
19
|
-
const
|
|
20
|
-
const placement = placementOrExit(fireDir);
|
|
21
|
-
loadDotEnv(placement.agentDir);
|
|
22
|
-
installProxyFetch();
|
|
23
|
-
await resolveFirstRunModel(placement.agentDir, opts);
|
|
17
|
+
const placement = await enterAgentCommand(dirArg, opts);
|
|
24
18
|
// Schedules are agent surface — discover them where dev/start/`schedule list` do (the agent
|
|
25
19
|
// dir), so `fire` sees the same set the scheduler serves.
|
|
26
20
|
const { schedules, failures } = await loadSchedules(placement.agentDir).catch(failStartup);
|
|
@@ -32,7 +26,7 @@ export async function runFire(name, dirArg, opts) {
|
|
|
32
26
|
failStartup(new Error(`unknown schedule "${name}" (looked in ${displayPath(process.cwd(), join(placement.agentDir, "schedules")) ?? "schedules"}). ` +
|
|
33
27
|
`available: ${schedules.map((s) => s.name).join(", ") || "(none)"}`));
|
|
34
28
|
}
|
|
35
|
-
const { agent, modelSpec, authPath } = await createPiAgentFromDir(
|
|
29
|
+
const { agent, modelSpec, authPath } = await createPiAgentFromDir(placement.workspace, {
|
|
36
30
|
model: opts.model,
|
|
37
31
|
authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
|
|
38
32
|
}).catch(failStartup);
|
|
@@ -8,7 +8,8 @@ import { resolveStateRoot, workspaceHint } from "../../paths.js";
|
|
|
8
8
|
import { CODING_TOOL_NAMES, resolveAgentTools } from "../../engines/pi/create.js";
|
|
9
9
|
import { loadAgentDefinition } from "../../engines/pi/definition.js";
|
|
10
10
|
import { reportFindingsIfChanged, reportToolCollisions } from "../../engines/pi/report.js";
|
|
11
|
-
import {
|
|
11
|
+
import { log } from "../../log.js";
|
|
12
|
+
import { reportModuleLoadFailures } from "../../loader.js";
|
|
12
13
|
import { nextRun } from "../../schedule/cron.js";
|
|
13
14
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
14
15
|
import { failStartup, placementOrExit } from "../fail.js";
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/** `fastagent invoke <message> [dir]`: run ONE turn against the assembled agent, then exit. */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { loadDotEnv } from "../../env.js";
|
|
5
3
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
6
4
|
import { runInvokeStream } from "../invoke-stream.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { reportAuth, resolveFirstRunModel } from "../shared.js";
|
|
5
|
+
import { failStartup } from "../fail.js";
|
|
6
|
+
import { enterAgentCommand, reportAuth } from "../shared.js";
|
|
10
7
|
export async function runInvoke(message, dirArg, opts) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
loadDotEnv(placement.agentDir);
|
|
14
|
-
installProxyFetch();
|
|
15
|
-
await resolveFirstRunModel(placement.agentDir, opts);
|
|
16
|
-
const { agent, modelSpec, authPath } = await createPiAgentFromDir(invokeDir, {
|
|
8
|
+
const placement = await enterAgentCommand(dirArg, opts);
|
|
9
|
+
const { agent, modelSpec, authPath } = await createPiAgentFromDir(placement.workspace, {
|
|
17
10
|
model: opts.model,
|
|
18
11
|
authPath: opts.authPath, // flag > FASTAGENT_AUTH_PATH > default — resolved by the opener (one owner)
|
|
19
12
|
}).catch(failStartup);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { loadDotEnv } from "../../env.js";
|
|
8
8
|
import { resolveStateRoot } from "../../paths.js";
|
|
9
|
-
import { reportModuleLoadFailures } from "../../
|
|
9
|
+
import { reportModuleLoadFailures } from "../../loader.js";
|
|
10
10
|
import { readRuns } from "../../schedule/audit.js";
|
|
11
11
|
import { nextRun } from "../../schedule/cron.js";
|
|
12
12
|
import { loadSchedules } from "../../schedule/discover.js";
|
|
@@ -2,42 +2,40 @@
|
|
|
2
2
|
* `fastagent start [dir]`: run the agent in production posture — the SAME assembly as dev (your
|
|
3
3
|
* directory is the agent), just no file-watching. No build step: start reads the definition directly.
|
|
4
4
|
*/
|
|
5
|
-
import { dirname
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
6
|
import { writeFileAtomic } from "../../atomic-write.js";
|
|
7
7
|
import { authSeedBytes, collectAuthSeed } from "../../deploy/secrets.js";
|
|
8
|
-
import { loadDotEnv } from "../../env.js";
|
|
9
8
|
import { resolveAuthPath, resolveSessionsDirOverride } from "../../engines/pi/config.js";
|
|
10
9
|
import { SECRET_FILE_MODE, ensureSecretsDir, resolveSecretsDir, isUnderDir, exists } from "../../paths.js";
|
|
11
10
|
import { log, setLogLevel } from "../../log.js";
|
|
12
11
|
import { createPiAgentFromDir } from "../../engines/pi/open.js";
|
|
13
12
|
import { mountAgentService } from "../../service.js";
|
|
14
13
|
import { logAgentLoop } from "../../observe.js";
|
|
15
|
-
import { installProxyFetch } from "../../proxy.js";
|
|
16
|
-
import { bindAddress } from "../../bind.js";
|
|
17
14
|
import { isAgentcoreRuntime, mountAgentcoreService } from "../../channels/agentcore-service.js";
|
|
18
15
|
import { createWakeAlarmSink } from "../../schedule/wake-alarm.js";
|
|
19
16
|
import { setWakeupsSink } from "../../schedule/wakeups.js";
|
|
20
|
-
import { failStartup
|
|
21
|
-
import {
|
|
22
|
-
import { parseBind, parsePort, reportAssembly
|
|
17
|
+
import { failStartup } from "../fail.js";
|
|
18
|
+
import { cliMountOptions, resolveBindHost, serveService } from "../serve.js";
|
|
19
|
+
import { enterAgentCommand, parseBind, parsePort, reportAssembly } from "../shared.js";
|
|
23
20
|
export async function runStart(dirArg, opts) {
|
|
24
|
-
const dir = resolve(dirArg);
|
|
25
21
|
// Flag validation first: a bad --port is a USAGE error (exit 2), and reporting it must not depend on
|
|
26
22
|
// the directory being an agent (which is a runtime/environment failure, exit 1).
|
|
27
23
|
const portFlag = parsePort(opts.port, "--port", "flag");
|
|
28
24
|
const bindFlag = parseBind(opts.bind);
|
|
29
|
-
const
|
|
25
|
+
const tunnel = opts.tunnel ?? false;
|
|
30
26
|
setLogLevel("info"); // production posture: info+, the debug turn trace (and its end-user content) gated out
|
|
31
|
-
|
|
32
|
-
installProxyFetch();
|
|
33
|
-
await resolveFirstRunModel(placement.agentDir, opts);
|
|
27
|
+
const placement = await enterAgentCommand(dirArg, opts);
|
|
34
28
|
// A `deploy --run` may carry the operator's local credential as FASTAGENT_AUTH_SEED —
|
|
35
29
|
// materialize it into the writable secrets dir BEFORE the opener resolves auth (once, absent-only).
|
|
36
30
|
// Same resolveAuthPath the opener uses — ONE owner of the flag > env > default chain.
|
|
37
31
|
await maybeSeedAuth(resolveAuthPath(placement.agentDir, opts.authPath));
|
|
38
32
|
// The same opener dev uses (single assembly source), just no watch.
|
|
39
|
-
const
|
|
40
|
-
|
|
33
|
+
const opened = await createPiAgentFromDir(placement.workspace, {
|
|
34
|
+
model: opts.model,
|
|
35
|
+
sessionsDir: resolveSessionsDirOverride(opts.sessionsDir),
|
|
36
|
+
authPath: opts.authPath,
|
|
37
|
+
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
38
|
+
}).catch(failStartup);
|
|
41
39
|
const { agent, agentDir, config, stateRoot, sessionsDir } = opened;
|
|
42
40
|
// The same report `dev` prints; `state:`/`sessions:` are start's own extras, and the persistence
|
|
43
41
|
// notes below are why (see reportAssembly on the asymmetry).
|
|
@@ -65,14 +63,9 @@ export async function runStart(dirArg, opts) {
|
|
|
65
63
|
log.info(`[fastagent] note: secrets (.env, rotated auth.json) live under the definition dir; point ` +
|
|
66
64
|
`FASTAGENT_SECRETS_DIR at a persistent volume so a redeploy that replaces the dir does not wipe them.`);
|
|
67
65
|
}
|
|
68
|
-
// Same debug turn trace as dev; gated out here by the info level
|
|
66
|
+
// Same debug turn trace as dev; gated out here by the info level.
|
|
69
67
|
const traced = logAgentLoop(agent);
|
|
70
|
-
|
|
71
|
-
// configured `localhost` is an ADDRESS by the time anything binds, renders or dials it.
|
|
72
|
-
const configured = config.http?.host;
|
|
73
|
-
const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
|
|
74
|
-
assertTunnelBindable(host, opts.tunnel ?? false, bindFlag ? "flag" : "config");
|
|
75
|
-
const control = { tunnel: opts.tunnel ?? false, ...(host !== undefined ? { host } : {}) };
|
|
68
|
+
const host = resolveBindHost(bindFlag, config.http?.host, tunnel);
|
|
76
69
|
// ONE branch for the whole posture. AgentCore assembles differently — lazy channels over a
|
|
77
70
|
// pre-restore state mount, an external clock, no resident connections — but it yields the same
|
|
78
71
|
// AgentService, so everything below this point is common.
|
|
@@ -81,36 +74,14 @@ export async function runStart(dirArg, opts) {
|
|
|
81
74
|
// handed to something that can, without inventing an ownership the singleton does not have.
|
|
82
75
|
const onStateReady = isAgentcoreRuntime() && config.selfSchedule ? armWakeAlarms(stateRoot) : undefined;
|
|
83
76
|
const service = await (isAgentcoreRuntime()
|
|
84
|
-
? mountAgentcoreService(opened, { wrapAgent: () => traced,
|
|
85
|
-
: mountAgentService(opened,
|
|
86
|
-
|
|
87
|
-
closeTimeoutMs: SHUTDOWN_GRACE_MS,
|
|
88
|
-
control,
|
|
89
|
-
onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
|
|
90
|
-
})).catch(failStartup);
|
|
91
|
-
serve(service.handler, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, {
|
|
92
|
-
ready: service.ready,
|
|
93
|
-
onListening: (p) => {
|
|
94
|
-
reportServing(service, host, p);
|
|
95
|
-
service.announce(p);
|
|
96
|
-
maybeTunnel(agentDir, service.channels.routes, p, opts.tunnel ?? false, stateRoot);
|
|
97
|
-
},
|
|
98
|
-
onShutdown: () => service.close(),
|
|
99
|
-
});
|
|
77
|
+
? mountAgentcoreService(opened, { wrapAgent: () => traced, onStateReady })
|
|
78
|
+
: mountAgentService(opened, cliMountOptions(() => traced))).catch(failStartup);
|
|
79
|
+
serveService(service, { port: portFlag ?? parsePort(process.env.PORT, "PORT env", "env") ?? config.http?.port ?? 8787, host }, { tunnel, agentDir, stateRoot });
|
|
100
80
|
// No graceful drain: webhook turns run fire-and-forget; SIGTERM just exits mid-turn. Whether an
|
|
101
81
|
// in-flight turn is LOST depends on the channel: the Telegram channel persists turn intent pre-ACK
|
|
102
82
|
// and replays it next start (turn-store.ts, L1 durable execution, at-least-once); HTTP and other
|
|
103
83
|
// channels have no such layer, so their in-flight turns are still lost (the asker re-invokes).
|
|
104
84
|
}
|
|
105
|
-
/** The opener, kept as its own call so `opened` can also feed the shared assembly below. */
|
|
106
|
-
function openStartDir(dir, opts, sessionsDir) {
|
|
107
|
-
return createPiAgentFromDir(dir, {
|
|
108
|
-
model: opts.model,
|
|
109
|
-
sessionsDir,
|
|
110
|
-
authPath: opts.authPath,
|
|
111
|
-
serving: true, // long-running serve: the scheduler poller runs (wake mounts iff config.selfSchedule)
|
|
112
|
-
}).catch(failStartup);
|
|
113
|
-
}
|
|
114
85
|
/**
|
|
115
86
|
* Materialize `FASTAGENT_AUTH_SEED` (base64 of an auth.json, set by `deploy --run`) into the
|
|
116
87
|
* writable secrets dir ONCE — only when the seed is set AND the auth file is absent, so a refreshed
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/** `fastagent tool <name> '<json>' [dir]`: run one tool's body directly with JSON args — no model. */
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
4
3
|
import { loadDotEnv } from "../../env.js";
|
|
5
4
|
import { loadConfig } from "../../engines/pi/config.js";
|
|
6
5
|
import { resolveAgentTools } from "../../engines/pi/create.js";
|
|
7
|
-
import { reportModuleLoadFailures } from "../../
|
|
6
|
+
import { reportModuleLoadFailures } from "../../loader.js";
|
|
8
7
|
import { turnContext } from "../../engines/pi/tool-context.js";
|
|
9
8
|
import { failStartup, failUsage, placementOrExit } from "../fail.js";
|
|
10
9
|
export async function runTool(name, argsJson, dirArg) {
|
|
@@ -26,12 +25,8 @@ export async function runTool(name, argsJson, dirArg) {
|
|
|
26
25
|
if (!tool) {
|
|
27
26
|
failStartup(new Error(`unknown tool "${name}". available: ${tools.map((t) => t.name).join(", ") || "(none)"}`));
|
|
28
27
|
}
|
|
29
|
-
// Authored tools read cwd from turnContext; coding tools are already rooted at the workspace.
|
|
30
|
-
|
|
31
|
-
const env = new NodeExecutionEnv({ cwd: workspace });
|
|
32
|
-
const result = await turnContext
|
|
33
|
-
.run({ cwd: workspace }, () => tool.execute(`cli-${name}`, args, undefined, undefined, { env }))
|
|
34
|
-
.catch(failStartup);
|
|
28
|
+
// Authored tools read cwd from turnContext; coding tools are already rooted at the workspace.
|
|
29
|
+
const result = await turnContext.run({ cwd: workspace }, () => tool.execute(`cli-${name}`, args)).catch(failStartup);
|
|
35
30
|
const out = result?.details !== undefined
|
|
36
31
|
? result.details
|
|
37
32
|
: (result?.content ?? []).map((c) => ("text" in c ? c.text : "")).join("");
|
package/dist/cli/kernel.d.ts
CHANGED
|
@@ -24,8 +24,6 @@ interface ArgSpec {
|
|
|
24
24
|
export interface FlagSpec {
|
|
25
25
|
flags: string;
|
|
26
26
|
description: string;
|
|
27
|
-
/** Parses but does not appear in help — for retired flags that should still explain themselves. */
|
|
28
|
-
hidden?: boolean;
|
|
29
27
|
/** Mutually exclusive with these {@link optionKey} values — validated at build time. */
|
|
30
28
|
conflicts?: string[];
|
|
31
29
|
}
|
package/dist/cli/kernel.js
CHANGED
package/dist/cli/program.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { fastagentVersion } from "../version.js";
|
|
8
8
|
import { buildProgram } from "./kernel.js";
|
|
9
|
+
import { DEPLOY_HOSTS } from "../deploy/hosts.js";
|
|
9
10
|
// Help groups (clig: most common commands first) — the authoring loop leads, operations close.
|
|
10
11
|
// Shared flags — same name, same meaning, on every command that supports them (clig: consistency).
|
|
11
12
|
const DIR_ARG = {
|
|
@@ -256,8 +257,6 @@ const start = {
|
|
|
256
257
|
input: f.input !== false,
|
|
257
258
|
}),
|
|
258
259
|
};
|
|
259
|
-
/** The retired app-creation flag — parsed so it can explain itself, hidden from help. */
|
|
260
|
-
const CREATE_APP = { flags: "--create-app", description: "(retired)", hidden: true };
|
|
261
260
|
const INGRESS = {
|
|
262
261
|
flags: "--ingress <mode>",
|
|
263
262
|
description: "Feishu/Lark ingress: websocket or webhook (interactive when omitted)",
|
|
@@ -280,18 +279,14 @@ const channelSub = (kind, summary, description, notes) => ({
|
|
|
280
279
|
summary,
|
|
281
280
|
description,
|
|
282
281
|
args: [DIR_ARG],
|
|
283
|
-
flags:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
? [
|
|
287
|
-
:
|
|
288
|
-
? [GROUP_BEHAVIOR, NO_ONBOARD, REPLACE_CONFIG]
|
|
289
|
-
: []),
|
|
290
|
-
],
|
|
282
|
+
flags: kind === "feishu" || kind === "lark"
|
|
283
|
+
? [INGRESS, GROUP_BEHAVIOR]
|
|
284
|
+
: kind === "slack"
|
|
285
|
+
? [GROUP_BEHAVIOR, NO_ONBOARD, REPLACE_CONFIG]
|
|
286
|
+
: [],
|
|
291
287
|
examples: [{ cmd: `fastagent add ${kind}` }],
|
|
292
288
|
...(notes ? { notes } : {}),
|
|
293
289
|
run: async (args, f) => (await import("./commands/add.js")).runAddChannel(kind, args[0], {
|
|
294
|
-
createApp: f.createApp === true,
|
|
295
290
|
ingress: f.ingress,
|
|
296
291
|
groupBehavior: f.groupBehavior,
|
|
297
292
|
onboard: f.onboard !== false,
|
|
@@ -357,7 +352,7 @@ const deploy = {
|
|
|
357
352
|
"CloudFormation stack (AWS Bedrock AgentCore Runtime + forwarder Lambda for webhooks + " +
|
|
358
353
|
"EventBridge rules for schedules; linux/arm64 image built locally). Durable ingress " +
|
|
359
354
|
"remains operator-owned (agentcore's forwarder URL is the exception — the stack owns it).",
|
|
360
|
-
args: [{ name: "<host>", description: "deploy target", choices: [
|
|
355
|
+
args: [{ name: "<host>", description: "deploy target", choices: [...DEPLOY_HOSTS] }, DIR_ARG],
|
|
361
356
|
flags: [
|
|
362
357
|
{
|
|
363
358
|
flags: "--run",
|
package/dist/cli/serve.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AgentService } from "../service.ts";
|
|
1
|
+
import type { Agent } from "../agent.ts";
|
|
2
|
+
import type { AgentService, MountAgentServiceOptions } from "../service.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Refuse `--tunnel` with a bind that cloudflared cannot reach: it dials the NAME `localhost:<port>`
|
|
5
5
|
* (the dev supervisor's tunnel too), so anything outside `127.0.0.1`/`::1`/wildcard — including a
|
|
@@ -10,6 +10,26 @@ import type { AgentService } from "../service.ts";
|
|
|
10
10
|
* came from config is broken runtime configuration (1).
|
|
11
11
|
*/
|
|
12
12
|
export declare function assertTunnelBindable(host: string | undefined, tunnel: boolean, source: "flag" | "config"): void;
|
|
13
|
+
/**
|
|
14
|
+
* The bind address a serve uses: the flag, else `http.host` from the config — through `bindAddress`,
|
|
15
|
+
* so a configured `localhost` is an ADDRESS by the time anything binds, renders or dials it — checked
|
|
16
|
+
* against `--tunnel` with the exit code the SOURCE of the value earns (serve.ts assertTunnelBindable).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveBindHost(bindFlag: string | undefined, configured: string | undefined, tunnel: boolean): string | undefined;
|
|
19
|
+
/** What the CLI adds to the assembly: its shutdown grace, and exit on a connection that drops. */
|
|
20
|
+
export declare function cliMountOptions(wrapAgent: (agent: Agent) => Agent): MountAgentServiceOptions;
|
|
21
|
+
/**
|
|
22
|
+
* Bind, report, announce the control plane, open the tunnel, and close in order on a signal — the
|
|
23
|
+
* tail dev's worker and start share once the service is assembled.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serveService(service: AgentService, bind: {
|
|
26
|
+
port: number;
|
|
27
|
+
host?: string;
|
|
28
|
+
}, posture: {
|
|
29
|
+
tunnel: boolean;
|
|
30
|
+
agentDir: string;
|
|
31
|
+
stateRoot: string;
|
|
32
|
+
}): void;
|
|
13
33
|
/**
|
|
14
34
|
* The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
|
|
15
35
|
* what mounted. One function because both commands must say the same thing at the same moment —
|
|
@@ -26,23 +46,20 @@ export declare function reportServing(service: AgentService, host: string | unde
|
|
|
26
46
|
* still needs one to dial, which is what `clientHost` gives (loopback for a wildcard, itself otherwise).
|
|
27
47
|
*/
|
|
28
48
|
export declare function readyAddressLines(host: string | undefined, boundPort: number, builtinInvoke: boolean): string[];
|
|
29
|
-
/** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
|
|
30
|
-
* a forced exit before the service answers would report a clean shutdown over a stuck channel. */
|
|
31
|
-
export declare const SHUTDOWN_GRACE_MS = 800;
|
|
32
49
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
50
|
+
* Write `<stateRoot>/control.json` so a LOCAL client (`fastagent attach`) can find the control plane
|
|
51
|
+
* once the port is known, and say what reaches it. Returns the file's removal — wired into the
|
|
52
|
+
* shutdown so a stale file cannot point a client at a dead port (`attach` then fails with "cannot
|
|
53
|
+
* read", accurate, instead of a stale token's misleading 401/ECONNREFUSED). A hard exit leaves the
|
|
54
|
+
* file behind: advisory, overwritten by the next boot.
|
|
55
|
+
*
|
|
56
|
+
* Here and not in the assembly: the file, its permissions and the warnings are how THIS process's
|
|
57
|
+
* operator finds and protects the plane. An embedder distributes `service.control` itself.
|
|
37
58
|
*/
|
|
38
|
-
export declare function
|
|
39
|
-
|
|
59
|
+
export declare function announceControl(control: {
|
|
60
|
+
token: string;
|
|
61
|
+
prefix: string;
|
|
62
|
+
} | undefined, stateRoot: string, bind: {
|
|
40
63
|
host?: string;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ready?: Promise<void>;
|
|
44
|
-
onListening?: (boundPort: number) => void;
|
|
45
|
-
onShutdown?: () => Promise<void> | void;
|
|
46
|
-
}): void;
|
|
47
|
-
/** Start a Cloudflare tunnel for route channels only. */
|
|
48
|
-
export declare function maybeTunnel(agentDir: string, routeChannels: string[], boundPort: number, tunnel: boolean, stateRoot?: string): void;
|
|
64
|
+
tunnel: boolean;
|
|
65
|
+
}, boundPort: number): () => void;
|
package/dist/cli/serve.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What `dev` (its worker) and `start` need beyond the service itself: binding a port, the shutdown
|
|
3
|
-
* order, the startup report,
|
|
3
|
+
* order, the startup report, the optional Cloudflare quick tunnel, and the options the CLI hands the
|
|
4
|
+
* assembly (`cliMountOptions`, `resolveBindHost` — policy ABOUT the assembly, decided per command).
|
|
4
5
|
*
|
|
5
|
-
* The ASSEMBLY is not here — it lives in `src/service.ts`, which a public entry may import and
|
|
6
|
-
* directory may not be (it decides process-level things: `fail.ts` calls `process.exit`).
|
|
6
|
+
* The ASSEMBLY itself is not here — it lives in `src/service.ts`, which a public entry may import and
|
|
7
|
+
* this directory may not be (it decides process-level things: `fail.ts` calls `process.exit`).
|
|
7
8
|
*/
|
|
9
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
8
11
|
import { INVOKE_EXAMPLE_BODY } from "../channels/http.js";
|
|
9
|
-
import { answersLocalhost, bindLabel, classifyBind, clientHost } from "../bind.js";
|
|
12
|
+
import { answersLocalhost, bindAddress, bindLabel, classifyBind, clientHost } from "../bind.js";
|
|
13
|
+
import { writeFileAtomic } from "../atomic-write.js";
|
|
10
14
|
import { serveNode } from "../channels/serve.js";
|
|
11
15
|
import { log } from "../log.js";
|
|
12
16
|
import { openExternalUrl } from "../open-url.js";
|
|
@@ -35,6 +39,45 @@ export function assertTunnelBindable(host, tunnel, source) {
|
|
|
35
39
|
failUsage(message);
|
|
36
40
|
failStartup(new Error(message));
|
|
37
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The bind address a serve uses: the flag, else `http.host` from the config — through `bindAddress`,
|
|
44
|
+
* so a configured `localhost` is an ADDRESS by the time anything binds, renders or dials it — checked
|
|
45
|
+
* against `--tunnel` with the exit code the SOURCE of the value earns (serve.ts assertTunnelBindable).
|
|
46
|
+
*/
|
|
47
|
+
export function resolveBindHost(bindFlag, configured, tunnel) {
|
|
48
|
+
const host = bindFlag ?? (configured === undefined ? undefined : bindAddress(configured));
|
|
49
|
+
assertTunnelBindable(host, tunnel, bindFlag ? "flag" : "config");
|
|
50
|
+
return host;
|
|
51
|
+
}
|
|
52
|
+
/** What the CLI adds to the assembly: its shutdown grace, and exit on a connection that drops. */
|
|
53
|
+
export function cliMountOptions(wrapAgent) {
|
|
54
|
+
return {
|
|
55
|
+
wrapAgent,
|
|
56
|
+
closeTimeoutMs: SHUTDOWN_GRACE_MS,
|
|
57
|
+
onChannelClosed: (name, error) => failStartup(new Error(`${name} ${error === undefined ? "closed unexpectedly" : `failed: ${String(error)}`}`)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Bind, report, announce the control plane, open the tunnel, and close in order on a signal — the
|
|
62
|
+
* tail dev's worker and start share once the service is assembled.
|
|
63
|
+
*/
|
|
64
|
+
export function serveService(service, bind, posture) {
|
|
65
|
+
const { host } = bind;
|
|
66
|
+
const { tunnel, agentDir, stateRoot } = posture;
|
|
67
|
+
let unannounce = () => { };
|
|
68
|
+
serve(service.handler, bind, {
|
|
69
|
+
ready: service.ready,
|
|
70
|
+
onListening: (p) => {
|
|
71
|
+
reportServing(service, host, p);
|
|
72
|
+
unannounce = announceControl(service.control, stateRoot, { host, tunnel }, p);
|
|
73
|
+
maybeTunnel(agentDir, service.channels.routes, p, tunnel, stateRoot);
|
|
74
|
+
},
|
|
75
|
+
onShutdown: () => {
|
|
76
|
+
unannounce();
|
|
77
|
+
return service.close();
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
38
81
|
/**
|
|
39
82
|
* The "we are serving" report: the supervisor message `dev`'s watcher waits for, the addresses, and
|
|
40
83
|
* what mounted. One function because both commands must say the same thing at the same moment —
|
|
@@ -68,9 +111,50 @@ export function readyAddressLines(host, boundPort, builtinInvoke) {
|
|
|
68
111
|
}
|
|
69
112
|
return lines;
|
|
70
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Write `<stateRoot>/control.json` so a LOCAL client (`fastagent attach`) can find the control plane
|
|
116
|
+
* once the port is known, and say what reaches it. Returns the file's removal — wired into the
|
|
117
|
+
* shutdown so a stale file cannot point a client at a dead port (`attach` then fails with "cannot
|
|
118
|
+
* read", accurate, instead of a stale token's misleading 401/ECONNREFUSED). A hard exit leaves the
|
|
119
|
+
* file behind: advisory, overwritten by the next boot.
|
|
120
|
+
*
|
|
121
|
+
* Here and not in the assembly: the file, its permissions and the warnings are how THIS process's
|
|
122
|
+
* operator finds and protects the plane. An embedder distributes `service.control` itself.
|
|
123
|
+
*/
|
|
124
|
+
export function announceControl(control, stateRoot, bind, boundPort) {
|
|
125
|
+
if (!control)
|
|
126
|
+
return () => { };
|
|
127
|
+
mkdirSync(stateRoot, { recursive: true, mode: 0o700 });
|
|
128
|
+
const path = join(stateRoot, "control.json");
|
|
129
|
+
const url = `http://${clientHost(bind.host)}:${boundPort}`;
|
|
130
|
+
writeFileAtomic(path, `${JSON.stringify({ url, token: control.token })}\n`, 0o600);
|
|
131
|
+
log.info(`[fastagent] session control on ${control.prefix}/* (token in ${path})`);
|
|
132
|
+
// LAN-reachable with the bearer token as the only protection — the tunnel and deploy paths warn
|
|
133
|
+
// loudly, and the LAN path must not be the silent third way past the local trust story. A
|
|
134
|
+
// loopback bind closes exactly that reach, so it earns silence.
|
|
135
|
+
const reach = classifyBind(bind.host);
|
|
136
|
+
if (reach !== "loopback") {
|
|
137
|
+
log.warn(`[fastagent] the port binds ${reach === "wildcard" ? "all interfaces" : `${bind.host} (off this machine)`}: ` +
|
|
138
|
+
"/control/* is reachable on your LAN, protected only by the bearer token — bind loopback " +
|
|
139
|
+
"(--bind 127.0.0.1), firewall the port, or wrap it for real exposure (docs/design/session-control.md §14)");
|
|
140
|
+
}
|
|
141
|
+
if (bind.tunnel) {
|
|
142
|
+
// Local trust = the token + its file permissions; --tunnel takes the whole port PUBLIC.
|
|
143
|
+
log.warn("[fastagent] --tunnel exposes /control/* (steer, stop, rewrite or delete a session) at the public tunnel URL, " +
|
|
144
|
+
"protected ONLY by the bearer token — wrap it with real auth before sharing that URL (docs/design/session-control.md §14)");
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
try {
|
|
148
|
+
rmSync(path, { force: true });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* the file is advisory — shutdown must not fail on it */
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
71
155
|
/** What the CLI gives a service to stop in, and the hard exit that follows it. The order matters:
|
|
72
156
|
* a forced exit before the service answers would report a clean shutdown over a stuck channel. */
|
|
73
|
-
|
|
157
|
+
const SHUTDOWN_GRACE_MS = 800;
|
|
74
158
|
const FORCED_EXIT_MS = 1_500;
|
|
75
159
|
/**
|
|
76
160
|
* Bind HTTP and report ready — but only once the SERVICE is, which is not the same moment: a bound
|
|
@@ -78,7 +162,7 @@ const FORCED_EXIT_MS = 1_500;
|
|
|
78
162
|
* awaits `hooks.ready` (mountAgentService owns the connections themselves) before announcing
|
|
79
163
|
* anything. Signals are the sole clean-shutdown command; `host` unset binds all interfaces.
|
|
80
164
|
*/
|
|
81
|
-
|
|
165
|
+
function serve(handler, bind, hooks = {}) {
|
|
82
166
|
const { port, host } = bind;
|
|
83
167
|
const hosted = serveNode(handler, { port, host });
|
|
84
168
|
let stopping = false;
|
|
@@ -141,7 +225,7 @@ export function serve(handler, bind, hooks = {}) {
|
|
|
141
225
|
});
|
|
142
226
|
}
|
|
143
227
|
/** Start a Cloudflare tunnel for route channels only. */
|
|
144
|
-
|
|
228
|
+
function maybeTunnel(agentDir, routeChannels, boundPort, tunnel, stateRoot) {
|
|
145
229
|
if (!tunnel || process.env.FASTAGENT_DEV_WORKER === "1")
|
|
146
230
|
return;
|
|
147
231
|
void startCloudflareTunnel(boundPort).then((instance) => {
|