@fastagent-sh/fastagent 0.15.0 → 0.16.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 +14 -4
- package/dist/bind.d.ts +34 -0
- package/dist/bind.js +74 -0
- package/dist/channels/agentcore-limits.d.ts +7 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-state.d.ts +83 -0
- package/dist/channels/agentcore-state.js +266 -0
- package/dist/channels/agentcore.d.ts +98 -0
- package/dist/channels/agentcore.js +260 -0
- package/dist/channels/busy.d.ts +23 -0
- package/dist/channels/busy.js +53 -0
- package/dist/channels/context-buffer.d.ts +5 -0
- package/dist/channels/context-buffer.js +5 -0
- package/dist/channels/feishu/context-buffer.d.ts +13 -8
- package/dist/channels/feishu/context-buffer.js +60 -6
- package/dist/channels/feishu/feishu-api.js +4 -1
- package/dist/channels/feishu/feishu.d.ts +0 -12
- package/dist/channels/feishu/feishu.js +143 -54
- package/dist/channels/feishu/invoke-turn.js +35 -18
- package/dist/channels/feishu/model.d.ts +0 -1
- package/dist/channels/feishu/normalize.js +0 -1
- package/dist/channels/feishu/parse.d.ts +21 -7
- package/dist/channels/feishu/parse.js +24 -7
- package/dist/channels/feishu/preview.js +3 -2
- package/dist/channels/feishu/scaffold/channel.ts +9 -8
- package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
- package/dist/channels/feishu/setup-mode.d.ts +30 -1
- package/dist/channels/feishu/setup-mode.js +27 -1
- package/dist/channels/github/github.js +8 -1
- package/dist/channels/http.js +1 -1
- package/dist/channels/lark/scaffold/channel.ts +9 -8
- package/dist/channels/lark/scaffold/lark-send.ts +6 -4
- package/dist/channels/preview-kit.d.ts +7 -1
- package/dist/channels/preview-kit.js +3 -2
- package/dist/channels/slack/parse.d.ts +16 -1
- package/dist/channels/slack/parse.js +46 -3
- package/dist/channels/slack/preview.d.ts +1 -2
- package/dist/channels/slack/preview.js +68 -24
- package/dist/channels/slack/scaffold/channel.ts +5 -5
- package/dist/channels/slack/slack-api.d.ts +3 -23
- package/dist/channels/slack/slack-api.js +6 -22
- package/dist/channels/slack/slack.d.ts +13 -20
- package/dist/channels/slack/slack.js +95 -50
- package/dist/channels/state.d.ts +11 -4
- package/dist/channels/state.js +19 -12
- package/dist/channels/tasks.d.ts +0 -6
- package/dist/channels/tasks.js +16 -1
- package/dist/channels/telegram/parse.d.ts +0 -7
- package/dist/channels/telegram/parse.js +4 -2
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
- package/dist/channels/telegram/telegram.js +1 -1
- package/dist/channels/text.d.ts +14 -0
- package/dist/channels/text.js +14 -0
- package/dist/channels/thread-participants.d.ts +21 -0
- package/dist/channels/thread-participants.js +132 -0
- package/dist/channels/turn-queue.js +7 -0
- package/dist/cli/add-feishu.d.ts +7 -4
- package/dist/cli/add-feishu.js +57 -37
- package/dist/cli/add-slack.d.ts +2 -1
- package/dist/cli/add-slack.js +6 -11
- package/dist/cli/commands/add.js +50 -51
- package/dist/cli/commands/attach.js +8 -4
- package/dist/cli/commands/chat.js +8 -8
- package/dist/cli/commands/deploy.d.ts +14 -1
- package/dist/cli/commands/deploy.js +330 -78
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +38 -25
- package/dist/cli/commands/fire.js +15 -16
- package/dist/cli/commands/info.js +36 -29
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.js +65 -53
- package/dist/cli/commands/invoke.js +9 -6
- package/dist/cli/commands/login.js +35 -21
- package/dist/cli/commands/schedule.js +6 -8
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +101 -37
- package/dist/cli/commands/tool.js +30 -18
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +24 -0
- package/dist/cli/program.js +66 -36
- package/dist/cli/serve.d.ts +52 -8
- package/dist/cli/serve.js +133 -32
- package/dist/cli/shared.d.ts +21 -2
- package/dist/cli/shared.js +44 -18
- package/dist/deploy/agentcore/plan.d.ts +117 -0
- package/dist/deploy/agentcore/plan.js +721 -0
- package/dist/deploy/agentcore/run.d.ts +73 -0
- package/dist/deploy/agentcore/run.js +412 -0
- package/dist/deploy/agentcore/zip.d.ts +17 -0
- package/dist/deploy/agentcore/zip.js +68 -0
- package/dist/deploy/container.d.ts +26 -25
- package/dist/deploy/container.js +93 -89
- package/dist/deploy/docker/plan.d.ts +1 -1
- package/dist/deploy/docker/plan.js +12 -17
- package/dist/deploy/fly/plan.d.ts +2 -0
- package/dist/deploy/fly/plan.js +27 -19
- package/dist/deploy/fly/run.d.ts +12 -1
- package/dist/deploy/fly/run.js +36 -2
- package/dist/deploy/preflight.d.ts +11 -5
- package/dist/deploy/preflight.js +235 -65
- package/dist/deploy/railway/plan.d.ts +7 -0
- package/dist/deploy/railway/plan.js +41 -16
- package/dist/deploy/railway/run.d.ts +8 -1
- package/dist/deploy/railway/run.js +7 -2
- package/dist/deploy/runner.d.ts +5 -2
- package/dist/deploy/runner.js +9 -3
- package/dist/dev-supervisor.d.ts +11 -8
- package/dist/dev-supervisor.js +53 -51
- package/dist/engines/pi/auth.d.ts +8 -7
- package/dist/engines/pi/auth.js +12 -10
- package/dist/engines/pi/channel.d.ts +1 -1
- package/dist/engines/pi/channel.js +5 -5
- package/dist/engines/pi/chat.js +2 -2
- package/dist/engines/pi/config.d.ts +12 -48
- package/dist/engines/pi/config.js +29 -110
- package/dist/engines/pi/create.d.ts +32 -24
- package/dist/engines/pi/create.js +47 -23
- package/dist/engines/pi/definition.d.ts +7 -26
- package/dist/engines/pi/definition.js +8 -54
- package/dist/engines/pi/harness.d.ts +19 -5
- package/dist/engines/pi/harness.js +3 -5
- package/dist/engines/pi/login.d.ts +1 -1
- package/dist/engines/pi/models.d.ts +3 -3
- package/dist/engines/pi/models.js +1 -1
- package/dist/engines/pi/{workspace.d.ts → open.d.ts} +31 -25
- package/dist/engines/pi/{workspace.js → open.js} +27 -29
- package/dist/engines/pi/read-image.d.ts +4 -0
- package/dist/engines/pi/read-image.js +62 -0
- package/dist/engines/pi/search-tools.d.ts +6 -4
- package/dist/engines/pi/search-tools.js +3 -1
- package/dist/engines/pi/session-builder.d.ts +2 -2
- package/dist/engines/pi/session-builder.js +18 -13
- package/dist/engines/pi/tool.d.ts +13 -5
- package/dist/engines/pi/tool.js +4 -0
- package/dist/engines/pi/wake-tool.d.ts +3 -3
- package/dist/env.d.ts +16 -4
- package/dist/env.js +43 -5
- package/dist/host/node.d.ts +4 -2
- package/dist/host/node.js +2 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.js +3 -3
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/paths.d.ts +138 -0
- package/dist/paths.js +326 -0
- package/dist/pi.d.ts +2 -2
- package/dist/pi.js +2 -2
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.js +2 -2
- package/dist/scaffold/add-channel.d.ts +7 -3
- package/dist/scaffold/add-channel.js +55 -29
- package/dist/scaffold/init.d.ts +32 -41
- package/dist/scaffold/init.js +161 -185
- package/dist/scaffold/templates/env.example +15 -6
- package/dist/scaffold/templates/fastagent.config.mjs +1 -1
- package/dist/scaffold/templates/gitignore +14 -6
- package/dist/scaffold/templates/persona.md +4 -2
- package/dist/scaffold/templates/secrets.gitignore +5 -0
- package/dist/scaffold/templates.d.ts +1 -7
- package/dist/scaffold/templates.js +3 -25
- package/dist/scaffold/vendor-skill.d.ts +2 -2
- package/dist/scaffold/vendor-skill.js +13 -13
- package/dist/schedule/discover.js +4 -4
- package/dist/schedule/scheduler.d.ts +40 -1
- package/dist/schedule/scheduler.js +89 -56
- package/dist/schedule/state.js +1 -1
- package/dist/schedule/wake-alarm.d.ts +47 -0
- package/dist/schedule/wake-alarm.js +136 -0
- package/dist/schedule/wakeups.d.ts +1 -0
- package/dist/schedule/wakeups.js +18 -0
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.js +7 -7
- package/package.json +7 -4
- package/dist/channels/feishu/owned-threads.d.ts +0 -7
- package/dist/channels/feishu/owned-threads.js +0 -47
- package/dist/channels/slack/owned-threads.d.ts +0 -6
- package/dist/channels/slack/owned-threads.js +0 -43
- package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
- package/dist/scaffold/templates/gitignore.kit +0 -2
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.js +0 -45
|
@@ -11,15 +11,19 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
11
11
|
* (resolveModel, resolveModelSpec). One concern: everything about fastagent.config.ts.
|
|
12
12
|
*
|
|
13
13
|
* Red line: config describes deployment/runtime choices, never authored identity or expertise (those
|
|
14
|
-
* live in persona.md + skills, with AGENTS.md as project context).
|
|
15
|
-
*
|
|
14
|
+
* live in persona.md + skills, with AGENTS.md as project context). It has no say in PLACEMENT either —
|
|
15
|
+
* that rule lives in the neutral src/paths.ts — but its FILE is what marks a directory as an agent, so a
|
|
16
|
+
* served agent always has one (its contents may be `export default {}`; a model can still come from
|
|
17
|
+
* --model / FASTAGENT_MODEL). The loader below stays tolerant of a missing file because it is a loader:
|
|
18
|
+
* callers that have not resolved a placement (deploy inspection, tests) may point it anywhere.
|
|
16
19
|
*/
|
|
17
|
-
import { existsSync,
|
|
18
|
-
import {
|
|
19
|
-
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
20
|
+
import { existsSync, statSync } from "node:fs";
|
|
21
|
+
import { basename, join } from "node:path";
|
|
20
22
|
import { pathToFileURL } from "node:url";
|
|
21
23
|
import { THINKING_LEVELS } from "./harness.js";
|
|
24
|
+
import { isBindAddress } from "../../bind.js";
|
|
22
25
|
import { moduleLoadHint } from "../../loader.js";
|
|
26
|
+
import { AGENT_CONFIG_NAMES, resolveOverridePath, resolveSecretsDir } from "../../paths.js";
|
|
23
27
|
/** Identity function for typing and IDE completion (vite/next-style). */
|
|
24
28
|
export function defineConfig(config) {
|
|
25
29
|
return config;
|
|
@@ -41,13 +45,9 @@ function validateStringList(value, key, shape, desc, path) {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
|
-
/**
|
|
45
|
-
* loader (below) and `scaffoldWorkspace`'s already-a-workspace refusal both read this, so "is there a
|
|
46
|
-
* config?" can't diverge between them when the set changes. */
|
|
47
|
-
export const WORKSPACE_CONFIG_NAMES = ["fastagent.config.ts", "fastagent.config.js", "fastagent.config.mjs"];
|
|
48
|
-
/** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = zero-config; a wrong-shape file throws. */
|
|
48
|
+
/** Load `<dir>/fastagent.config.ts|.js|.mjs`. No file = defaults; a wrong-shape file throws. */
|
|
49
49
|
export async function loadConfig(dir) {
|
|
50
|
-
const found =
|
|
50
|
+
const found = AGENT_CONFIG_NAMES.map((name) => join(dir, name)).filter((path) => existsSync(path));
|
|
51
51
|
if (found.length === 0)
|
|
52
52
|
return { config: {} };
|
|
53
53
|
if (found.length > 1) {
|
|
@@ -75,17 +75,16 @@ export async function loadConfig(dir) {
|
|
|
75
75
|
}
|
|
76
76
|
const c = config;
|
|
77
77
|
// Unknown keys throw: defineConfig only type-protects .ts authors; a typo in a .js/.mjs config
|
|
78
|
-
// (`modle:`) must not silently degrade to
|
|
78
|
+
// (`modle:`) must not silently degrade to defaults.
|
|
79
79
|
for (const key of Object.keys(c)) {
|
|
80
80
|
if (key !== "model" &&
|
|
81
81
|
key !== "thinkingLevel" &&
|
|
82
|
-
key !== "agentDir" &&
|
|
83
82
|
key !== "tools" &&
|
|
84
83
|
key !== "http" &&
|
|
85
84
|
key !== "deploy" &&
|
|
86
85
|
key !== "selfSchedule" &&
|
|
87
86
|
key !== "sessionControl") {
|
|
88
|
-
throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel,
|
|
87
|
+
throw new Error(`${path}: unknown key "${key}" (valid keys: model, thinkingLevel, tools, http, deploy, selfSchedule, sessionControl)`);
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
if (c.model !== undefined && typeof c.model !== "string") {
|
|
@@ -97,47 +96,6 @@ export async function loadConfig(dir) {
|
|
|
97
96
|
if (c.thinkingLevel !== undefined && !THINKING_LEVELS.has(c.thinkingLevel)) {
|
|
98
97
|
throw new Error(`${path}: "thinkingLevel" must be one of ${[...THINKING_LEVELS].join(", ")}`);
|
|
99
98
|
}
|
|
100
|
-
if (c.agentDir !== undefined && typeof c.agentDir !== "string") {
|
|
101
|
-
throw new Error(`${path}: "agentDir" must be a string (a subdirectory relative to the config file)`);
|
|
102
|
-
}
|
|
103
|
-
if (typeof c.agentDir === "string") {
|
|
104
|
-
// Enforce the documented "subdirectory of the config dir" contract: an escaping agentDir (e.g.
|
|
105
|
-
// "../shared") would still resolve for tool/channel/persona discovery, but `dev`'s chokidar only
|
|
106
|
-
// watches the config dir subtree — edits outside it would silently never trigger a restart. Reject
|
|
107
|
-
// it here (fail visibly) rather than let hot-reload break without a signal.
|
|
108
|
-
const rel = relative(dir, resolve(dir, c.agentDir));
|
|
109
|
-
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
110
|
-
throw new Error(`${path}: "agentDir" ("${c.agentDir}") must be a subdirectory of the config directory, not escape it`);
|
|
111
|
-
}
|
|
112
|
-
// An explicitly declared agentDir that doesn't exist is a typo until proven otherwise ("./agnet"):
|
|
113
|
-
// without this check every opener would assemble an EMPTY agent (no persona, no skills, no tools)
|
|
114
|
-
// with zero errors — the worst silent failure this config can produce. Deliberately NOT auto-created:
|
|
115
|
-
// config load is read-only (no implicit operations), and a mkdir would turn the typo into a served
|
|
116
|
-
// empty agent plus a junk directory.
|
|
117
|
-
// lstat, not stat: a symlink would pass the literal containment check above while its TARGET lives
|
|
118
|
-
// outside the config dir — exactly what that check exists to prevent (dev's watch would silently
|
|
119
|
-
// never see edits). Same rule as init's parent preflight: reject, don't follow.
|
|
120
|
-
const agentDirAbs = resolve(dir, c.agentDir);
|
|
121
|
-
const st = lstatSync(agentDirAbs, { throwIfNoEntry: false });
|
|
122
|
-
if (!st) {
|
|
123
|
-
throw new Error(`${path}: "agentDir" ("${c.agentDir}") does not exist — create it, or fix the path`);
|
|
124
|
-
}
|
|
125
|
-
if (st.isSymbolicLink()) {
|
|
126
|
-
// Separate message: to its user a symlink LOOKS like a working directory — name the reason and the fix.
|
|
127
|
-
throw new Error(`${path}: "agentDir" ("${c.agentDir}") is a symlink — not allowed (its target can live outside the ` +
|
|
128
|
-
`config directory, where dev's watch would never see edits); use a real directory, or point agentDir at the target's real path`);
|
|
129
|
-
}
|
|
130
|
-
if (!st.isDirectory()) {
|
|
131
|
-
throw new Error(`${path}: "agentDir" ("${c.agentDir}") is not a directory`);
|
|
132
|
-
}
|
|
133
|
-
// The leaf lstat can't see a symlinked INTERMEDIATE segment (agentDir "./a/b" with `a` → outside):
|
|
134
|
-
// realpath equality covers every segment under the config dir in one comparison. dir itself is
|
|
135
|
-
// realpath'd on both sides, so a symlinked config-dir path (macOS /tmp) stays legal.
|
|
136
|
-
if (realpathSync(agentDirAbs) !== resolve(realpathSync(dir), relative(dir, agentDirAbs))) {
|
|
137
|
-
throw new Error(`${path}: "agentDir" ("${c.agentDir}") resolves through a symlink — not allowed (the target can ` +
|
|
138
|
-
`live outside the config directory, where dev's watch would never see edits); use the real path`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
99
|
if (c.selfSchedule !== undefined && typeof c.selfSchedule !== "boolean") {
|
|
142
100
|
throw new Error(`${path}: "selfSchedule" must be a boolean`);
|
|
143
101
|
}
|
|
@@ -159,13 +117,18 @@ export async function loadConfig(dir) {
|
|
|
159
117
|
throw new Error(`${path}: "http" must be an object`);
|
|
160
118
|
}
|
|
161
119
|
for (const key of Object.keys(c.http ?? {})) {
|
|
162
|
-
if (key !== "port") {
|
|
163
|
-
throw new Error(`${path}: unknown key "http.${key}" (valid keys: port)`);
|
|
120
|
+
if (key !== "port" && key !== "host") {
|
|
121
|
+
throw new Error(`${path}: unknown key "http.${key}" (valid keys: port, host)`);
|
|
164
122
|
}
|
|
165
123
|
}
|
|
166
124
|
if (c.http?.port !== undefined && (typeof c.http.port !== "number" || !isValidPort(c.http.port))) {
|
|
167
125
|
throw new Error(`${path}: "http.port" must be an integer 0-65535`);
|
|
168
126
|
}
|
|
127
|
+
// Validated as strictly as http.port: an unbindable string ("banana") must fail HERE, not surface
|
|
128
|
+
// later as a topology diagnostic about "the interface you bound".
|
|
129
|
+
if (c.http?.host !== undefined && (typeof c.http.host !== "string" || !isBindAddress(c.http.host))) {
|
|
130
|
+
throw new Error(`${path}: "http.host" must be an IP address or "localhost" (e.g. "127.0.0.1", "0.0.0.0")`);
|
|
131
|
+
}
|
|
169
132
|
if (c.deploy !== undefined && (typeof c.deploy !== "object" || c.deploy === null)) {
|
|
170
133
|
throw new Error(`${path}: "deploy" must be an object`);
|
|
171
134
|
}
|
|
@@ -181,15 +144,6 @@ export async function loadConfig(dir) {
|
|
|
181
144
|
validateStringList(c.deploy?.apt, "deploy.apt", /^[a-z0-9][a-z0-9.+-]*$/, "a Debian package name", path);
|
|
182
145
|
return { config: c, path };
|
|
183
146
|
}
|
|
184
|
-
/**
|
|
185
|
-
* The agent-definition dir from config: `config.agentDir` resolved against `dir`, or `dir` itself when
|
|
186
|
-
* unset (flat). The ONE place this is computed — every opener (`dev`/`start`/`info`/`tool`/`deploy`/`chat`)
|
|
187
|
-
* calls it, so the "relative to the config dir, default `.`" rule can never diverge. loadConfig has
|
|
188
|
-
* already validated that agentDir stays under `dir`.
|
|
189
|
-
*/
|
|
190
|
-
export function resolveAgentDir(dir, config) {
|
|
191
|
-
return resolve(dir, config.agentDir ?? ".");
|
|
192
|
-
}
|
|
193
147
|
/** The provider prefix of a "provider/modelId" spec. A spec without "/" returns whole — downstream
|
|
194
148
|
* lookups then miss visibly (an unknown-provider error / a login-required hint), never a mangled id
|
|
195
149
|
* (`slice(0, indexOf("/"))` silently drops the last char when "/" is absent). */
|
|
@@ -223,7 +177,7 @@ export function listModels(models) {
|
|
|
223
177
|
/**
|
|
224
178
|
* Rewrite the `model` in a config file's SOURCE TEXT to `spec`, for the first-run picker's write-back.
|
|
225
179
|
* Handles the scaffold's commented placeholder (`// model: "…"`) and an existing `model:` line; returns
|
|
226
|
-
* null when neither is present (
|
|
180
|
+
* null when neither is present (no config file, or a hand-shaped one) so the caller falls back to a
|
|
227
181
|
* printed hint instead of guessing where to insert. Text-level (not AST) on purpose — it only ever
|
|
228
182
|
* touches a line it recognizes, never reformats the author's file.
|
|
229
183
|
*/
|
|
@@ -248,19 +202,6 @@ export function rewriteConfigModel(src, spec) {
|
|
|
248
202
|
export function resolveModelSpec(flag, config, env = process.env) {
|
|
249
203
|
return flag ?? env.FASTAGENT_MODEL ?? config.model;
|
|
250
204
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Resolve a user-supplied path override (a CLI flag or an env var) to an absolute path, expanding a
|
|
253
|
-
* leading `~`/`~/` to the home dir FIRST. Path-valued config from `.env` (or any non-shell source)
|
|
254
|
-
* never gets the shell's `~` expansion, so a bare `resolve("~/x")` would silently create a literal `~`
|
|
255
|
-
* directory — a fail-silently footgun for a secret/state path. Expanding here makes `~` mean home
|
|
256
|
-
* everywhere these knobs are read.
|
|
257
|
-
*/
|
|
258
|
-
function resolveOverridePath(raw) {
|
|
259
|
-
if (!raw)
|
|
260
|
-
return undefined;
|
|
261
|
-
const expanded = raw === "~" ? homedir() : raw.startsWith("~/") ? join(homedir(), raw.slice(2)) : raw;
|
|
262
|
-
return resolve(expanded);
|
|
263
|
-
}
|
|
264
205
|
/**
|
|
265
206
|
* `start`'s sessions-dir override: `--sessions-dir` flag > `FASTAGENT_SESSIONS_DIR` env > undefined
|
|
266
207
|
* (the opener then falls back to {@link defaultSessionsDir} under the {@link resolveStateRoot} root).
|
|
@@ -271,43 +212,21 @@ export function resolveSessionsDirOverride(flag, env = process.env) {
|
|
|
271
212
|
}
|
|
272
213
|
/**
|
|
273
214
|
* The auth-file override: `--auth-path` flag > `FASTAGENT_AUTH_PATH` env > undefined (the opener then
|
|
274
|
-
* falls back to {@link defaultAuthPath} under the {@link
|
|
215
|
+
* falls back to {@link defaultAuthPath} under the {@link resolveSecretsDir} dir). Resolved to absolute
|
|
275
216
|
* so the store and the startup report agree regardless of cwd. No implicit project↔global fallback (isolation
|
|
276
217
|
* + fail-visibly; see auth.ts); to share one account across projects, point this at the global
|
|
277
|
-
* `~/.fastagent/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
|
|
218
|
+
* `~/.fastagent/.secrets/auth.json` — sharing ONE file is safe under the store's cross-process refresh lock.
|
|
278
219
|
*/
|
|
279
|
-
|
|
220
|
+
function resolveAuthPathOverride(flag, env = process.env) {
|
|
280
221
|
return resolveOverridePath(flag ?? env.FASTAGENT_AUTH_PATH);
|
|
281
222
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*/
|
|
286
|
-
function projectStateDir(dir) {
|
|
287
|
-
return join(dir, ".fastagent");
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* The resolved state root — the ONE durable machine-state home everything derives from (auth.json,
|
|
291
|
-
* sessions/, channels/<kind>/): `FASTAGENT_STATE_DIR` env > `<dir>/.fastagent`. Absolute, so channels
|
|
292
|
-
* and the startup report agree regardless of cwd. Definition: single lifecycle (precious, survives
|
|
293
|
-
* redeploy), single process — a container mounts ONE volume here. The finer knobs
|
|
294
|
-
* (`FASTAGENT_SESSIONS_DIR`, `FASTAGENT_AUTH_PATH`) still override their specific path on top.
|
|
295
|
-
*
|
|
296
|
-
* `FASTAGENT_STATE_DIR` is an OPERATOR override, so a relative value resolves against `process.cwd()`
|
|
297
|
-
* — the CLI convention its sibling knobs share (`resolveOverridePath`), NOT against `dir`. Only the
|
|
298
|
-
* DEFAULT (`<dir>/.fastagent`) is dir-anchored. Deployments set an absolute path (a mounted volume);
|
|
299
|
-
* a relative value is in-tree — hence self-ignored — only when run from the definition dir (cwd == dir).
|
|
300
|
-
*/
|
|
301
|
-
export function resolveStateRoot(dir, env = process.env) {
|
|
302
|
-
return resolveOverridePath(env.FASTAGENT_STATE_DIR) ?? resolve(projectStateDir(dir));
|
|
303
|
-
}
|
|
304
|
-
/** The default credentials file under a resolved state root ({@link resolveStateRoot}). */
|
|
305
|
-
export function defaultAuthPath(stateRoot) {
|
|
306
|
-
return join(stateRoot, "auth.json");
|
|
223
|
+
/** The default credentials file under a resolved secrets dir ({@link resolveSecretsDir}). */
|
|
224
|
+
export function defaultAuthPath(secretsDir) {
|
|
225
|
+
return join(secretsDir, "auth.json");
|
|
307
226
|
}
|
|
308
|
-
/** The effective auth file for
|
|
227
|
+
/** The effective auth file for an agent: override if present, else `<secrets dir>/auth.json`. */
|
|
309
228
|
export function resolveAuthPath(dir, flag, env = process.env) {
|
|
310
|
-
return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(
|
|
229
|
+
return resolveAuthPathOverride(flag, env) ?? defaultAuthPath(resolveSecretsDir(dir, env));
|
|
311
230
|
}
|
|
312
231
|
/** The default sessions dir under a resolved state root ({@link resolveStateRoot}). */
|
|
313
232
|
export function defaultSessionsDir(stateRoot) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ExecutionEnv, type Skill, type ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Models, Provider } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { Agent } from "../../agent.ts";
|
|
4
4
|
import { type FastagentConfig } from "./config.ts";
|
|
@@ -6,19 +6,20 @@ import { type LoadedDefinition } from "./definition.ts";
|
|
|
6
6
|
import { piHarnessFactory } from "./harness.ts";
|
|
7
7
|
import { type PiSessionStore } from "./sessions.ts";
|
|
8
8
|
import type { ModuleLoadFailure } from "../../loader.ts";
|
|
9
|
-
import { type
|
|
9
|
+
import { type ToolCollision, type MountedTool } from "./tool.ts";
|
|
10
10
|
import { type Lease, type SessionObserver } from "./invoke.ts";
|
|
11
|
-
/** pi's core default toolset (read/bash/edit/write)
|
|
12
|
-
|
|
11
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
12
|
+
* turn as the harness tool context — hence no argument here. */
|
|
13
|
+
export declare function piDefaultTools(): MountedTool[];
|
|
13
14
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
14
|
-
export declare function resolveTools(config: FastagentConfig
|
|
15
|
+
export declare function resolveTools(config: FastagentConfig): MountedTool[];
|
|
15
16
|
/**
|
|
16
|
-
* The full tool set
|
|
17
|
+
* The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
|
|
17
18
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
18
19
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
19
20
|
*/
|
|
20
|
-
export declare function
|
|
21
|
-
tools:
|
|
21
|
+
export declare function resolveAgentTools(config: FastagentConfig, agentDir: string): Promise<{
|
|
22
|
+
tools: MountedTool[];
|
|
22
23
|
toolNames: string[];
|
|
23
24
|
/** Tools registered but not initially active (defineTool `deferred: true`) — discovered/activated
|
|
24
25
|
* via the built-in `search_tools` loader. Surfaced so the operator can see deferral took effect. */
|
|
@@ -33,7 +34,7 @@ export declare function resolveWorkspaceTools(config: FastagentConfig, agentDir:
|
|
|
33
34
|
* `persona` (from persona.md) replaces the default identity line, keeping the tools list + guidelines.
|
|
34
35
|
*/
|
|
35
36
|
export declare function piBasePrompt(options?: {
|
|
36
|
-
tools?:
|
|
37
|
+
tools?: MountedTool[];
|
|
37
38
|
persona?: string;
|
|
38
39
|
}): string;
|
|
39
40
|
export interface AssembleSystemPromptOptions {
|
|
@@ -76,8 +77,10 @@ export interface CreatePiAgentOptions {
|
|
|
76
77
|
* or a factory re-evaluated per invoke. When {@link skills} are mounted their listing is appended.
|
|
77
78
|
*/
|
|
78
79
|
instructions?: string | (() => string);
|
|
79
|
-
/** `FastagentTool`
|
|
80
|
-
|
|
80
|
+
/** The tool set to mount. `FastagentTool` (AgentTool plus the optional `deferred` marker, see
|
|
81
|
+
* {@link DefineToolOptions}) widens into {@link MountedTool}, which additionally admits pi's default
|
|
82
|
+
* coding tools — they read the turn's ExecutionEnv as a fifth `execute` parameter. */
|
|
83
|
+
tools?: MountedTool[];
|
|
81
84
|
skills?: Skill[];
|
|
82
85
|
/**
|
|
83
86
|
* Extra providers registered on top of the built-ins — your own gateway / self-hosted endpoint /
|
|
@@ -86,15 +89,17 @@ export interface CreatePiAgentOptions {
|
|
|
86
89
|
*/
|
|
87
90
|
providers?: Provider[];
|
|
88
91
|
/**
|
|
89
|
-
* Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/auth.json`; the
|
|
90
|
-
* directory opener passes the project-level `<
|
|
92
|
+
* Credentials file for stored OAuth/API-key auth. Defaults to `~/.fastagent/.secrets/auth.json`; the
|
|
93
|
+
* directory opener passes the project-level `<root>/.secrets/auth.json` instead. Env vars are still
|
|
91
94
|
* consulted when a provider is absent from the file (resolution order is upstream-owned).
|
|
92
95
|
*/
|
|
93
96
|
authPath?: string;
|
|
94
97
|
/** Session persistence. Defaults to in-memory; inject jsonlSessionStore for restart-surviving continuity. */
|
|
95
98
|
sessions?: PiSessionStore;
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
99
|
+
/** Filesystem/process environment. Defaults to a local NodeExecutionEnv at `process.cwd()`, and its
|
|
100
|
+
* cwd is the agent's. The default coding tools (read/bash/edit/write) take it as the turn's tool
|
|
101
|
+
* context, so injecting a constrained one narrows where the agent reads, writes and shells. It does
|
|
102
|
+
* NOT constrain author-written `tools/`, which are code and can import anything. */
|
|
98
103
|
env?: ExecutionEnv;
|
|
99
104
|
/** Single-writer lease. Defaults to in-process fail-fast inProcessLease(). */
|
|
100
105
|
lease?: Lease;
|
|
@@ -116,27 +121,30 @@ export interface CreatePiAgentFromDefinitionOptions {
|
|
|
116
121
|
/** Override the engine base prompt (segment ①). Defaults to piBasePrompt({ tools, persona }) using the
|
|
117
122
|
* live-read persona.md; pass base to fully opt out of persona.md. */
|
|
118
123
|
base?: string;
|
|
119
|
-
/** Override tools. Defaults to piDefaultTools (lock down with a custom list).
|
|
120
|
-
* AgentTool plus the optional `deferred` marker. */
|
|
121
|
-
tools?:
|
|
124
|
+
/** Override tools. Defaults to {@link piDefaultTools} (lock down with a custom list). An authored
|
|
125
|
+
* `FastagentTool[]` (AgentTool plus the optional `deferred` marker) widens into {@link MountedTool}. */
|
|
126
|
+
tools?: MountedTool[];
|
|
122
127
|
/**
|
|
123
128
|
* The agent's working directory: where the default tools operate AND whose ancestors are walked for
|
|
124
|
-
* ② project context (AGENTS.md). Defaults to `dir
|
|
125
|
-
*
|
|
126
|
-
*
|
|
129
|
+
* ② project context (AGENTS.md). Defaults to `dir`. Set it to the enclosing repo so a coding agent
|
|
130
|
+
* whose definition lives in `dir` operates on — and reads the AGENTS.md of — that repo (core.md
|
|
131
|
+
* scenario grid); that is what the CLI's opener does with the workspace.
|
|
127
132
|
*/
|
|
128
133
|
cwd?: string;
|
|
129
134
|
/** Extra providers registered on top of the built-ins (your own gateway / self-hosted endpoint). */
|
|
130
135
|
providers?: Provider[];
|
|
131
136
|
/**
|
|
132
137
|
* Credentials file (see {@link CreatePiAgentOptions.authPath}). Being dir-aware, this rung defaults
|
|
133
|
-
* to the PROJECT-level `<dir>/.
|
|
138
|
+
* to the PROJECT-level `<dir>/.secrets/auth.json` (matching `fastagent dev`/`start` on the same
|
|
134
139
|
* dir) — unlike the dir-less {@link createPiAgent}/{@link createPiModels}, which default global.
|
|
135
140
|
*/
|
|
136
141
|
authPath?: string;
|
|
137
142
|
sessions?: PiSessionStore;
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
143
|
+
/** Filesystem/process environment; see {@link CreatePiAgentOptions.env}. At THIS rung it does more
|
|
144
|
+
* than root the default tools: persona.md and skills/ are read through it too. Two surfaces stay
|
|
145
|
+
* OUTSIDE it — ② project context (pi's loadProjectContextFiles uses node fs directly; see
|
|
146
|
+
* definition.ts) and author-written `tools/`, which are code and can import anything. Injecting an
|
|
147
|
+
* env narrows the blast radius rather than closing it. */
|
|
140
148
|
env?: ExecutionEnv;
|
|
141
149
|
lease?: Lease;
|
|
142
150
|
/** Observation-plane tap; see {@link CreatePiAgentOptions.observer}. */
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
* L1 createPiAgent(options) — assemble from typed parts (the canonical ctor).
|
|
7
7
|
* L0 createPiAgentFromHarness({ harnessFactory }) — in invoke.ts (its body is the turn mechanism).
|
|
8
8
|
*
|
|
9
|
-
* Above L2 sits the
|
|
9
|
+
* Above L2 sits the agent opener createPiAgentFromDir (open.ts), which both `dev` and
|
|
10
10
|
* `start` drive. Each rung calls the one below; options narrow as you go up (L2 owns systemPrompt/skills —
|
|
11
11
|
* they come from the definition; the openers own model/tools — from config resolution).
|
|
12
12
|
*/
|
|
13
13
|
import { formatSkillsForSystemPrompt } from "@earendil-works/pi-agent-core";
|
|
14
|
+
import { createBashTool, createEditTool, createReadTool, createWriteTool, } from "@earendil-works/pi-agent-core";
|
|
14
15
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
15
|
-
import {
|
|
16
|
-
import { defaultAuthPath, resolveModel
|
|
16
|
+
import { readImageProcessor } from "./read-image.js";
|
|
17
|
+
import { defaultAuthPath, resolveModel } from "./config.js";
|
|
18
|
+
import { resolveSecretsDir } from "../../paths.js";
|
|
17
19
|
import { loadAgentDefinition } from "./definition.js";
|
|
18
20
|
import { piHarnessFactory } from "./harness.js";
|
|
19
21
|
import { createPiModels } from "./models.js";
|
|
@@ -25,30 +27,52 @@ import { createPiAgentFromHarness, inProcessLease } from "./invoke.js";
|
|
|
25
27
|
// ── §1 tools ─────────────────────────────────────────────────────────────────
|
|
26
28
|
//
|
|
27
29
|
// The full pi toolset is the default for fidelity: authors vibe in local pi with it, so serving with
|
|
28
|
-
// fewer tools is behavior drift.
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// fewer tools is behavior drift. Locking down for public exposure = passing a restricted `tools` list
|
|
31
|
+
// (a deployment posture).
|
|
32
|
+
//
|
|
33
|
+
// These are pi-agent-core's tools, which reach the filesystem and the shell through the
|
|
34
|
+
// {@link ExecutionEnv} the harness hands them per turn — NOT pi-coding-agent's, which are the same four
|
|
35
|
+
// tools wired to `node:fs` directly. Going through the env is the point, and the whole of it: it makes
|
|
36
|
+
// {@link CreatePiAgentOptions.env} the one seam a sandbox adapter has to implement, instead of a knob
|
|
37
|
+
// that governed everything except the tools that actually touch the machine. (It buys no decoupling
|
|
38
|
+
// from pi-coding-agent — definition.ts, models.ts and read-image.ts all import it regardless.)
|
|
39
|
+
//
|
|
40
|
+
// The swap holds only while the two behave alike, which they do NOT for free: core's `read` does
|
|
41
|
+
// nothing with images unless a processor is injected (read-image.ts), and both families are compared
|
|
42
|
+
// on every path in test/tools-parity.test.ts.
|
|
43
|
+
//
|
|
44
|
+
// `chat` is unaffected: it takes these NAMES only and lets pi's own runtime rebuild the tools it
|
|
45
|
+
// renders (see session-builder.ts).
|
|
46
|
+
/** pi's core default toolset (read/bash/edit/write). Rooted at the ExecutionEnv's cwd, supplied per
|
|
47
|
+
* turn as the harness tool context — hence no argument here. */
|
|
48
|
+
export function piDefaultTools() {
|
|
49
|
+
// `read` needs its image pipeline INJECTED (core ships none); see read-image.ts for what is at stake.
|
|
50
|
+
return [
|
|
51
|
+
createReadTool({ imageProcessor: readImageProcessor }),
|
|
52
|
+
createBashTool(),
|
|
53
|
+
createEditTool(),
|
|
54
|
+
createWriteTool(),
|
|
55
|
+
];
|
|
33
56
|
}
|
|
34
57
|
/** `config.tools` semantics: extra tools APPENDED after pi's defaults, never replacing them. */
|
|
35
|
-
export function resolveTools(config
|
|
36
|
-
const defaults = piDefaultTools(
|
|
58
|
+
export function resolveTools(config) {
|
|
59
|
+
const defaults = piDefaultTools();
|
|
37
60
|
return config.tools ? [...defaults, ...config.tools] : defaults;
|
|
38
61
|
}
|
|
39
62
|
/**
|
|
40
|
-
* The full tool set
|
|
63
|
+
* The full tool set an agent mounts: pi defaults + `config.tools` + discovered `tools/` (deduped,
|
|
41
64
|
* existing win), plus the non-default tool names and collisions to report. One source for the
|
|
42
65
|
* dev/start openers AND `fastagent tool`, so they all mount exactly the same set.
|
|
43
66
|
*/
|
|
44
|
-
export async function
|
|
45
|
-
//
|
|
46
|
-
//
|
|
67
|
+
export async function resolveAgentTools(config, agentDir) {
|
|
68
|
+
// Discovered `tools/` come from `agentDir` (the agent's own surface); the default coding tools carry
|
|
69
|
+
// no root of their own — they operate through the ExecutionEnv handed to them per turn, whose cwd is
|
|
70
|
+
// the workspace.
|
|
47
71
|
const discovered = await loadTools(agentDir);
|
|
48
|
-
const merged = mergeDiscoveredTools(resolveTools(config
|
|
49
|
-
// The built-in `search_tools` loader mounts here — the one place the
|
|
50
|
-
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent;
|
|
51
|
-
//
|
|
72
|
+
const merged = mergeDiscoveredTools(resolveTools(config), discovered.tools);
|
|
73
|
+
// The built-in `search_tools` loader mounts here — the one place the agent's full tool set is
|
|
74
|
+
// computed — so `dev`/`start`/`info`/`fastagent tool` all see the same surface (idempotent; an
|
|
75
|
+
// agent-defined search_tools wins).
|
|
52
76
|
const tools = withSearchTool(merged.tools);
|
|
53
77
|
// Builtin = a search_tools that was ABSENT before withSearchTool (a reference compare would misfire
|
|
54
78
|
// on the deferred-authored-loader case, where withSearchTool returns a new array without adding one).
|
|
@@ -58,7 +82,7 @@ export async function resolveWorkspaceTools(config, agentDir, cwd = agentDir) {
|
|
|
58
82
|
// defaults, the builtin loader (like wake, a builtin gets its own report line, not an anonymous
|
|
59
83
|
// slot in the author's list — an author-DEFINED search_tools still shows), and deferred tools —
|
|
60
84
|
// each name lives in exactly ONE report slot, and deferred names live in `deferredToolNames`.
|
|
61
|
-
const defaultNames = new Set(piDefaultTools(
|
|
85
|
+
const defaultNames = new Set(piDefaultTools().map((t) => t.name));
|
|
62
86
|
const toolNames = tools
|
|
63
87
|
.filter((t) => !defaultNames.has(t.name) && !isDeferredTool(t) && !(builtinLoaderMounted && t.name === "search_tools"))
|
|
64
88
|
.map((t) => t.name);
|
|
@@ -98,7 +122,7 @@ export function piBasePrompt(options = {}) {
|
|
|
98
122
|
const deferredCount = mounted.length - tools.length;
|
|
99
123
|
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t.name}: ${(t.description ?? "").split("\n")[0]}`).join("\n") : "(none)";
|
|
100
124
|
// Segment ① identity: an authored persona (persona.md) replaces the default engine identity line
|
|
101
|
-
// (
|
|
125
|
+
// (core.md §11), keeping the tools list + guidelines below.
|
|
102
126
|
const identity = options.persona?.trim() ||
|
|
103
127
|
"You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
104
128
|
const deferredNote = deferredCount > 0
|
|
@@ -215,14 +239,14 @@ export async function createPiAgentFromDefinition(dir, options) {
|
|
|
215
239
|
let reportedFindings = findingsSignature(definition);
|
|
216
240
|
// Deferred tools need their loader on every rung (idempotent — the workspace opener already applied
|
|
217
241
|
// it; a caller's own search_tools wins).
|
|
218
|
-
const tools = withSearchTool(options.tools ?? piDefaultTools(
|
|
242
|
+
const tools = withSearchTool(options.tools ?? piDefaultTools());
|
|
219
243
|
const agent = buildPiAgent({
|
|
220
244
|
model: options.model,
|
|
221
245
|
thinkingLevel: options.thinkingLevel,
|
|
222
246
|
providers: options.providers,
|
|
223
|
-
// Dir-aware default: the same
|
|
247
|
+
// Dir-aware default: the same secrets-dir-derived file the opener uses for this dir (the opener
|
|
224
248
|
// passes an explicit authPath, so this only affects direct L2 callers).
|
|
225
|
-
authPath: options.authPath ?? defaultAuthPath(
|
|
249
|
+
authPath: options.authPath ?? defaultAuthPath(resolveSecretsDir(dir)),
|
|
226
250
|
// The directory is the agent, LIVE: re-read the definition on every invoke, so AGENTS.md/skills
|
|
227
251
|
// edits (the author's, or the agent's own self-modification) take effect on the next turn with
|
|
228
252
|
// no process restart — restarts are reserved for code (tools/channels/config, module cache).
|
|
@@ -32,9 +32,9 @@ export interface LoadedDefinition {
|
|
|
32
32
|
}
|
|
33
33
|
export interface LoadAgentDefinitionOptions {
|
|
34
34
|
/**
|
|
35
|
-
* Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Working directory whose ancestors are walked for context files (segment ②). Default = `agentDir`.
|
|
36
|
+
* The opener passes the workspace instead, so an agent that lives in `agentDir` picks up the
|
|
37
|
+
* project's AGENTS.md up the tree (core.md scenario grid).
|
|
38
38
|
*/
|
|
39
39
|
cwd?: string;
|
|
40
40
|
env?: ExecutionEnv;
|
|
@@ -42,31 +42,12 @@ export interface LoadAgentDefinitionOptions {
|
|
|
42
42
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
43
43
|
export declare function loadAgentDefinition(agentDir: string, options?: LoadAgentDefinitionOptions): Promise<LoadedDefinition>;
|
|
44
44
|
/**
|
|
45
|
-
* Whether `targetPath` lives inside `baseDir` (same path counts).
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
|
|
45
|
+
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
46
|
+
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
47
|
+
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
48
|
+
* does not act on where a user's paths point.
|
|
50
49
|
*/
|
|
51
50
|
export declare function isUnderDir(targetPath: string, baseDir: string): boolean;
|
|
52
|
-
/**
|
|
53
|
-
* The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
|
|
54
|
-
* tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
|
|
55
|
-
* every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
|
|
56
|
-
* write a `.gitignore` bypassing this.
|
|
57
|
-
*
|
|
58
|
-
* ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
|
|
59
|
-
* root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
|
|
60
|
-
* dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
|
|
61
|
-
* (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
|
|
62
|
-
* (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
|
|
63
|
-
* `.gitignore` into it (it may be a directory the operator deliberately tracks).
|
|
64
|
-
*
|
|
65
|
-
* Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
|
|
66
|
-
* for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
|
|
67
|
-
* home, which a dotfiles repo may track. The global credential file there was never self-ignored.
|
|
68
|
-
*/
|
|
69
|
-
export declare function ensureStateRootSelfIgnored(dir: string, stateRoot: string): Promise<void>;
|
|
70
51
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
|
|
71
52
|
* A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
|
|
72
53
|
export declare function canonicalPath(p: string): string;
|
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
* (bad skill files, name collisions) are returned as data. An unreadable ② context file only warns (pi).
|
|
15
15
|
*/
|
|
16
16
|
import { realpathSync } from "node:fs";
|
|
17
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
-
import { homedir } from "node:os";
|
|
19
17
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
20
18
|
import { loadSkills } from "@earendil-works/pi-agent-core";
|
|
21
19
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
22
20
|
import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { assertInsideAgentDir } from "../../paths.js";
|
|
23
22
|
/** Read an agent definition. persona.md/skills come from `agentDir`; ② context = pi's loadProjectContextFiles({ cwd, agentDir }). */
|
|
24
23
|
export async function loadAgentDefinition(agentDir, options = {}) {
|
|
25
24
|
// One resolved default for the working directory (env cwd AND the context-walk start), so they can
|
|
@@ -44,7 +43,9 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
44
43
|
}
|
|
45
44
|
const persona = personaRead.ok ? personaRead.value : undefined;
|
|
46
45
|
// Skills come ONLY from the definition's own skills/ (no external/global mount), so the same
|
|
47
|
-
// definition loads the same skills on every machine
|
|
46
|
+
// definition loads the same skills on every machine — and, like tools/channels/schedules, a symlink
|
|
47
|
+
// that escapes the agent dir is refused rather than followed (the fourth of four surfaces).
|
|
48
|
+
await assertInsideAgentDir(root, "skills");
|
|
48
49
|
const { skills: raw, diagnostics } = await loadSkills(e, [join(root, "skills")]);
|
|
49
50
|
const byName = new Map();
|
|
50
51
|
const collisions = [];
|
|
@@ -60,62 +61,15 @@ export async function loadAgentDefinition(agentDir, options = {}) {
|
|
|
60
61
|
return { contextFiles, persona, skills: [...byName.values()], diagnostics, collisions, dir: root };
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
|
-
* Whether `targetPath` lives inside `baseDir` (same path counts).
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* the tree). Whether a relative override lands in-tree is a cwd question — see `resolveStateRoot`.
|
|
64
|
+
* Whether `targetPath` lives inside `baseDir` (same path counts). Used to ask "did an override move
|
|
65
|
+
* this OUT of the agent?" — the startup report's redeploy notes, `add`'s printed `.env` label, and the
|
|
66
|
+
* dev watcher's "your .env is not watched" warning all turn on that fact. Reporting only: fastagent
|
|
67
|
+
* does not act on where a user's paths point.
|
|
68
68
|
*/
|
|
69
69
|
export function isUnderDir(targetPath, baseDir) {
|
|
70
70
|
const rel = relative(baseDir, targetPath);
|
|
71
71
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Self-ignore a state dir: create it if missing, then write `<stateDir>/.gitignore` = "*" (idempotent
|
|
75
|
-
* — an existing one is kept), so a workspace that runs dev/start never shows machine state as
|
|
76
|
-
* untracked. Creates the dir because a caller may self-ignore it before anything else populates it
|
|
77
|
-
* (e.g. `login` writing auth.json into a not-yet-created root).
|
|
78
|
-
*
|
|
79
|
-
* Module-PRIVATE on purpose: the only entry to the leak guard is {@link ensureStateRootSelfIgnored}
|
|
80
|
-
* (home exclusion + containment). Keeping this unexported makes that single-owner claim hold at the
|
|
81
|
-
* type level — a sibling command can't bypass those checks by writing a `.gitignore` directly.
|
|
82
|
-
*/
|
|
83
|
-
async function ensureStateDirSelfIgnored(stateDir) {
|
|
84
|
-
await mkdir(stateDir, { recursive: true });
|
|
85
|
-
await writeFile(join(stateDir, ".gitignore"), "*\n", { flag: "wx" }).catch((e) => {
|
|
86
|
-
if (e.code !== "EEXIST")
|
|
87
|
-
throw e;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* The single owner of the self-ignore MECHANISM: iff the resolved state ROOT lands inside the workspace
|
|
92
|
-
* tree, write `<stateRoot>/.gitignore="*"` — which then covers EVERYTHING under it (sessions, auth.json,
|
|
93
|
-
* every channel's `channels/<kind>` home). `ensureStateDirSelfIgnored` is private, so a caller cannot
|
|
94
|
-
* write a `.gitignore` bypassing this.
|
|
95
|
-
*
|
|
96
|
-
* ROOT-based, not path-based: everything derives from the state root (config.ts), so protecting the
|
|
97
|
-
* root protects all of it — INCLUDING a custom in-tree root (a `FASTAGENT_STATE_DIR` inside the agent
|
|
98
|
-
* dir), the case a path-based (`.fastagent`-only) guard would leak. A per-path override
|
|
99
|
-
* (`--sessions-dir`/`--auth-path`) is operator-owned: pointed at an external volume it is out-of-tree
|
|
100
|
-
* (correctly not ours to ignore); pointed at a custom in-tree dir WE DON'T OWN, we do not write a
|
|
101
|
-
* `.gitignore` into it (it may be a directory the operator deliberately tracks).
|
|
102
|
-
*
|
|
103
|
-
* Excludes the user's HOME-global `~/.fastagent` (e.g. `login`/`dev` run from `$HOME`): self-ignore is
|
|
104
|
-
* for protecting state inside an agent PROJECT tree, not for writing a `.gitignore` into the user's
|
|
105
|
-
* home, which a dotfiles repo may track. The global credential file there was never self-ignored.
|
|
106
|
-
*/
|
|
107
|
-
export async function ensureStateRootSelfIgnored(dir, stateRoot) {
|
|
108
|
-
// Compare CANONICAL paths for the home check: `dir` arrives realpath-resolved (it is `process.cwd()`
|
|
109
|
-
// or `resolve(".")`) but `homedir()` returns the raw `$HOME`, so a symlinked home would slip past raw
|
|
110
|
-
// equality and we'd write a `.gitignore` into the real `~/.fastagent` — the very thing the doc forbids
|
|
111
|
-
// (chat.ts canonicalizes for the same reason).
|
|
112
|
-
if (canonicalPath(dir) === canonicalPath(homedir()))
|
|
113
|
-
return;
|
|
114
|
-
// Containment on RAW paths: stateRoot is resolve()'d (config.ts) and `dir` is absolute, so it is exact
|
|
115
|
-
// by construction. An external-volume root resolves outside the tree → skip (not ours to ignore).
|
|
116
|
-
if (isUnderDir(stateRoot, dir))
|
|
117
|
-
await ensureStateDirSelfIgnored(stateRoot);
|
|
118
|
-
}
|
|
119
73
|
/** Resolve to a canonical (symlink-free) absolute path so comparisons match `process.cwd()`'s realpath.
|
|
120
74
|
* A non-existent path can't be realpath'd, so it stays as the plain absolute resolve. */
|
|
121
75
|
export function canonicalPath(p) {
|