@fastagent-sh/fastagent 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-service.d.ts +12 -11
- package/dist/channels/agentcore-service.js +14 -18
- package/dist/channels/agentcore.d.ts +11 -19
- package/dist/channels/agentcore.js +14 -7
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +239 -96
- package/dist/channels/discover.d.ts +26 -4
- package/dist/channels/discover.js +12 -23
- package/dist/channels/feishu/feishu-api.js +5 -8
- package/dist/channels/feishu/feishu.js +35 -18
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/onboard.js +5 -5
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.js +1 -1
- package/dist/channels/slack/scaffold/slack-send.ts +2 -17
- package/dist/channels/slack/slack-api.js +22 -54
- package/dist/channels/slack/slack.js +13 -13
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +57 -69
- package/dist/cli/add-slack.js +6 -5
- package/dist/cli/commands/add.js +1 -5
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/deploy.d.ts +50 -5
- package/dist/cli/commands/deploy.js +183 -134
- package/dist/cli/commands/dev.js +4 -31
- package/dist/cli/commands/info.js +1 -2
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/start.js +31 -41
- package/dist/cli/serve.d.ts +12 -21
- package/dist/cli/serve.js +20 -25
- package/dist/cli/shared.d.ts +40 -10
- package/dist/cli/shared.js +51 -4
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +12 -10
- package/dist/deploy/agentcore/plan.js +39 -31
- package/dist/deploy/agentcore/run.d.ts +5 -4
- package/dist/deploy/agentcore/run.js +13 -36
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +18 -1
- package/dist/deploy/docker/run.js +10 -0
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +7 -22
- package/dist/deploy/fly/run.d.ts +9 -23
- package/dist/deploy/fly/run.js +32 -68
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +26 -22
- package/dist/deploy/railway/plan.d.ts +4 -5
- package/dist/deploy/railway/plan.js +11 -26
- package/dist/deploy/railway/run.d.ts +9 -9
- package/dist/deploy/railway/run.js +40 -40
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +64 -12
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.js +20 -39
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +14 -2
- package/dist/engines/pi/create.js +2 -2
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +3 -4
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +1 -2
- package/dist/engines/pi/open.js +1 -1
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +6 -33
- package/dist/engines/pi/session-control.d.ts +19 -6
- package/dist/engines/pi/session-control.js +565 -470
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +4 -4
- package/dist/engines/pi/session-settings.js +7 -4
- package/dist/engines/pi/session-store.d.ts +83 -13
- package/dist/engines/pi/session-store.js +408 -55
- package/dist/engines/pi/tool-context.d.ts +21 -8
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.js +2 -2
- package/dist/env.js +1 -2
- package/dist/loader.d.ts +49 -7
- package/dist/loader.js +79 -18
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +27 -2
- package/dist/scaffold/add-channel.js +2 -2
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +13 -7
- package/dist/schedule/wake-alarm.js +88 -46
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +1 -2
- package/dist/service.js +27 -5
- package/dist/session-remote.d.ts +7 -2
- package/dist/session-remote.js +162 -92
- package/dist/session.d.ts +226 -92
- package/dist/session.js +61 -23
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +18 -3
- package/dist/tunnel.js +32 -40
- package/package.json +1 -1
package/dist/loader.d.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/** One module file a directory declares. `name` is what the domain knows it by; nothing is imported
|
|
2
|
+
* to produce this. */
|
|
3
|
+
export interface InventoryEntry {
|
|
4
|
+
/** Basename without extension — the authoritative name for tools/channels/schedules. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** "tools/foo.ts"-style label for errors and collisions. */
|
|
7
|
+
label: string;
|
|
8
|
+
file: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* WHAT A CODE-INPUT DIRECTORY DECLARES — the single answer to "which files here are modules",
|
|
12
|
+
* without importing any of them.
|
|
13
|
+
*
|
|
14
|
+
* It exists because four consumers need that answer and only one of them may import: the loader
|
|
15
|
+
* below, `fastagent info`'s channel listing, `--tunnel`'s webhook registration, and the deploy
|
|
16
|
+
* pre-flight's schedule probe. When they each read the directory themselves they disagreed — on
|
|
17
|
+
* what counts as a module file, on how to strip the extension, on which errno means "no such
|
|
18
|
+
* directory" — and a fix had to be applied in four places, which is why the fourth kept being
|
|
19
|
+
* missed. There is one reading now; what to DO with a failure stays with the caller, because that
|
|
20
|
+
* genuinely differs (the loader throws, the tunnel cannot).
|
|
21
|
+
*
|
|
22
|
+
* A missing directory is an empty inventory. Everything else throws, ENOTDIR included: `channels`
|
|
23
|
+
* and `schedules` are named directories, so the path existing as a FILE is a mistake in the agent
|
|
24
|
+
* rather than an agent without one (`service.test.ts` pins that for `schedules`). `paths.ts` folds
|
|
25
|
+
* ENOTDIR into its empty scan for the opposite reason — it asks which children HAPPEN to be agent
|
|
26
|
+
* dirs, where a file simply is not one. `not_found` is a non-Node runtime's ENOENT.
|
|
27
|
+
*
|
|
28
|
+
* SYMLINKS ARE SKIPPED, and that is a boundary rather than an oversight: `assertInsideAgentDir`
|
|
29
|
+
* guards the code-input DIRECTORY against escaping the definition, and nothing guards the entries
|
|
30
|
+
* inside it, so following a link would import from anywhere on the box past the very check meant to
|
|
31
|
+
* prevent it. Do not "fix" this by following them — report them, which this does.
|
|
32
|
+
*
|
|
33
|
+
* The skip is WARNED HERE, not handed back, because "this file is not loadable" holds for all four
|
|
34
|
+
* consumers and only one of them imports — a listing that reports the name it cannot load reads as
|
|
35
|
+
* "I never created it". What to do about a load FAILURE does differ per caller, so that travels as
|
|
36
|
+
* data. One warning per READ, so a command that both lists and loads the same directory (`deploy
|
|
37
|
+
* agentcore` does) says it twice — both readings are true, and remembering what was already said
|
|
38
|
+
* would mean a restart stops mentioning a skip that is still there.
|
|
39
|
+
*
|
|
40
|
+
* A skip is therefore a warning ONLY, unlike a {@link ModuleLoadFailure}: it is absent from `info
|
|
41
|
+
* --json` and `deploy` does not gate on it. That is the deliberate cost of one report for four
|
|
42
|
+
* consumers, three of which list names and have nowhere to put data. To give a machine consumer the
|
|
43
|
+
* skips, return them beside the entries — do not reconstruct them from stderr.
|
|
44
|
+
*/
|
|
45
|
+
export declare function moduleInventory(subDir: string): Promise<InventoryEntry[]>;
|
|
3
46
|
export interface DiscoveredModule {
|
|
4
47
|
/** Basename without extension — the authoritative name for tools/channels. */
|
|
5
48
|
name: string;
|
|
@@ -21,11 +64,10 @@ export interface ModuleLoadFailure {
|
|
|
21
64
|
message: string;
|
|
22
65
|
}
|
|
23
66
|
/**
|
|
24
|
-
* Import every module
|
|
25
|
-
*
|
|
26
|
-
* report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
|
|
27
|
-
* same way.
|
|
28
|
-
* — that's not a per-file problem.)
|
|
67
|
+
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
68
|
+
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
|
69
|
+
* report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
|
|
70
|
+
* the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
|
|
29
71
|
*/
|
|
30
72
|
export declare function loadModuleDir(subDir: string): Promise<{
|
|
31
73
|
modules: DiscoveredModule[];
|
package/dist/loader.js
CHANGED
|
@@ -9,40 +9,101 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { readdir } from "node:fs/promises";
|
|
10
10
|
import { basename, extname, join } from "node:path";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { log } from "./log.js";
|
|
12
13
|
const MODULE_EXTS = new Set([".ts", ".js", ".mjs"]);
|
|
13
14
|
/** Whether `name` is an importable agent module (a discovery candidate, not a type declaration). */
|
|
14
|
-
|
|
15
|
+
function isModuleFile(name) {
|
|
15
16
|
return MODULE_EXTS.has(extname(name)) && !name.endsWith(".d.ts");
|
|
16
17
|
}
|
|
18
|
+
/** The name a module is known by: its basename without the extension. */
|
|
19
|
+
function moduleName(fileName) {
|
|
20
|
+
return basename(fileName, extname(fileName));
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* WHAT A CODE-INPUT DIRECTORY DECLARES — the single answer to "which files here are modules",
|
|
24
|
+
* without importing any of them.
|
|
25
|
+
*
|
|
26
|
+
* It exists because four consumers need that answer and only one of them may import: the loader
|
|
27
|
+
* below, `fastagent info`'s channel listing, `--tunnel`'s webhook registration, and the deploy
|
|
28
|
+
* pre-flight's schedule probe. When they each read the directory themselves they disagreed — on
|
|
29
|
+
* what counts as a module file, on how to strip the extension, on which errno means "no such
|
|
30
|
+
* directory" — and a fix had to be applied in four places, which is why the fourth kept being
|
|
31
|
+
* missed. There is one reading now; what to DO with a failure stays with the caller, because that
|
|
32
|
+
* genuinely differs (the loader throws, the tunnel cannot).
|
|
33
|
+
*
|
|
34
|
+
* A missing directory is an empty inventory. Everything else throws, ENOTDIR included: `channels`
|
|
35
|
+
* and `schedules` are named directories, so the path existing as a FILE is a mistake in the agent
|
|
36
|
+
* rather than an agent without one (`service.test.ts` pins that for `schedules`). `paths.ts` folds
|
|
37
|
+
* ENOTDIR into its empty scan for the opposite reason — it asks which children HAPPEN to be agent
|
|
38
|
+
* dirs, where a file simply is not one. `not_found` is a non-Node runtime's ENOENT.
|
|
39
|
+
*
|
|
40
|
+
* SYMLINKS ARE SKIPPED, and that is a boundary rather than an oversight: `assertInsideAgentDir`
|
|
41
|
+
* guards the code-input DIRECTORY against escaping the definition, and nothing guards the entries
|
|
42
|
+
* inside it, so following a link would import from anywhere on the box past the very check meant to
|
|
43
|
+
* prevent it. Do not "fix" this by following them — report them, which this does.
|
|
44
|
+
*
|
|
45
|
+
* The skip is WARNED HERE, not handed back, because "this file is not loadable" holds for all four
|
|
46
|
+
* consumers and only one of them imports — a listing that reports the name it cannot load reads as
|
|
47
|
+
* "I never created it". What to do about a load FAILURE does differ per caller, so that travels as
|
|
48
|
+
* data. One warning per READ, so a command that both lists and loads the same directory (`deploy
|
|
49
|
+
* agentcore` does) says it twice — both readings are true, and remembering what was already said
|
|
50
|
+
* would mean a restart stops mentioning a skip that is still there.
|
|
51
|
+
*
|
|
52
|
+
* A skip is therefore a warning ONLY, unlike a {@link ModuleLoadFailure}: it is absent from `info
|
|
53
|
+
* --json` and `deploy` does not gate on it. That is the deliberate cost of one report for four
|
|
54
|
+
* consumers, three of which list names and have nowhere to put data. To give a machine consumer the
|
|
55
|
+
* skips, return them beside the entries — do not reconstruct them from stderr.
|
|
23
56
|
*/
|
|
24
|
-
export async function
|
|
25
|
-
let
|
|
57
|
+
export async function moduleInventory(subDir) {
|
|
58
|
+
let dirents;
|
|
26
59
|
try {
|
|
27
|
-
|
|
60
|
+
dirents = await readdir(subDir, { withFileTypes: true });
|
|
28
61
|
}
|
|
29
62
|
catch (error) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code === "ENOENT" || code === "not_found")
|
|
65
|
+
return [];
|
|
33
66
|
throw new Error(`cannot read ${subDir}: ${error.message}`);
|
|
34
67
|
}
|
|
35
68
|
const sub = basename(subDir);
|
|
69
|
+
const entries = [];
|
|
70
|
+
// Sorted by the NAME a consumer sees, so none of them re-sorts and none can disagree about order.
|
|
71
|
+
// Filename breaks a tie: `foo.js` and `foo.ts` both read as `foo`, and the domain loaders document
|
|
72
|
+
// that the FIRST wins — deciding that here keeps it from depending on readdir's order.
|
|
73
|
+
const byName = (a, b) => moduleName(a.name).localeCompare(moduleName(b.name)) || a.name.localeCompare(b.name);
|
|
74
|
+
for (const dirent of dirents.sort(byName)) {
|
|
75
|
+
if (!isModuleFile(dirent.name))
|
|
76
|
+
continue;
|
|
77
|
+
const label = `${sub}/${dirent.name}`;
|
|
78
|
+
if (dirent.isFile()) {
|
|
79
|
+
entries.push({ name: moduleName(dirent.name), label, file: join(subDir, dirent.name) });
|
|
80
|
+
}
|
|
81
|
+
else if (dirent.isSymbolicLink()) {
|
|
82
|
+
log.warn(`[fastagent] ${label} is a symlink — code inputs must be real files inside the agent dir — not loaded`);
|
|
83
|
+
}
|
|
84
|
+
else if (dirent.isDirectory()) {
|
|
85
|
+
log.warn(`[fastagent] ${label} is a directory, not a file — not loaded`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
log.warn(`[fastagent] ${label} is not a regular file — not loaded`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
95
|
+
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
|
96
|
+
* report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
|
|
97
|
+
* the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
|
|
98
|
+
*/
|
|
99
|
+
export async function loadModuleDir(subDir) {
|
|
100
|
+
const entries = await moduleInventory(subDir);
|
|
36
101
|
const modules = [];
|
|
37
102
|
const failures = [];
|
|
38
|
-
for (const
|
|
39
|
-
if (!entry.isFile() || !isModuleFile(entry.name))
|
|
40
|
-
continue;
|
|
41
|
-
const file = join(subDir, entry.name);
|
|
42
|
-
const label = `${sub}/${entry.name}`;
|
|
103
|
+
for (const { name, label, file } of entries) {
|
|
43
104
|
try {
|
|
44
105
|
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
45
|
-
modules.push({ name
|
|
106
|
+
modules.push({ name, label, file, mod });
|
|
46
107
|
}
|
|
47
108
|
catch (error) {
|
|
48
109
|
failures.push({
|
package/dist/paths.d.ts
CHANGED
|
@@ -136,6 +136,25 @@ export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv):
|
|
|
136
136
|
* auth.json but cannot move the file it is read from (env.ts dotEnvPath).
|
|
137
137
|
*/
|
|
138
138
|
export declare function resolveSecretsDir(dir: string, env?: NodeJS.ProcessEnv): string;
|
|
139
|
+
/** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
|
|
140
|
+
export declare const SECRET_FILE_MODE = 384;
|
|
141
|
+
/**
|
|
142
|
+
* Create the secrets directory with the mode its contents require — and REPAIR it when it already
|
|
143
|
+
* exists, which is the case that matters.
|
|
144
|
+
*
|
|
145
|
+
* The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
|
|
146
|
+
* below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
|
|
147
|
+
* writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
|
|
148
|
+
* later, more careful caller silently inherits the first one's answer. Four callers create this
|
|
149
|
+
* directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
|
|
150
|
+
* is init → add → login, so the careful one is LAST: the rule has to live where all of them can
|
|
151
|
+
* reach it, and it has to chmod rather than trust the create.
|
|
152
|
+
*
|
|
153
|
+
* A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
|
|
154
|
+
* reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
|
|
155
|
+
* directory they pointed it at, and says nothing about the way out.
|
|
156
|
+
*/
|
|
157
|
+
export declare function ensureSecretsDir(dir: string): Promise<void>;
|
|
139
158
|
/**
|
|
140
159
|
* Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
|
|
141
160
|
* absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
|
package/dist/paths.js
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
* modules depend on it for something the engine has no say in.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
19
|
-
import { access } from "node:fs/promises";
|
|
20
|
-
import { realpath } from "node:fs/promises";
|
|
19
|
+
import { access, chmod, mkdir, realpath } from "node:fs/promises";
|
|
21
20
|
import { homedir } from "node:os";
|
|
22
21
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
22
|
/**
|
|
@@ -309,6 +308,32 @@ export function resolveStateRoot(dir, env = process.env) {
|
|
|
309
308
|
export function resolveSecretsDir(dir, env = process.env) {
|
|
310
309
|
return resolveOverridePath(env.FASTAGENT_SECRETS_DIR) ?? join(resolve(dir), SECRETS_DIRNAME);
|
|
311
310
|
}
|
|
311
|
+
/** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
|
|
312
|
+
export const SECRET_FILE_MODE = 0o600;
|
|
313
|
+
/** What the secrets directory itself is created with. */
|
|
314
|
+
const SECRETS_DIR_MODE = 0o700;
|
|
315
|
+
/**
|
|
316
|
+
* Create the secrets directory with the mode its contents require — and REPAIR it when it already
|
|
317
|
+
* exists, which is the case that matters.
|
|
318
|
+
*
|
|
319
|
+
* The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
|
|
320
|
+
* below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
|
|
321
|
+
* writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
|
|
322
|
+
* later, more careful caller silently inherits the first one's answer. Four callers create this
|
|
323
|
+
* directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
|
|
324
|
+
* is init → add → login, so the careful one is LAST: the rule has to live where all of them can
|
|
325
|
+
* reach it, and it has to chmod rather than trust the create.
|
|
326
|
+
*
|
|
327
|
+
* A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
|
|
328
|
+
* reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
|
|
329
|
+
* directory they pointed it at, and says nothing about the way out.
|
|
330
|
+
*/
|
|
331
|
+
export async function ensureSecretsDir(dir) {
|
|
332
|
+
await mkdir(dir, { recursive: true, mode: SECRETS_DIR_MODE });
|
|
333
|
+
await chmod(dir, SECRETS_DIR_MODE).catch((e) => {
|
|
334
|
+
throw new Error(`cannot secure secrets dir ${dir} (fastagent keeps it 0700): ${e.message} — point --auth-path/FASTAGENT_SECRETS_DIR at a directory this process owns`, { cause: e });
|
|
335
|
+
});
|
|
336
|
+
}
|
|
312
337
|
/**
|
|
313
338
|
* Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
|
|
314
339
|
* absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { detectRuntime } from "../runtime.js";
|
|
10
|
-
import { SECRETS_DIRNAME, assertInsideAgentDir, exists } from "../paths.js";
|
|
10
|
+
import { SECRETS_DIRNAME, assertInsideAgentDir, ensureSecretsDir, exists } from "../paths.js";
|
|
11
11
|
import { baseTemplate, channelBundleFiles, channelTemplate } from "./templates.js";
|
|
12
12
|
import { dotEnvPath, envExamplePath, parseEnvContent } from "../env.js";
|
|
13
13
|
const CHANNEL_SCAFFOLDS = {
|
|
@@ -229,7 +229,7 @@ function mentionsEnvName(content, name) {
|
|
|
229
229
|
export async function appendChannelDotEnv(dir, kind, generated, overwrite = [], ingress = "webhook") {
|
|
230
230
|
const file = dotEnvPath(dir);
|
|
231
231
|
const secretsDir = dirname(file);
|
|
232
|
-
await
|
|
232
|
+
await ensureSecretsDir(secretsDir);
|
|
233
233
|
// THE one exception to "fastagent has no opinion about git": the directory it writes secrets into
|
|
234
234
|
// carries its own `.gitignore`. `init` writes it, and so does this — the reachable case where it is
|
|
235
235
|
// missing (a hand-made agent) is exactly the one where the next line mints an unrecoverable app
|
package/dist/scaffold/init.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { lstat, mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
29
29
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
30
|
-
import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, exists, } from "../paths.js";
|
|
30
|
+
import { AGENT_CONFIG_NAMES, DEFAULT_AGENT_DIRNAME, SECRETS_DIRNAME, agentDefinitionOwner, agentsAt, displayPath, ensureSecretsDir, exists, } from "../paths.js";
|
|
31
31
|
import { baseTemplate, packageJson, toPackageName } from "./templates.js";
|
|
32
32
|
import { fastagentVersion } from "../version.js";
|
|
33
33
|
/** The agent directory name for a raw `--agent-dir` value: the default when unset, and `./bot` read as
|
|
@@ -191,7 +191,12 @@ export async function scaffoldAgent(dir, options = {}) {
|
|
|
191
191
|
try {
|
|
192
192
|
for (const file of files) {
|
|
193
193
|
const abs = join(dir, file.rel);
|
|
194
|
-
|
|
194
|
+
// The secrets dir carries a mode; every other directory here is ordinary. `init` is usually the
|
|
195
|
+
// FIRST of the four writers to create it, and whoever creates it decides the mode for the rest.
|
|
196
|
+
if (basename(dirname(abs)) === SECRETS_DIRNAME)
|
|
197
|
+
await ensureSecretsDir(dirname(abs));
|
|
198
|
+
else
|
|
199
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
195
200
|
try {
|
|
196
201
|
await writeFile(abs, file.content, { flag: "wx" });
|
|
197
202
|
created.push(file.rel);
|
|
@@ -3,8 +3,6 @@ import { defineTool, z } from "@fastagent-sh/fastagent";
|
|
|
3
3
|
// A code tool: filename (fetch-url.ts) is the tool name. tools/ is auto-discovered,
|
|
4
4
|
// so it needs no registration in fastagent.config. Test it without a model:
|
|
5
5
|
// fastagent tool fetch-url '{"url":"https://example.com"}'
|
|
6
|
-
// Serving this behind a public channel? The URL then comes from untrusted users — add an
|
|
7
|
-
// allowlist or block private-network addresses (localhost, 169.254.169.254, …) to prevent SSRF.
|
|
8
6
|
const MAX_TEXT = 20_000; // keep a huge page from flooding the model's context
|
|
9
7
|
|
|
10
8
|
export default defineTool({
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* named from its filename. This is the FILE producer of scheduled invocations (the author's, declarative,
|
|
5
5
|
* git-tracked, deploy-guaranteed); the agent's `wake` tool is the second producer.
|
|
6
6
|
*/
|
|
7
|
-
import { readdir } from "node:fs/promises";
|
|
8
7
|
import { join } from "node:path";
|
|
9
|
-
import {
|
|
8
|
+
import { loadModuleDir, moduleInventory } from "../loader.js";
|
|
10
9
|
import { assertInsideAgentDir } from "../paths.js";
|
|
11
10
|
import { cronError } from "./cron.js";
|
|
12
11
|
/** Schedule file basenames under `<dir>/schedules/` — an existence probe listed WITHOUT importing
|
|
@@ -14,19 +13,8 @@ import { cronError } from "./cron.js";
|
|
|
14
13
|
* since it also reports broken files and next instants). */
|
|
15
14
|
export async function discoverScheduleFiles(dir) {
|
|
16
15
|
await assertInsideAgentDir(dir, "schedules");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
names = await readdir(join(dir, "schedules"));
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error.code === "ENOENT")
|
|
23
|
-
return [];
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
return names
|
|
27
|
-
.filter(isModuleFile)
|
|
28
|
-
.map((n) => n.replace(/\.(ts|js|mjs)$/, ""))
|
|
29
|
-
.sort();
|
|
16
|
+
const entries = await moduleInventory(join(dir, "schedules"));
|
|
17
|
+
return entries.map((entry) => entry.name);
|
|
30
18
|
}
|
|
31
19
|
/**
|
|
32
20
|
* Discover schedules in `<dir>/schedules/`: each file default-exports a `defineSchedule({...})`, named
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Wakeup } from "./wakeups.ts";
|
|
2
2
|
/** The forwarder's reserved wake-alarm path — never forwarded to channel routes. */
|
|
3
3
|
export declare const WAKE_ALARM_PATH = "/__fastagent/wake-alarm";
|
|
4
4
|
/** One desired alarm: mirror of a pending wake-up (id names the EventBridge schedule; at = fireAt). */
|
|
@@ -31,6 +31,17 @@ export declare function toAlarms(pending: Wakeup[], now: Date): WakeAlarm[];
|
|
|
31
31
|
* when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
|
|
32
32
|
* failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
|
|
33
33
|
* still fires on the next time the box happens to be awake), it must never break the store write.
|
|
34
|
+
*
|
|
35
|
+
* A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
|
|
36
|
+
* state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
|
|
37
|
+
* a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
|
|
38
|
+
* {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
|
|
39
|
+
* do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
|
|
40
|
+
* number of retries could clear.
|
|
41
|
+
*
|
|
42
|
+
* Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
|
|
43
|
+
* the call that started it, and its dirty flag is shared, so a second root would fold into the first
|
|
44
|
+
* one's pass.
|
|
34
45
|
*/
|
|
35
46
|
export declare function createWakeAlarmSink(options: {
|
|
36
47
|
secret: string;
|
|
@@ -39,9 +50,4 @@ export declare function createWakeAlarmSink(options: {
|
|
|
39
50
|
now?: () => Date;
|
|
40
51
|
/** Injectable retry pause (tests); defaults to exponential-ish backoff off RETRY_BASE_MS. */
|
|
41
52
|
delay?: (ms: number) => Promise<void>;
|
|
42
|
-
}): (stateRoot: string
|
|
43
|
-
/**
|
|
44
|
-
* One boot-time reconcile: pending wake-ups may exist while their alarms were lost (a deploy
|
|
45
|
-
* replaced the forwarder, a sink call failed) — re-mirror the current set once at start.
|
|
46
|
-
*/
|
|
47
|
-
export declare function reconcileWakeAlarms(stateRoot: string, sink: (stateRoot: string, pending: Wakeup[]) => void): void;
|
|
53
|
+
}): (stateRoot: string) => void;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* Function URL at cold start), and the adapter persists it here; every wake write happens inside a
|
|
18
18
|
* turn, and every ingress turn arrived through an envelope, so the URL is always known by then.
|
|
19
19
|
*
|
|
20
|
-
* Reconciliation is DECLARATIVE (the
|
|
20
|
+
* Reconciliation is DECLARATIVE (the sink is told only THAT the store changed and reads the full
|
|
21
|
+
* pending set back per attempt, so no retry ever carries a stale view) and deletion is lazy: a
|
|
21
22
|
* cancelled wake-up's alarm still fires its poke, finds nothing due, and self-deletes — a harmless
|
|
22
23
|
* wasted wake-up of the box, traded for never needing list/delete choreography.
|
|
23
24
|
*/
|
|
@@ -71,66 +72,107 @@ export function toAlarms(pending, now) {
|
|
|
71
72
|
* when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
|
|
72
73
|
* failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
|
|
73
74
|
* still fires on the next time the box happens to be awake), it must never break the store write.
|
|
75
|
+
*
|
|
76
|
+
* A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
|
|
77
|
+
* state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
|
|
78
|
+
* a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
|
|
79
|
+
* {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
|
|
80
|
+
* do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
|
|
81
|
+
* number of retries could clear.
|
|
82
|
+
*
|
|
83
|
+
* Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
|
|
84
|
+
* the call that started it, and its dirty flag is shared, so a second root would fold into the first
|
|
85
|
+
* one's pass.
|
|
74
86
|
*/
|
|
75
87
|
export function createWakeAlarmSink(options) {
|
|
76
88
|
const { secret, fetchImpl = fetch, now = () => new Date() } = options;
|
|
77
89
|
const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
let running = false;
|
|
91
|
+
let dirty = false;
|
|
92
|
+
/** One POST of the CURRENT desired set. Returns false to retry, true when there is nothing left
|
|
93
|
+
* to do (converged, or nothing this loop can act on). */
|
|
94
|
+
async function attemptOnce(stateRoot, attempt) {
|
|
95
|
+
const alarms = toAlarms(listWakeups(stateRoot), now());
|
|
96
|
+
// Nothing future to mirror: converged. Deletion is lazy by design — alarms already mirrored for
|
|
97
|
+
// cancelled wake-ups fire, find nothing, self-delete — so an empty set is never POSTed.
|
|
98
|
+
if (alarms.length === 0)
|
|
99
|
+
return true;
|
|
100
|
+
const url = readWakeAlarmUrl(stateRoot);
|
|
101
|
+
if (!url) {
|
|
102
|
+
// Before the first envelope, or an unreadable state mount (readWakeAlarmUrl folds both into
|
|
103
|
+
// undefined). The wake itself is stored; the next store mutation or boot re-mirrors it.
|
|
104
|
+
log.warn("[schedule] wake alarm skipped — forwarder URL unavailable (not seen yet, or unreadable)");
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const body = { secret, alarms };
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetchImpl(`${url.replace(/\/$/, "")}${WAKE_ALARM_PATH}`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { "content-type": "application/json" },
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
|
114
|
+
});
|
|
115
|
+
if (res.ok)
|
|
116
|
+
return true;
|
|
117
|
+
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: HTTP ${res.status}`);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(e)}`);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
async function reconcile(stateRoot) {
|
|
82
125
|
// Counted as in-flight work: the retry loop is exactly the window where the box must not be
|
|
83
126
|
// reclaimed — idle away mid-retry and a pending wake has no alarm until the next boot.
|
|
84
127
|
const workDone = beginWork();
|
|
128
|
+
// Consecutive failures ACROSS passes, not within one. A mid-retry mutation restarts the attempt
|
|
129
|
+
// sequence (the backoff and the log's `n/N` are about the new desired set), but it must not renew
|
|
130
|
+
// the budget: a store mutating faster than the backoff would keep the loop alive forever, and the
|
|
131
|
+
// one loud line saying the forwarder is down — the per-attempt warns are filterable — would never
|
|
132
|
+
// be reached. This counter is the thing that survives to reach it.
|
|
133
|
+
let failures = 0;
|
|
85
134
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
while (dirty && failures < MAX_SYNC_ATTEMPTS) {
|
|
136
|
+
dirty = false;
|
|
137
|
+
for (let attempt = 1; attempt <= MAX_SYNC_ATTEMPTS; attempt++) {
|
|
138
|
+
if (await attemptOnce(stateRoot, attempt)) {
|
|
139
|
+
failures = 0;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
failures++;
|
|
143
|
+
if (attempt === MAX_SYNC_ATTEMPTS || failures >= MAX_SYNC_ATTEMPTS)
|
|
144
|
+
break;
|
|
145
|
+
await delay(RETRY_BASE_MS * attempt);
|
|
146
|
+
// A mutation landed mid-retry: the desired state moved, so this budget is spent on a set
|
|
147
|
+
// that no longer exists. Restart the attempt count against the new one.
|
|
148
|
+
if (dirty)
|
|
149
|
+
break;
|
|
99
150
|
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(e)}`);
|
|
102
|
-
}
|
|
103
|
-
await delay(RETRY_BASE_MS * attempt);
|
|
104
151
|
}
|
|
105
|
-
|
|
106
|
-
`
|
|
152
|
+
if (failures >= MAX_SYNC_ATTEMPTS) {
|
|
153
|
+
log.error(`[schedule] wake alarm sync FAILED after ${MAX_SYNC_ATTEMPTS} attempts — pending wake-ups have no ` +
|
|
154
|
+
`external alarm until the next store change or boot re-mirrors them`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
// The END of the error path: nothing awaits this loop, so an escape is an unhandled rejection
|
|
159
|
+
// that kills the container. `listWakeups` throws by design on an unreadable store (state.ts),
|
|
160
|
+
// exactly the fault a wake alarm cannot fix — report it and leave the store write untouched,
|
|
161
|
+
// which is this sink's whole contract. The next mutation or boot re-runs the mirror.
|
|
162
|
+
log.error(`[schedule] wake alarm reconcile failed (alarms are stale until the next store change): ${String(e)}`);
|
|
107
163
|
}
|
|
108
164
|
finally {
|
|
165
|
+
running = false;
|
|
109
166
|
workDone();
|
|
110
167
|
}
|
|
111
168
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log.warn("[schedule] wake alarm skipped — forwarder URL not seen yet (it arrives with the first envelope)");
|
|
169
|
+
// Single-flight: a save arriving while the loop runs only marks it dirty, so a burst coalesces
|
|
170
|
+
// into one more pass instead of one concurrent loop each.
|
|
171
|
+
return (stateRoot) => {
|
|
172
|
+
dirty = true;
|
|
173
|
+
if (running)
|
|
118
174
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (alarms.length === 0)
|
|
122
|
-
return; // nothing future to mirror (deletion is lazy by design)
|
|
123
|
-
const token = Symbol("wake-alarm-sync");
|
|
124
|
-
latest = token;
|
|
125
|
-
void sync(url, { secret, alarms }, token);
|
|
175
|
+
running = true;
|
|
176
|
+
void reconcile(stateRoot);
|
|
126
177
|
};
|
|
127
178
|
}
|
|
128
|
-
/**
|
|
129
|
-
* One boot-time reconcile: pending wake-ups may exist while their alarms were lost (a deploy
|
|
130
|
-
* replaced the forwarder, a sink call failed) — re-mirror the current set once at start.
|
|
131
|
-
*/
|
|
132
|
-
export function reconcileWakeAlarms(stateRoot, sink) {
|
|
133
|
-
const pending = listWakeups(stateRoot);
|
|
134
|
-
if (pending.length > 0)
|
|
135
|
-
sink(stateRoot, pending);
|
|
136
|
-
}
|
|
@@ -26,7 +26,7 @@ export declare const MAX_PENDING_WAKEUPS = 20;
|
|
|
26
26
|
* wake into an active conversation fires in the first gap between the user's turns; this generous
|
|
27
27
|
* ceiling (~1h) only gives up on a pathologically stuck session (then logs, operator-visible). */
|
|
28
28
|
export declare const MAX_WAKE_ATTEMPTS = 120;
|
|
29
|
-
export declare function setWakeupsSink(sink: ((stateRoot: string
|
|
29
|
+
export declare function setWakeupsSink(sink: ((stateRoot: string) => void) | undefined): void;
|
|
30
30
|
/** The current pending wake-ups (`fastagent schedule list` uses this). */
|
|
31
31
|
export declare function listWakeups(stateRoot: string): Wakeup[];
|
|
32
32
|
export type AddWakeupResult = {
|
package/dist/schedule/wakeups.js
CHANGED
|
@@ -48,12 +48,15 @@ function isWakeup(e) {
|
|
|
48
48
|
(typeof w.cron === "string" && cronError(w.cron, typeof w.tz === "string" ? w.tz : undefined) === undefined)));
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* The wake-ALARM sink: notified after EVERY wakeups-store mutation
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
51
|
+
* The wake-ALARM sink: notified after EVERY wakeups-store mutation. A notification, not a delivery
|
|
52
|
+
* — it carries no set, because the sink reconciles by re-reading the store, and handing it a set
|
|
53
|
+
* captured here would go stale in its retries. The AgentCore deployment registers one
|
|
54
|
+
* (schedule/wake-alarm.ts) that mirrors pending wake-ups into one-shot EventBridge schedules — the
|
|
55
|
+
* external clock that makes `wake` reliable on a host with no resident process. Neutral seam: this
|
|
56
|
+
* module knows only "someone wants to observe changes"; a resident host registers nothing and
|
|
57
|
+
* behaves exactly as before. The sink is fire-and-forget and must own its errors. The guard below
|
|
58
|
+
* catches a SYNCHRONOUS throw so a broken alarm never corrupts a store write; an async sink returns
|
|
59
|
+
* before it does any work, so its rejections are ITS to terminate — nothing here can see them.
|
|
57
60
|
*/
|
|
58
61
|
let wakeupsSink;
|
|
59
62
|
export function setWakeupsSink(sink) {
|
|
@@ -81,7 +84,7 @@ function load(stateRoot) {
|
|
|
81
84
|
function save(stateRoot, wakeups) {
|
|
82
85
|
writeScheduleFile(scheduleFile(stateRoot, "wakeups"), wakeups);
|
|
83
86
|
try {
|
|
84
|
-
wakeupsSink?.(stateRoot
|
|
87
|
+
wakeupsSink?.(stateRoot);
|
|
85
88
|
}
|
|
86
89
|
catch (e) {
|
|
87
90
|
log.error(`[schedule] wake-alarm sink failed (store write is unaffected): ${String(e)}`);
|
package/dist/service.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Agent } from "./agent.ts";
|
|
2
|
+
import { type PrefixMount } from "./channels/serve.ts";
|
|
2
3
|
import { type LoadedLongConnectionChannel } from "./channels/discover.ts";
|
|
3
4
|
import type { SessionControl } from "./session.ts";
|
|
4
5
|
import type { ChannelHandler, Routes } from "./channel.ts";
|
|
5
|
-
import { type PrefixMount } from "./channels/serve.ts";
|
|
6
6
|
import type { LoadedSchedule } from "./schedule/schedule.ts";
|
|
7
7
|
export interface ServingSurface {
|
|
8
8
|
routes: Routes;
|
|
@@ -12,7 +12,6 @@ export interface ServingSurface {
|
|
|
12
12
|
/** Route-channel basenames; the tunnel registers only this subset. */
|
|
13
13
|
routeChannels: string[];
|
|
14
14
|
builtinInvoke: boolean;
|
|
15
|
-
/** Marks the built-in health route ready after every long-connection channel first connects. */
|
|
16
15
|
/** Flip health between 200 and 503. Two-way on purpose: a long connection that dies after coming
|
|
17
16
|
* up leaves the surface serving something it no longer has, and a load balancer should hear it. */
|
|
18
17
|
setReady(value: boolean): void;
|