@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
|
@@ -26,11 +26,36 @@ export function agentSessionManager(session, sessionId) {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The activation bridge over a live pi session — the ONE implementation, for both consumers.
|
|
31
|
+
*
|
|
32
|
+
* Serving (`agent-session-factory.ts`) and chat (`session-builder.ts`) had a copy each, identical
|
|
33
|
+
* but for the persistence line; the neighbouring `definitionResourceLoaderOptions` exists because
|
|
34
|
+
* that exact duplication drifted once before. The difference is a PARAMETER now: `onActivated` is
|
|
35
|
+
* what a served session uses to record the delta that carries the discovery into its next turn,
|
|
36
|
+
* and chat has nowhere to put one (pi's SessionContext has no active-tool set).
|
|
37
|
+
*
|
|
38
|
+
* Bind it to the SESSION, never to a tool call: the next call has to see what this one activated.
|
|
39
|
+
*/
|
|
40
|
+
export function sessionToolActivation(session, onActivated) {
|
|
41
|
+
return {
|
|
42
|
+
active: () => session.getActiveToolNames(),
|
|
43
|
+
registered: () => session.getAllTools().map((t) => ({ name: t.name, description: t.description ?? "" })),
|
|
44
|
+
activate(names) {
|
|
45
|
+
const current = session.getActiveToolNames();
|
|
46
|
+
const added = additiveActivation(session.getAllTools().map((t) => t.name), current, names);
|
|
47
|
+
if (added.length === 0)
|
|
48
|
+
return added;
|
|
49
|
+
session.setActiveToolsByName([...current, ...added]);
|
|
50
|
+
onActivated?.(added);
|
|
51
|
+
return added;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
29
55
|
export const turnContext = new AsyncLocalStorage();
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
export function additiveActivation(registered, current, names) {
|
|
56
|
+
/** dedupe → keep registered names only (pi's setters THROW on unknown) → exclude already-active →
|
|
57
|
+
* the names to actually add (empty = nothing to set). */
|
|
58
|
+
function additiveActivation(registered, current, names) {
|
|
34
59
|
const known = new Set(registered);
|
|
35
60
|
const active = new Set(current);
|
|
36
61
|
return [...new Set(names)].filter((name) => known.has(name) && !active.has(name));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { type ModuleLoadFailure } from "../../loader.ts";
|
|
4
5
|
import { type ReadonlySessionManager, type ToolActivation } from "./tool-context.ts";
|
|
@@ -34,14 +35,10 @@ export interface DefineToolOptions<I extends z.ZodType> {
|
|
|
34
35
|
executionMode?: "sequential" | "parallel";
|
|
35
36
|
execute: (input: z.infer<I>, ctx: ToolContext) => unknown | Promise<unknown>;
|
|
36
37
|
}
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* Naming the wider type is what lets `defineTool` stay context-free for authors while both families
|
|
42
|
-
* live in one array; every helper that only inspects or reorders tools is typed on THIS.
|
|
43
|
-
*/
|
|
44
|
-
export type MountedTool = AgentHarnessTool<ExecutionToolContext>;
|
|
38
|
+
/** AgentTool with Pi's optional per-call context; absent for sessionless CLI execution. */
|
|
39
|
+
export type MountedTool = Omit<AgentTool, "execute"> & {
|
|
40
|
+
execute(...args: [...Parameters<AgentTool["execute"]>, context?: ExtensionContext]): ReturnType<AgentTool["execute"]>;
|
|
41
|
+
};
|
|
45
42
|
/** An AgentTool with fastagent's deferral marker — the type for raw tools handed to fastagent
|
|
46
43
|
* (`config.tools`, L1/L2 `tools`): plain `AgentTool` has no `deferred`, so an object literal with the
|
|
47
44
|
* marker would fail excess-property checking against upstream's type. `defineTool` produces it. */
|
package/dist/engines/pi/tool.js
CHANGED
|
@@ -40,6 +40,7 @@ export function defineTool(options) {
|
|
|
40
40
|
const { $schema: _drop, ...parameters } = z.toJSONSchema(options.input);
|
|
41
41
|
const tool = {
|
|
42
42
|
name: options.name ?? "",
|
|
43
|
+
label: options.name ?? "",
|
|
43
44
|
description: options.description,
|
|
44
45
|
parameters,
|
|
45
46
|
...(options.deferred ? { deferred: true } : {}),
|
|
@@ -62,9 +63,9 @@ export function defineTool(options) {
|
|
|
62
63
|
const tools = store?.tools
|
|
63
64
|
? {
|
|
64
65
|
...store.tools,
|
|
65
|
-
activate:
|
|
66
|
+
activate: (names) => {
|
|
66
67
|
// biome-ignore lint/style/noNonNullAssertion: guarded by the ternary above
|
|
67
|
-
const activated =
|
|
68
|
+
const activated = store.tools.activate(names);
|
|
68
69
|
added.push(...activated);
|
|
69
70
|
return activated;
|
|
70
71
|
},
|
package/dist/env.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
3
|
import { log } from "./log.js";
|
|
5
4
|
import { SECRETS_DIRNAME, resolveSecretsDir } from "./paths.js";
|
|
6
5
|
/**
|
package/dist/feishu.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
|
|
2
2
|
export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, type FeishuChannelOptions, type FeishuWebSocketChannelOptions, type FeishuMessageEvent, type FeishuMessage, type FeishuRoute, type FeishuFailure, } from "./channels/feishu/feishu.ts";
|
|
3
|
+
export { feishuTransport, type FeishuTransport } from "./channels/feishu/shared-api.ts";
|
package/dist/feishu.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/feishu` — the canonical Feishu (open.feishu.cn) bot-channel surface. */
|
|
2
2
|
export { feishuChannel, feishuWebSocketChannel, defaultFeishuRoute, feishuEnvelope, } from "./channels/feishu/feishu.js";
|
|
3
|
+
export { feishuTransport } from "./channels/feishu/shared-api.js";
|
package/dist/lark.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
|
|
2
2
|
* Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
|
|
3
3
|
export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, type LarkChannelOptions, type LarkWebSocketChannelOptions, type LarkMessageEvent, type LarkMessage, type LarkRoute, type LarkFailure, } from "./channels/lark/lark.ts";
|
|
4
|
+
export { larkTransport, type FeishuTransport as LarkTransport } from "./channels/feishu/shared-api.ts";
|
package/dist/lark.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
/** `@fastagent-sh/fastagent/lark` — the Lark-international compatibility surface over the canonical
|
|
2
2
|
* Feishu engine. Feishu tenants use `@fastagent-sh/fastagent/feishu`. */
|
|
3
3
|
export { larkChannel, larkWebSocketChannel, defaultLarkRoute, larkEnvelope, } from "./channels/lark/lark.js";
|
|
4
|
+
export { larkTransport } from "./channels/feishu/shared-api.js";
|
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;
|
|
@@ -20,12 +63,13 @@ export interface ModuleLoadFailure {
|
|
|
20
63
|
/** The failure message (an import error carries {@link moduleLoadHint}). */
|
|
21
64
|
message: string;
|
|
22
65
|
}
|
|
66
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
67
|
+
export declare function reportModuleLoadFailures(failures: readonly ModuleLoadFailure[]): void;
|
|
23
68
|
/**
|
|
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.)
|
|
69
|
+
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
70
|
+
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
|
71
|
+
* report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
|
|
72
|
+
* the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
|
|
29
73
|
*/
|
|
30
74
|
export declare function loadModuleDir(subDir: string): Promise<{
|
|
31
75
|
modules: DiscoveredModule[];
|
package/dist/loader.js
CHANGED
|
@@ -9,40 +9,106 @@ 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
|
+
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
94
|
+
export function reportModuleLoadFailures(failures) {
|
|
95
|
+
for (const f of failures)
|
|
96
|
+
log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Import every module the directory declares ({@link moduleInventory}). A file that fails to IMPORT
|
|
100
|
+
* is collected into `failures` (with {@link moduleLoadHint}) rather than thrown, so the caller can
|
|
101
|
+
* report every bad file and apply domain policy; `loadTools`/`loadChannels` add validation failures
|
|
102
|
+
* the same way. Entries the inventory SKIPPED are already reported by it, for every consumer.
|
|
103
|
+
*/
|
|
104
|
+
export async function loadModuleDir(subDir) {
|
|
105
|
+
const entries = await moduleInventory(subDir);
|
|
36
106
|
const modules = [];
|
|
37
107
|
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}`;
|
|
108
|
+
for (const { name, label, file } of entries) {
|
|
43
109
|
try {
|
|
44
110
|
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
|
|
45
|
-
modules.push({ name
|
|
111
|
+
modules.push({ name, label, file, mod });
|
|
46
112
|
}
|
|
47
113
|
catch (error) {
|
|
48
114
|
failures.push({
|
package/dist/log.d.ts
CHANGED
|
@@ -8,22 +8,14 @@
|
|
|
8
8
|
* This is NOT the CLI's user-facing output (help text, command results): that is the program talking to
|
|
9
9
|
* its operator and stays on plain `console`. Everything here is operational logging to stderr.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
debug(msg: string): void;
|
|
14
|
-
info(msg: string): void;
|
|
15
|
-
warn(msg: string): void;
|
|
16
|
-
error(msg: string): void;
|
|
17
|
-
}
|
|
18
|
-
/** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
|
|
19
|
-
export declare function createLogger(opts: {
|
|
20
|
-
level: LogLevel;
|
|
21
|
-
sink?: (line: string) => void;
|
|
22
|
-
}): Logger;
|
|
23
|
-
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` override, if present, wins and is not changed. */
|
|
11
|
+
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
12
|
+
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` wins over it, whenever a line is emitted. */
|
|
24
13
|
export declare function setLogLevel(level: LogLevel): void;
|
|
25
14
|
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
export declare const log: {
|
|
16
|
+
debug: (msg: string) => void;
|
|
17
|
+
info: (msg: string) => void;
|
|
18
|
+
warn: (msg: string) => void;
|
|
19
|
+
error: (msg: string) => void;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
package/dist/log.js
CHANGED
|
@@ -9,47 +9,42 @@
|
|
|
9
9
|
* its operator and stays on plain `console`. Everything here is operational logging to stderr.
|
|
10
10
|
*/
|
|
11
11
|
const ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
12
|
-
const isLevel = (s) => s
|
|
12
|
+
const isLevel = (s) => Object.hasOwn(ORDER, s);
|
|
13
13
|
const format = (level, msg) => `${level.toUpperCase().padEnd(5)} ${msg}`;
|
|
14
|
-
/** An embedded logger over an explicit sink — used in tests to assert level gating without the singleton. */
|
|
15
|
-
export function createLogger(opts) {
|
|
16
|
-
const sink = opts.sink ?? ((line) => console.error(line));
|
|
17
|
-
const make = (level) => (msg) => {
|
|
18
|
-
if (ORDER[level] >= ORDER[opts.level])
|
|
19
|
-
sink(format(level, msg));
|
|
20
|
-
};
|
|
21
|
-
return { debug: make("debug"), info: make("info"), warn: make("warn"), error: make("error") };
|
|
22
|
-
}
|
|
23
14
|
/**
|
|
24
|
-
* `FASTAGENT_LOG_LEVEL`
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
15
|
+
* `FASTAGENT_LOG_LEVEL` resolved PER EMIT, not at import: this module is imported transitively by every
|
|
16
|
+
* command module, so an import-time read runs before any command reaches `loadDotEnv` and could never
|
|
17
|
+
* see the agent's `.secrets/.env` — a key that file could not carry at all.
|
|
18
|
+
*
|
|
19
|
+
* Three states: a valid value wins over the posture; an invalid value warns when it differs from the
|
|
20
|
+
* last warned value and is treated as absent, so a typo (meant to make logs louder) can never silently pin the
|
|
21
|
+
* level nor kill the posture default; absent leaves the posture. Empty is absent, matching
|
|
22
|
+
* `resolveOverridePath` — `KEY=` is how a `.env` parks a key it does not want set, not a typo to warn
|
|
23
|
+
* about. The warning is raw `console.error` because it is the logger reporting on its own gate, but
|
|
24
|
+
* reuses `format` for the same shape.
|
|
28
25
|
*/
|
|
29
|
-
|
|
26
|
+
let posture = "info";
|
|
27
|
+
let warnedFor;
|
|
28
|
+
function effectiveLevel() {
|
|
30
29
|
const raw = process.env.FASTAGENT_LOG_LEVEL;
|
|
31
|
-
if (raw
|
|
32
|
-
return
|
|
30
|
+
if (!raw)
|
|
31
|
+
return posture;
|
|
33
32
|
const value = raw.toLowerCase();
|
|
34
33
|
if (isLevel(value))
|
|
35
34
|
return value;
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
if (warnedFor !== raw) {
|
|
36
|
+
warnedFor = raw;
|
|
37
|
+
console.error(format("warn", `[fastagent] unknown FASTAGENT_LOG_LEVEL "${raw}"; using the posture default`));
|
|
38
|
+
}
|
|
39
|
+
return posture;
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
let currentLevel = override ?? "info";
|
|
41
|
-
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` override, if present, wins and is not changed. */
|
|
41
|
+
/** Set the posture default. A valid `FASTAGENT_LOG_LEVEL` wins over it, whenever a line is emitted. */
|
|
42
42
|
export function setLogLevel(level) {
|
|
43
|
-
|
|
44
|
-
currentLevel = level;
|
|
43
|
+
posture = level;
|
|
45
44
|
}
|
|
46
45
|
const emit = (level) => (msg) => {
|
|
47
|
-
if (ORDER[level] >= ORDER[
|
|
46
|
+
if (ORDER[level] >= ORDER[effectiveLevel()])
|
|
48
47
|
console.error(format(level, msg));
|
|
49
48
|
};
|
|
49
|
+
/** The process logger. Runtime code imports this and calls `log.info(...)` etc. */
|
|
50
50
|
export const log = { debug: emit("debug"), info: emit("info"), warn: emit("warn"), error: emit("error") };
|
|
51
|
-
/** A module the loader skipped, said once, the same way for tools, channels and schedules. */
|
|
52
|
-
export function reportModuleLoadFailures(failures) {
|
|
53
|
-
for (const f of failures)
|
|
54
|
-
log.warn(`[fastagent] ${f.label} failed to load, skipping it — ${f.message}`);
|
|
55
|
-
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -62,9 +62,9 @@ export declare function agentsAt(dir: string): string[];
|
|
|
62
62
|
* reader to a refusal whose own advice points back here.
|
|
63
63
|
*/
|
|
64
64
|
export declare function workspaceHint({ agentDir, workspace }: ResolvedPlacement, env?: NodeJS.ProcessEnv): string | undefined;
|
|
65
|
-
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
66
|
+
* or the throw. Login and standalone channel senders also operate outside configured agents, so
|
|
67
|
+
* they need absence as a value. Filesystem errors still propagate. */
|
|
68
68
|
export declare function findAgentDir(dir: string): string | undefined;
|
|
69
69
|
/**
|
|
70
70
|
* The agent whose DEFINITION contains `dir` — scaffolding there would make the new agent part of the
|
|
@@ -104,6 +104,10 @@ export declare function displayPath(cwd: string, dir: string): string | undefine
|
|
|
104
104
|
/** Does a path exist? Plain fs, no placement in it — it lives here because `paths.ts` is where the
|
|
105
105
|
* neutral path helpers are, and the scaffolder is not a utility home for the CLI and deploy. */
|
|
106
106
|
export declare function exists(p: string): Promise<boolean>;
|
|
107
|
+
/** The file's text, or undefined when there is no file. ONLY absence reads as absence: a file that
|
|
108
|
+
* exists but cannot be read (EACCES, a directory in its place) throws, because a decision made on
|
|
109
|
+
* "not there" — regenerate it, skip its gate — is the wrong one for a file that is there. */
|
|
110
|
+
export declare function readTextIfExists(p: string): Promise<string | undefined>;
|
|
107
111
|
/**
|
|
108
112
|
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
109
113
|
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
|
@@ -136,6 +140,25 @@ export declare function resolveStateRoot(dir: string, env?: NodeJS.ProcessEnv):
|
|
|
136
140
|
* auth.json but cannot move the file it is read from (env.ts dotEnvPath).
|
|
137
141
|
*/
|
|
138
142
|
export declare function resolveSecretsDir(dir: string, env?: NodeJS.ProcessEnv): string;
|
|
143
|
+
/** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
|
|
144
|
+
export declare const SECRET_FILE_MODE = 384;
|
|
145
|
+
/**
|
|
146
|
+
* Create the secrets directory with the mode its contents require — and REPAIR it when it already
|
|
147
|
+
* exists, which is the case that matters.
|
|
148
|
+
*
|
|
149
|
+
* The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
|
|
150
|
+
* below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
|
|
151
|
+
* writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
|
|
152
|
+
* later, more careful caller silently inherits the first one's answer. Four callers create this
|
|
153
|
+
* directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
|
|
154
|
+
* is init → add → login, so the careful one is LAST: the rule has to live where all of them can
|
|
155
|
+
* reach it, and it has to chmod rather than trust the create.
|
|
156
|
+
*
|
|
157
|
+
* A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
|
|
158
|
+
* reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
|
|
159
|
+
* directory they pointed it at, and says nothing about the way out.
|
|
160
|
+
*/
|
|
161
|
+
export declare function ensureSecretsDir(dir: string): Promise<void>;
|
|
139
162
|
/**
|
|
140
163
|
* Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
|
|
141
164
|
* 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, readFile, 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
|
/**
|
|
@@ -160,9 +159,9 @@ export function workspaceHint({ agentDir, workspace }, env = process.env) {
|
|
|
160
159
|
return undefined;
|
|
161
160
|
return `${parent} looks like a project — point fastagent at it (\`..\`) to have the agent work ON it`;
|
|
162
161
|
}
|
|
163
|
-
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
164
|
-
*
|
|
165
|
-
*
|
|
162
|
+
/** The agent dir for `dir`, or undefined when there is none — {@link findPlacement} without the pair
|
|
163
|
+
* or the throw. Login and standalone channel senders also operate outside configured agents, so
|
|
164
|
+
* they need absence as a value. Filesystem errors still propagate. */
|
|
166
165
|
export function findAgentDir(dir) {
|
|
167
166
|
return findPlacement(dir)?.agentDir;
|
|
168
167
|
}
|
|
@@ -268,6 +267,19 @@ export function displayPath(cwd, dir) {
|
|
|
268
267
|
export async function exists(p) {
|
|
269
268
|
return access(p).then(() => true, () => false);
|
|
270
269
|
}
|
|
270
|
+
/** The file's text, or undefined when there is no file. ONLY absence reads as absence: a file that
|
|
271
|
+
* exists but cannot be read (EACCES, a directory in its place) throws, because a decision made on
|
|
272
|
+
* "not there" — regenerate it, skip its gate — is the wrong one for a file that is there. */
|
|
273
|
+
export async function readTextIfExists(p) {
|
|
274
|
+
try {
|
|
275
|
+
return await readFile(p, "utf8");
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code === "ENOENT")
|
|
279
|
+
return undefined;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
271
283
|
/**
|
|
272
284
|
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
273
285
|
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
|
@@ -309,6 +321,32 @@ export function resolveStateRoot(dir, env = process.env) {
|
|
|
309
321
|
export function resolveSecretsDir(dir, env = process.env) {
|
|
310
322
|
return resolveOverridePath(env.FASTAGENT_SECRETS_DIR) ?? join(resolve(dir), SECRETS_DIRNAME);
|
|
311
323
|
}
|
|
324
|
+
/** What a file under {@link resolveSecretsDir} is written with (auth.json, .env). */
|
|
325
|
+
export const SECRET_FILE_MODE = 0o600;
|
|
326
|
+
/** What the secrets directory itself is created with. */
|
|
327
|
+
const SECRETS_DIR_MODE = 0o700;
|
|
328
|
+
/**
|
|
329
|
+
* Create the secrets directory with the mode its contents require — and REPAIR it when it already
|
|
330
|
+
* exists, which is the case that matters.
|
|
331
|
+
*
|
|
332
|
+
* The DIRECTORY is the boundary that actually protects a credential: without its `x` bit nothing
|
|
333
|
+
* below it is reachable, whatever a file's own mode says. And it is decided ONCE, by whichever
|
|
334
|
+
* writer gets there first — `mkdir`'s `mode` is ignored for a directory that already exists, so a
|
|
335
|
+
* later, more careful caller silently inherits the first one's answer. Four callers create this
|
|
336
|
+
* directory (`init`, `add <channel>`, the credential store, the deploy seed) and the ordinary order
|
|
337
|
+
* is init → add → login, so the careful one is LAST: the rule has to live where all of them can
|
|
338
|
+
* reach it, and it has to chmod rather than trust the create.
|
|
339
|
+
*
|
|
340
|
+
* A chmod the caller never asked for owes them its reason: the raw `EPERM ... chmod '/shared/creds'`
|
|
341
|
+
* reads as a bug in whatever they WERE doing (storing a credential), not as fastagent tightening a
|
|
342
|
+
* directory they pointed it at, and says nothing about the way out.
|
|
343
|
+
*/
|
|
344
|
+
export async function ensureSecretsDir(dir) {
|
|
345
|
+
await mkdir(dir, { recursive: true, mode: SECRETS_DIR_MODE });
|
|
346
|
+
await chmod(dir, SECRETS_DIR_MODE).catch((e) => {
|
|
347
|
+
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 });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
312
350
|
/**
|
|
313
351
|
* Guard that `<agentDir>/<name>` resolves INSIDE the agent dir — a symlink that escapes (or an
|
|
314
352
|
* absolute target) is rejected, so discovery/scaffolding never reaches out of the definition directory.
|
|
@@ -57,8 +57,13 @@ export declare function channelExists(dir: string, kind: ChannelKind): Promise<b
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function scaffoldChannel(dir: string, kind: ChannelKind, options?: {
|
|
59
59
|
ingress?: FeishuSubscriptionMode;
|
|
60
|
-
groupBehavior?: GroupBehavior;
|
|
61
60
|
}): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* The bundle's companion tools (every `.ts` beside `channel.ts` → `tools/<name>`). Unlike the channel
|
|
63
|
+
* file they are the package's, not authored glue, so they are written on EVERY add: re-running
|
|
64
|
+
* `add <kind>` is how an upgraded tool reaches an existing agent.
|
|
65
|
+
*/
|
|
66
|
+
export declare function scaffoldCompanionTools(dir: string, kind: ChannelKind): Promise<string[]>;
|
|
62
67
|
/**
|
|
63
68
|
* Verify the AGENT DIR is ready to host a channel: an ESM package.json that declares
|
|
64
69
|
* `@fastagent-sh/fastagent` (the channel file imports it). `add` checks and guides, never bootstraps — that
|