pi-forge 0.0.0 → 1.1.4
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/LICENSE +21 -0
- package/README.md +48 -4
- package/bin/pi-forge.mjs +37 -0
- package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js +34 -0
- package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js.map +1 -0
- package/dist/client/assets/index-B-529kgJ.css +32 -0
- package/dist/client/assets/index-BzKzxXFs.js +392 -0
- package/dist/client/assets/index-BzKzxXFs.js.map +1 -0
- package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js +3 -0
- package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js.map +1 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/icons/icon.svg +9 -0
- package/dist/client/index.html +24 -0
- package/dist/client/manifest.webmanifest +1 -0
- package/dist/client/offline.html +142 -0
- package/dist/client/sw.js +3 -0
- package/dist/client/sw.js.map +1 -0
- package/dist/client/workbox-6d7155ed.js +3 -0
- package/dist/client/workbox-6d7155ed.js.map +1 -0
- package/dist/server/agent-resource-loader.js +126 -0
- package/dist/server/agent-resource-loader.js.map +1 -0
- package/dist/server/attachment-converters.js +96 -0
- package/dist/server/attachment-converters.js.map +1 -0
- package/dist/server/auth.js +209 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/compaction-history.js +106 -0
- package/dist/server/compaction-history.js.map +1 -0
- package/dist/server/concurrency.js +49 -0
- package/dist/server/concurrency.js.map +1 -0
- package/dist/server/config-export.js +220 -0
- package/dist/server/config-export.js.map +1 -0
- package/dist/server/config-manager.js +528 -0
- package/dist/server/config-manager.js.map +1 -0
- package/dist/server/config.js +326 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/conversion-worker.mjs +90 -0
- package/dist/server/diagnostics.js +137 -0
- package/dist/server/diagnostics.js.map +1 -0
- package/dist/server/extensions-discovery.js +147 -0
- package/dist/server/extensions-discovery.js.map +1 -0
- package/dist/server/file-manager.js +734 -0
- package/dist/server/file-manager.js.map +1 -0
- package/dist/server/file-references.js +215 -0
- package/dist/server/file-references.js.map +1 -0
- package/dist/server/file-searcher.js +385 -0
- package/dist/server/file-searcher.js.map +1 -0
- package/dist/server/git-runner.js +684 -0
- package/dist/server/git-runner.js.map +1 -0
- package/dist/server/index.js +468 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp/config.js +133 -0
- package/dist/server/mcp/config.js.map +1 -0
- package/dist/server/mcp/manager.js +351 -0
- package/dist/server/mcp/manager.js.map +1 -0
- package/dist/server/mcp/tool-bridge.js +173 -0
- package/dist/server/mcp/tool-bridge.js.map +1 -0
- package/dist/server/project-manager.js +301 -0
- package/dist/server/project-manager.js.map +1 -0
- package/dist/server/pty-manager.js +354 -0
- package/dist/server/pty-manager.js.map +1 -0
- package/dist/server/routes/_schemas.js +73 -0
- package/dist/server/routes/_schemas.js.map +1 -0
- package/dist/server/routes/auth.js +164 -0
- package/dist/server/routes/auth.js.map +1 -0
- package/dist/server/routes/config.js +1163 -0
- package/dist/server/routes/config.js.map +1 -0
- package/dist/server/routes/control.js +464 -0
- package/dist/server/routes/control.js.map +1 -0
- package/dist/server/routes/exec.js +217 -0
- package/dist/server/routes/exec.js.map +1 -0
- package/dist/server/routes/files.js +847 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/git.js +837 -0
- package/dist/server/routes/git.js.map +1 -0
- package/dist/server/routes/health.js +97 -0
- package/dist/server/routes/health.js.map +1 -0
- package/dist/server/routes/mcp.js +300 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/projects.js +259 -0
- package/dist/server/routes/projects.js.map +1 -0
- package/dist/server/routes/prompt.js +496 -0
- package/dist/server/routes/prompt.js.map +1 -0
- package/dist/server/routes/sessions.js +783 -0
- package/dist/server/routes/sessions.js.map +1 -0
- package/dist/server/routes/stream.js +69 -0
- package/dist/server/routes/stream.js.map +1 -0
- package/dist/server/routes/terminal.js +335 -0
- package/dist/server/routes/terminal.js.map +1 -0
- package/dist/server/session-registry.js +1197 -0
- package/dist/server/session-registry.js.map +1 -0
- package/dist/server/skill-overrides.js +151 -0
- package/dist/server/skill-overrides.js.map +1 -0
- package/dist/server/skills-export.js +257 -0
- package/dist/server/skills-export.js.map +1 -0
- package/dist/server/sse-bridge.js +220 -0
- package/dist/server/sse-bridge.js.map +1 -0
- package/dist/server/tool-overrides.js +277 -0
- package/dist/server/tool-overrides.js.map +1 -0
- package/dist/server/turn-diff-builder.js +280 -0
- package/dist/server/turn-diff-builder.js.map +1 -0
- package/package.json +53 -12
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
function readEnv(key) {
|
|
7
|
+
const v = process.env[key];
|
|
8
|
+
return v === undefined || v === "" ? undefined : v;
|
|
9
|
+
}
|
|
10
|
+
function readInt(key, fallback) {
|
|
11
|
+
const v = readEnv(key);
|
|
12
|
+
if (v === undefined)
|
|
13
|
+
return fallback;
|
|
14
|
+
const n = Number.parseInt(v, 10);
|
|
15
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
16
|
+
throw new Error(`config: ${key} must be a non-negative integer (got ${v})`);
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
function readStringList(key) {
|
|
21
|
+
const v = readEnv(key);
|
|
22
|
+
if (v === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
// Comma- or whitespace-separated; either is natural in shell, k8s
|
|
25
|
+
// env, and docker-compose `environment:` lists. Drop empties so
|
|
26
|
+
// trailing commas don't produce ghost entries.
|
|
27
|
+
return v
|
|
28
|
+
.split(/[,\s]+/)
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter((s) => s.length > 0);
|
|
31
|
+
}
|
|
32
|
+
function readBool(key, fallback) {
|
|
33
|
+
const v = readEnv(key)?.toLowerCase();
|
|
34
|
+
if (v === undefined)
|
|
35
|
+
return fallback;
|
|
36
|
+
if (["1", "true", "yes", "on"].includes(v))
|
|
37
|
+
return true;
|
|
38
|
+
if (["0", "false", "no", "off"].includes(v))
|
|
39
|
+
return false;
|
|
40
|
+
throw new Error(`config: ${key} must be a boolean-ish value (got ${v})`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Forge-owned root. `~/.pi-forge` is the single dotdir we own.
|
|
44
|
+
* By default it holds both the project registry and the workspace where
|
|
45
|
+
* user code lives:
|
|
46
|
+
*
|
|
47
|
+
* ~/.pi-forge/
|
|
48
|
+
* ├── projects.json ← FORGE_DATA_DIR by default
|
|
49
|
+
* └── workspace/ ← WORKSPACE_PATH by default
|
|
50
|
+
*
|
|
51
|
+
* Either path can be relocated independently via its env var (e.g. point
|
|
52
|
+
* `WORKSPACE_PATH` at an existing `~/Code` dir to use code you already
|
|
53
|
+
* have on disk). Docker compose sets both explicitly so the container
|
|
54
|
+
* layout is unchanged.
|
|
55
|
+
*/
|
|
56
|
+
const HOME = homedir();
|
|
57
|
+
if (HOME === "/" || HOME === "") {
|
|
58
|
+
throw new Error(`config: os.homedir() returned ${JSON.stringify(HOME)}. ` +
|
|
59
|
+
"This usually means HOME / USERPROFILE is unset. " +
|
|
60
|
+
"Set WORKSPACE_PATH, PI_CONFIG_DIR, and FORGE_DATA_DIR explicitly, " +
|
|
61
|
+
"or run the server with a real user account.");
|
|
62
|
+
}
|
|
63
|
+
const FORGE_HOME = join(HOME, ".pi-forge");
|
|
64
|
+
const WORKSPACE_PATH = resolve(readEnv("WORKSPACE_PATH") ?? join(FORGE_HOME, "workspace"));
|
|
65
|
+
// Default to the current user's home so local dev on macOS/Linux just works.
|
|
66
|
+
// In the documented Docker setup this still resolves to `/root/.pi/agent`
|
|
67
|
+
// (root's homedir IS `/root` inside the container), so the production target
|
|
68
|
+
// is unchanged. Override explicitly via PI_CONFIG_DIR if needed.
|
|
69
|
+
const PI_CONFIG_DIR = resolve(readEnv("PI_CONFIG_DIR") ?? join(HOME, ".pi", "agent"));
|
|
70
|
+
const SESSION_DIR = resolve(readEnv("SESSION_DIR") ?? `${WORKSPACE_PATH}/.pi/sessions`);
|
|
71
|
+
/**
|
|
72
|
+
* Forge-owned data dir. Holds `projects.json` (the project registry
|
|
73
|
+
* pi-forge layers on top of pi) and any other state that's ours, not
|
|
74
|
+
* pi's. Defaults to `FORGE_HOME` (~/.pi-forge) so projects.json
|
|
75
|
+
* sits next to the workspace folder. Kept SEPARATE from `PI_CONFIG_DIR`
|
|
76
|
+
* (~/.pi/agent), which is owned by the pi SDK — auth.json, models.json,
|
|
77
|
+
* settings.json. Dropping our state into the SDK's dir was the original
|
|
78
|
+
* design and got refactored out.
|
|
79
|
+
*/
|
|
80
|
+
const FORGE_DATA_DIR = resolve(readEnv("FORGE_DATA_DIR") ?? FORGE_HOME);
|
|
81
|
+
/**
|
|
82
|
+
* Path to the built client (Vite output). In production we serve this via
|
|
83
|
+
* `@fastify/static`. The default resolves relative to the compiled server
|
|
84
|
+
* file (`packages/server/dist/config.js` → `../../client/dist`), which
|
|
85
|
+
* works for both the local `npm run build && node dist/index.js` flow and
|
|
86
|
+
* the Docker image (which mirrors the same `packages/server/dist` +
|
|
87
|
+
* `packages/client/dist` layout). Override with `CLIENT_DIST_PATH` if you
|
|
88
|
+
* relocate the built assets.
|
|
89
|
+
*/
|
|
90
|
+
const CLIENT_DIST_PATH = resolve(readEnv("CLIENT_DIST_PATH") ??
|
|
91
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "client", "dist"));
|
|
92
|
+
const UI_PASSWORD = readEnv("UI_PASSWORD");
|
|
93
|
+
const API_KEY = readEnv("API_KEY");
|
|
94
|
+
const CORS_ORIGIN = readEnv("CORS_ORIGIN");
|
|
95
|
+
/**
|
|
96
|
+
* Load a JWT signing key from `${FORGE_DATA_DIR}/jwt-secret`, or
|
|
97
|
+
* generate-and-persist one on first boot. Treated like an SSH host key:
|
|
98
|
+
* created once, persisted to the data dir (which is the PVC / bind-mount
|
|
99
|
+
* in K8s and Docker), reused across restarts so issued tokens stay
|
|
100
|
+
* valid. Setting `JWT_SECRET` env explicitly skips this entirely.
|
|
101
|
+
*
|
|
102
|
+
* Only invoked when `UI_PASSWORD` is set — if browser auth isn't on,
|
|
103
|
+
* we don't need a secret at all.
|
|
104
|
+
*/
|
|
105
|
+
function loadOrGenerateJwtSecret(dataDir) {
|
|
106
|
+
const path = join(dataDir, "jwt-secret");
|
|
107
|
+
if (existsSync(path)) {
|
|
108
|
+
const v = readFileSync(path, "utf8").trim();
|
|
109
|
+
// 32 bytes = 256 bits ≈ 43 base64url chars. Anything shorter is
|
|
110
|
+
// either truncated or hand-edited; regenerate rather than trust it.
|
|
111
|
+
if (v.length >= 32)
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
mkdirSync(dataDir, { recursive: true });
|
|
115
|
+
const secret = randomBytes(48).toString("base64url");
|
|
116
|
+
const tmp = `${path}.tmp`;
|
|
117
|
+
writeFileSync(tmp, `${secret}\n`, { mode: 0o600 });
|
|
118
|
+
chmodSync(tmp, 0o600);
|
|
119
|
+
renameSync(tmp, path);
|
|
120
|
+
console.log(`[config] auto-generated JWT secret persisted at ${path}. ` +
|
|
121
|
+
"Delete this file to rotate (logs out all browser sessions).");
|
|
122
|
+
return secret;
|
|
123
|
+
}
|
|
124
|
+
const JWT_SECRET = readEnv("JWT_SECRET") ??
|
|
125
|
+
(UI_PASSWORD !== undefined ? loadOrGenerateJwtSecret(FORGE_DATA_DIR) : undefined);
|
|
126
|
+
export const config = Object.freeze({
|
|
127
|
+
port: readInt("PORT", 3000),
|
|
128
|
+
// HOST default depends on NODE_ENV. Production binds 0.0.0.0 (Docker
|
|
129
|
+
// image's normal mode); dev binds 127.0.0.1 so a `npm run dev` on a
|
|
130
|
+
// laptop doesn't silently expose the agent's shell + filesystem to
|
|
131
|
+
// anyone on the same WiFi/VLAN. Operators who want LAN access in dev
|
|
132
|
+
// can set HOST=0.0.0.0 explicitly.
|
|
133
|
+
host: readEnv("HOST") ?? (process.env.NODE_ENV === "production" ? "0.0.0.0" : "127.0.0.1"),
|
|
134
|
+
logLevel: readEnv("LOG_LEVEL") ?? "info",
|
|
135
|
+
isTest: (readEnv("NODE_ENV") ?? "") === "test",
|
|
136
|
+
trustProxy: readBool("TRUST_PROXY", false),
|
|
137
|
+
workspacePath: WORKSPACE_PATH,
|
|
138
|
+
piConfigDir: PI_CONFIG_DIR,
|
|
139
|
+
forgeDataDir: FORGE_DATA_DIR,
|
|
140
|
+
sessionDir: SESSION_DIR,
|
|
141
|
+
clientDistPath: CLIENT_DIST_PATH,
|
|
142
|
+
serveClient: readBool("SERVE_CLIENT", true),
|
|
143
|
+
/**
|
|
144
|
+
* Frontend "minimal" mode. When true, the client UI hides the
|
|
145
|
+
* terminal, git pane, last-turn pane, and the providers/agent
|
|
146
|
+
* settings sections, and replaces the project folder picker with
|
|
147
|
+
* a name-only form that creates `<workspacePath>/<name>`. Server
|
|
148
|
+
* routes are unchanged — this is purely a frontend gate exposed
|
|
149
|
+
* via `GET /api/v1/ui-config`. Use case: locked-down deployments
|
|
150
|
+
* where provider config is managed at the deploy level.
|
|
151
|
+
*/
|
|
152
|
+
minimalUi: readBool("MINIMAL_UI", false),
|
|
153
|
+
/**
|
|
154
|
+
* When true, `GET /config/providers` filters out provider entries
|
|
155
|
+
* whose name does NOT appear as a key in `models.json`. Built-in
|
|
156
|
+
* providers (anthropic, openai, etc. that the SDK ships with) are
|
|
157
|
+
* hidden from the Settings → Providers list, leaving only the
|
|
158
|
+
* custom providers the operator added via `models.json`. Useful
|
|
159
|
+
* for deployments that route every model through a single internal
|
|
160
|
+
* gateway (vLLM, LiteLLM, internal proxy) and don't want users
|
|
161
|
+
* picking the public providers from the UI.
|
|
162
|
+
*
|
|
163
|
+
* Intentionally not exposed in docker-compose / .env.example —
|
|
164
|
+
* advanced env knob, document if/when it's needed widely.
|
|
165
|
+
*/
|
|
166
|
+
hideBuiltinProviders: readBool("HIDE_BUILTIN_PROVIDERS", false),
|
|
167
|
+
/**
|
|
168
|
+
* Path to the forge-owned MCP server registry. Lives in the
|
|
169
|
+
* data dir (not pi's config dir) because pi has no native MCP
|
|
170
|
+
* support — `mcp.json` is purely a forge file, surfaced to
|
|
171
|
+
* the agent via `customTools` on createAgentSession.
|
|
172
|
+
*/
|
|
173
|
+
mcpConfigFile: join(FORGE_DATA_DIR, "mcp.json"),
|
|
174
|
+
/**
|
|
175
|
+
* Path to the forge-private per-project skill overrides file.
|
|
176
|
+
* Lives in the data dir (NOT in PI_CONFIG_DIR — pi's settings.skills
|
|
177
|
+
* is global, and not in `<project>/.pi/` — the user picked
|
|
178
|
+
* forge-private over team-shared so each install has its own
|
|
179
|
+
* preferences without bleeding into the project tree).
|
|
180
|
+
*/
|
|
181
|
+
skillOverridesFile: join(FORGE_DATA_DIR, "skills-overrides.json"),
|
|
182
|
+
/**
|
|
183
|
+
* Path to the forge-private per-tool override file. Captures
|
|
184
|
+
* "user has explicitly disabled this builtin tool" and "user has
|
|
185
|
+
* explicitly disabled this MCP tool" — both as flat allow-by-default
|
|
186
|
+
* sets. Lives in the data dir (forge-owned; pi's SDK has no
|
|
187
|
+
* native concept of per-tool toggles, this is purely a forge
|
|
188
|
+
* filter applied to the `tools` allowlist passed to
|
|
189
|
+
* createAgentSession).
|
|
190
|
+
*/
|
|
191
|
+
toolOverridesFile: join(FORGE_DATA_DIR, "tool-overrides.json"),
|
|
192
|
+
/**
|
|
193
|
+
* Whether `/api/docs` (Swagger UI + OpenAPI JSON spec) is reachable.
|
|
194
|
+
* Defaults to true so Docker / production deploys keep working without
|
|
195
|
+
* extra config (the README quickstart documents `/api/docs`). When
|
|
196
|
+
* auth is enabled, the existing token check still gates the docs;
|
|
197
|
+
* when auth is disabled, the docs are an info-leak surface (route
|
|
198
|
+
* catalogue, body schemas), so security-conscious operators in
|
|
199
|
+
* unauthenticated public-internet deployments should set
|
|
200
|
+
* `EXPOSE_DOCS=false` — though that combo is itself discouraged
|
|
201
|
+
* (see SECURITY.md: never network-expose without auth + TLS).
|
|
202
|
+
*/
|
|
203
|
+
exposeDocs: readBool("EXPOSE_DOCS", true),
|
|
204
|
+
auth: Object.freeze({
|
|
205
|
+
uiPassword: UI_PASSWORD,
|
|
206
|
+
jwtSecret: JWT_SECRET,
|
|
207
|
+
apiKey: API_KEY,
|
|
208
|
+
jwtExpiresInSeconds: readInt("JWT_EXPIRES_IN_SECONDS", 60 * 60 * 24 * 7),
|
|
209
|
+
loginRateLimitMax: readInt("RATE_LIMIT_LOGIN_MAX", 10),
|
|
210
|
+
loginRateLimitWindowMs: readInt("RATE_LIMIT_LOGIN_WINDOW_MS", 60_000),
|
|
211
|
+
/**
|
|
212
|
+
* When true and the only credential is the env-provided UI_PASSWORD
|
|
213
|
+
* (no on-disk hash yet), the login response carries
|
|
214
|
+
* `mustChangePassword: true` and the issued JWT is restricted —
|
|
215
|
+
* the user can only call `POST /auth/change-password` until they
|
|
216
|
+
* pick a new password. After the user changes it, the new password
|
|
217
|
+
* is hashed and persisted to `${FORGE_DATA_DIR}/password-hash`,
|
|
218
|
+
* and subsequent logins ignore the env value.
|
|
219
|
+
*
|
|
220
|
+
* Defaults to true so deployments that bake an initial password
|
|
221
|
+
* into env (helm secret, docker-compose .env) don't accidentally
|
|
222
|
+
* leave that credential as the long-lived one.
|
|
223
|
+
*/
|
|
224
|
+
requirePasswordChange: readBool("REQUIRE_PASSWORD_CHANGE", true),
|
|
225
|
+
/** Where the persisted scrypt hash lives — see auth.ts. */
|
|
226
|
+
passwordHashFile: join(FORGE_DATA_DIR, "password-hash"),
|
|
227
|
+
}),
|
|
228
|
+
/**
|
|
229
|
+
* Per-route rate limits applied to the cost-heavy / disk-heavy / CPU-heavy
|
|
230
|
+
* routes. Defaults are conservative — enough headroom for an interactive
|
|
231
|
+
* user, low enough that a leaked-token spam loop hits the cap fast.
|
|
232
|
+
* Operators with higher legitimate volume can raise via env.
|
|
233
|
+
*/
|
|
234
|
+
rateLimits: Object.freeze({
|
|
235
|
+
// /sessions/:id/{prompt,steer,compact,navigate} — per-user prompt
|
|
236
|
+
// floor. 60 / minute = 1 / second sustained, far above interactive
|
|
237
|
+
// typing speed; a runaway script gets capped in roughly 1 minute.
|
|
238
|
+
promptMax: readInt("RATE_LIMIT_PROMPT_MAX", 60),
|
|
239
|
+
promptWindowMs: readInt("RATE_LIMIT_PROMPT_WINDOW_MS", 60_000),
|
|
240
|
+
// /files/upload — disk fill. 30 / minute keeps an attentive user
|
|
241
|
+
// unblocked while capping a fill-the-disk loop.
|
|
242
|
+
uploadMax: readInt("RATE_LIMIT_UPLOAD_MAX", 30),
|
|
243
|
+
uploadWindowMs: readInt("RATE_LIMIT_UPLOAD_WINDOW_MS", 60_000),
|
|
244
|
+
// /files/search — CPU. ripgrep walks the workspace; each search is
|
|
245
|
+
// bounded by ripgrep but a tight loop still spins a CPU core.
|
|
246
|
+
searchMax: readInt("RATE_LIMIT_SEARCH_MAX", 60),
|
|
247
|
+
searchWindowMs: readInt("RATE_LIMIT_SEARCH_WINDOW_MS", 60_000),
|
|
248
|
+
// /git/push — network amplification + rate-limited by the git remote.
|
|
249
|
+
// Conservative — pushing 10x in a minute is almost always a mistake.
|
|
250
|
+
pushMax: readInt("RATE_LIMIT_PUSH_MAX", 10),
|
|
251
|
+
pushWindowMs: readInt("RATE_LIMIT_PUSH_WINDOW_MS", 60_000),
|
|
252
|
+
}),
|
|
253
|
+
corsOrigin: CORS_ORIGIN,
|
|
254
|
+
/**
|
|
255
|
+
* Extra env-var names the operator wants the integrated terminal
|
|
256
|
+
* (and the `!` exec route) to inherit from the pi-forge process.
|
|
257
|
+
*
|
|
258
|
+
* The terminal env starts from a small allowlist of harmless system
|
|
259
|
+
* vars (PATH, HOME, USER, SHELL, TERM, locales — see
|
|
260
|
+
* `pty-manager.ts#TERMINAL_ENV_ALLOWLIST`). Everything else is
|
|
261
|
+
* dropped — including provider API keys (`OPENAI_API_KEY`,
|
|
262
|
+
* `AWS_ACCESS_KEY_ID`, etc.) the operator may have in their host
|
|
263
|
+
* shell that would otherwise be inherited by every spawn. This
|
|
264
|
+
* defaults to fail-safe: any new sensitive var the operator sets
|
|
265
|
+
* is hidden from the shell unless they explicitly pass it through.
|
|
266
|
+
*
|
|
267
|
+
* Add specific vars here when the shell genuinely needs them
|
|
268
|
+
* (e.g. `KUBECONFIG`, `EDITOR`, `OPENAI_BASE_URL` for an internal
|
|
269
|
+
* proxy). Format: comma- or whitespace-separated.
|
|
270
|
+
*
|
|
271
|
+
* Example: `TERMINAL_PASSTHROUGH_ENV=KUBECONFIG,EDITOR,NODE_ENV`
|
|
272
|
+
*/
|
|
273
|
+
terminalPassthroughEnv: Object.freeze(readStringList("TERMINAL_PASSTHROUGH_ENV")),
|
|
274
|
+
/**
|
|
275
|
+
* Opt-in: append a pi-forge-defined "secret hygiene" rule to the
|
|
276
|
+
* agent's system prompt. The rule asks the model to treat env-var
|
|
277
|
+
* values as credentials by default and not echo them into responses
|
|
278
|
+
* or tool outputs unless explicitly asked. See
|
|
279
|
+
* `agent-resource-loader.ts#FORGE_SECRET_HYGIENE_RULE` for the
|
|
280
|
+
* exact wording and `SECURITY.md` for the threat-model framing
|
|
281
|
+
* (behavioral nudge, not a security control).
|
|
282
|
+
*
|
|
283
|
+
* Default OFF. Operators who want it explicitly opt in by setting
|
|
284
|
+
* `AGENT_SECRET_HYGIENE_RULE=true`. Kept opt-in (rather than
|
|
285
|
+
* default-on) so the pi-forge doesn't ship invisible behavioral
|
|
286
|
+
* rules that constrain the agent in ways the user never asked for.
|
|
287
|
+
* Deliberately not surfaced in `docker-compose.yml` or
|
|
288
|
+
* `.env.example` — this is an advanced knob, intentionally
|
|
289
|
+
* discoverable only via SECURITY.md so operators meet the rule
|
|
290
|
+
* the same time they meet its caveats.
|
|
291
|
+
*/
|
|
292
|
+
agentSecretHygieneRule: readBool("AGENT_SECRET_HYGIENE_RULE", false),
|
|
293
|
+
/**
|
|
294
|
+
* How long a detached PTY (its WebSocket closed but no replacement
|
|
295
|
+
* attached yet) is held alive before being reaped. The 10-minute
|
|
296
|
+
* default protects the common reattach use case: page refresh,
|
|
297
|
+
* transient network blip, laptop sleep — none of those should kill
|
|
298
|
+
* the user's shell.
|
|
299
|
+
*
|
|
300
|
+
* Operators in resource-constrained envs (kiosks, low-RAM
|
|
301
|
+
* containers) can shrink this. The integration test pins it to
|
|
302
|
+
* ~200 ms so the reap-on-close assertion completes within a normal
|
|
303
|
+
* test budget instead of waiting 10 minutes.
|
|
304
|
+
*
|
|
305
|
+
* Read by `pty-manager.ts#IDLE_REAP_MS` at module load. Setting
|
|
306
|
+
* this to 0 effectively disables reattach-after-WS-drop (every WS
|
|
307
|
+
* close becomes a hard kill); use that deliberately or not at all.
|
|
308
|
+
*/
|
|
309
|
+
terminalIdleReapMs: readInt("PTY_IDLE_REAP_MS", 10 * 60 * 1000),
|
|
310
|
+
});
|
|
311
|
+
export function authEnabled() {
|
|
312
|
+
return (config.auth.uiPassword !== undefined ||
|
|
313
|
+
config.auth.apiKey !== undefined ||
|
|
314
|
+
existsSync(config.auth.passwordHashFile));
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* True iff this deployment supports the browser password-change flow:
|
|
318
|
+
* either an env-supplied UI_PASSWORD is in use OR a hash has already
|
|
319
|
+
* been persisted from a prior change. API-key-only deployments don't
|
|
320
|
+
* have a password to change, so the Settings → General password
|
|
321
|
+
* section hides on them. Read by /ui-config.
|
|
322
|
+
*/
|
|
323
|
+
export function passwordAuthEnabled() {
|
|
324
|
+
return config.auth.uiPassword !== undefined || existsSync(config.auth.passwordHashFile);
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Worker entry for PDF / DOCX / XLSX text extraction.
|
|
2
|
+
//
|
|
3
|
+
// Lives off the main thread because pdfjs-dist (used by pdf-parse) and
|
|
4
|
+
// ExcelJS do heavy synchronous JavaScript work — a 2 MB PDF can block
|
|
5
|
+
// the event loop for several seconds. While blocked, the SSE bridge's
|
|
6
|
+
// heartbeat can't fire and the underlying TCP socket can stall enough
|
|
7
|
+
// for Node's HTTP layer (or any proxy in front) to drop the
|
|
8
|
+
// connection. Browsers see the SSE close and the chat shows
|
|
9
|
+
// "Reconnecting…" right after the user submits a prompt with an
|
|
10
|
+
// attachment.
|
|
11
|
+
//
|
|
12
|
+
// Pure ESM JS (.mjs) so it runs unmodified under both `tsx` (dev) and
|
|
13
|
+
// compiled-prod node — the build script copies it next to the
|
|
14
|
+
// compiled .js dispatcher.
|
|
15
|
+
import { Buffer } from "node:buffer";
|
|
16
|
+
import { parentPort } from "node:worker_threads";
|
|
17
|
+
import ExcelJS from "exceljs";
|
|
18
|
+
import mammoth from "mammoth";
|
|
19
|
+
import { PDFParse } from "pdf-parse";
|
|
20
|
+
|
|
21
|
+
if (parentPort === null) {
|
|
22
|
+
throw new Error("conversion-worker.mjs must be loaded as a worker_threads worker");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function convertPdf(buf) {
|
|
26
|
+
let parser;
|
|
27
|
+
try {
|
|
28
|
+
parser = new PDFParse({ data: new Uint8Array(buf) });
|
|
29
|
+
const result = await parser.getText();
|
|
30
|
+
if (result.pages.length === 0) {
|
|
31
|
+
return "[PDF contained no extractable text — possibly a scanned/image-only document]";
|
|
32
|
+
}
|
|
33
|
+
return result.pages.map((p) => `--- Page ${p.num} ---\n${p.text.trimEnd()}`).join("\n\n");
|
|
34
|
+
} finally {
|
|
35
|
+
if (parser !== undefined) {
|
|
36
|
+
await parser.destroy().catch(() => undefined);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function convertDocx(buf) {
|
|
42
|
+
const result = await mammoth.extractRawText({ buffer: buf });
|
|
43
|
+
return result.value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function convertXlsx(buf) {
|
|
47
|
+
const wb = new ExcelJS.Workbook();
|
|
48
|
+
await wb.xlsx.load(buf);
|
|
49
|
+
const sheets = [];
|
|
50
|
+
wb.eachSheet((ws) => {
|
|
51
|
+
const rows = [];
|
|
52
|
+
ws.eachRow({ includeEmpty: false }, (row) => {
|
|
53
|
+
const cells = [];
|
|
54
|
+
row.eachCell({ includeEmpty: true }, (cell) => {
|
|
55
|
+
cells.push(csvEscape(cell.text ?? ""));
|
|
56
|
+
});
|
|
57
|
+
rows.push(cells.join(","));
|
|
58
|
+
});
|
|
59
|
+
sheets.push(`--- Sheet: ${ws.name} ---\n${rows.join("\n")}`);
|
|
60
|
+
});
|
|
61
|
+
if (sheets.length === 0) return "[Workbook contained no readable sheets]";
|
|
62
|
+
return sheets.join("\n\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function csvEscape(s) {
|
|
66
|
+
if (s.length === 0) return s;
|
|
67
|
+
if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
|
|
68
|
+
return s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
parentPort.on("message", async (msg) => {
|
|
72
|
+
// msg = { id, format, buf } — buf arrives as ArrayBuffer because
|
|
73
|
+
// that transfers cheaply between threads.
|
|
74
|
+
const { id, format, buf } = msg;
|
|
75
|
+
const buffer = Buffer.from(buf);
|
|
76
|
+
try {
|
|
77
|
+
let text;
|
|
78
|
+
if (format === "pdf") text = await convertPdf(buffer);
|
|
79
|
+
else if (format === "docx") text = await convertDocx(buffer);
|
|
80
|
+
else if (format === "xlsx") text = await convertXlsx(buffer);
|
|
81
|
+
else throw new Error(`unknown format: ${format}`);
|
|
82
|
+
parentPort.postMessage({ id, ok: true, text });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
parentPort.postMessage({
|
|
85
|
+
id,
|
|
86
|
+
ok: false,
|
|
87
|
+
error: err instanceof Error ? err.message : String(err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator-visible error diagnostics. The pi SDK swallows provider
|
|
3
|
+
* errors into terse messages ("Connection Error", "fetch failed",
|
|
4
|
+
* "Provider returned an error stop reason") that omit the actual
|
|
5
|
+
* cause — most commonly a TLS handshake failure, DNS error, or
|
|
6
|
+
* connection refused. Without the cause chain, an operator gets a
|
|
7
|
+
* useless one-liner and has to attach a debugger.
|
|
8
|
+
*
|
|
9
|
+
* This module:
|
|
10
|
+
* 1. Installs `unhandledRejection` + `uncaughtException` handlers
|
|
11
|
+
* that print the full `cause` chain to stderr.
|
|
12
|
+
* 2. Exports `formatErrorChain()` for any code path that has caught
|
|
13
|
+
* an Error and wants to log the full underlying detail.
|
|
14
|
+
* 3. When `DEBUG_FETCH=1`, wraps `globalThis.fetch` to log any
|
|
15
|
+
* rejection with the full cause chain. This is the surface where
|
|
16
|
+
* TLS / DNS / connection errors actually originate; the SDK's
|
|
17
|
+
* stringification loses them, so we capture before the SDK does.
|
|
18
|
+
*
|
|
19
|
+
* All output goes to `process.stderr` as JSON lines so
|
|
20
|
+
* `docker logs <container> | jq` works.
|
|
21
|
+
*/
|
|
22
|
+
function asErrorLike(v) {
|
|
23
|
+
if (v === null || v === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (typeof v !== "object")
|
|
26
|
+
return undefined;
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
export function formatErrorChain(input) {
|
|
30
|
+
const chain = [];
|
|
31
|
+
let stack;
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const visit = (v, depth) => {
|
|
34
|
+
if (depth > 10)
|
|
35
|
+
return;
|
|
36
|
+
const e = asErrorLike(v);
|
|
37
|
+
if (!e || seen.has(v))
|
|
38
|
+
return;
|
|
39
|
+
seen.add(v);
|
|
40
|
+
const entry = {};
|
|
41
|
+
if (e.name !== undefined)
|
|
42
|
+
entry.name = e.name;
|
|
43
|
+
if (e.code !== undefined)
|
|
44
|
+
entry.code = e.code;
|
|
45
|
+
if (e.message !== undefined)
|
|
46
|
+
entry.message = e.message;
|
|
47
|
+
chain.push(entry);
|
|
48
|
+
if (stack === undefined && typeof e.stack === "string")
|
|
49
|
+
stack = e.stack;
|
|
50
|
+
if (e.cause !== undefined)
|
|
51
|
+
visit(e.cause, depth + 1);
|
|
52
|
+
if (Array.isArray(e.errors)) {
|
|
53
|
+
for (const inner of e.errors)
|
|
54
|
+
visit(inner, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
visit(input, 0);
|
|
58
|
+
if (chain.length === 0) {
|
|
59
|
+
return { message: typeof input === "string" ? input : JSON.stringify(input), chain: [] };
|
|
60
|
+
}
|
|
61
|
+
const message = chain
|
|
62
|
+
.map((c) => [c.name, c.code, c.message].filter(Boolean).join(": "))
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(" → ");
|
|
65
|
+
const result = { message, chain };
|
|
66
|
+
if (stack !== undefined)
|
|
67
|
+
result.stack = stack;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function writeJson(level, payload) {
|
|
71
|
+
process.stderr.write(`${JSON.stringify({ level, time: new Date().toISOString(), ...payload })}\n`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Install once at server startup. Idempotent — guards against double
|
|
75
|
+
* registration (e.g. tests that spin up the server multiple times in
|
|
76
|
+
* one process).
|
|
77
|
+
*/
|
|
78
|
+
let installed = false;
|
|
79
|
+
export function installDiagnostics() {
|
|
80
|
+
if (installed)
|
|
81
|
+
return;
|
|
82
|
+
installed = true;
|
|
83
|
+
process.on("unhandledRejection", (reason) => {
|
|
84
|
+
const f = formatErrorChain(reason);
|
|
85
|
+
writeJson("error", {
|
|
86
|
+
msg: "unhandledRejection",
|
|
87
|
+
error: f.message,
|
|
88
|
+
chain: f.chain,
|
|
89
|
+
stack: f.stack,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
process.on("uncaughtException", (err) => {
|
|
93
|
+
const f = formatErrorChain(err);
|
|
94
|
+
writeJson("error", {
|
|
95
|
+
msg: "uncaughtException",
|
|
96
|
+
error: f.message,
|
|
97
|
+
chain: f.chain,
|
|
98
|
+
stack: f.stack,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
if (process.env.DEBUG_FETCH === "1") {
|
|
102
|
+
const orig = globalThis.fetch;
|
|
103
|
+
globalThis.fetch = async (input, init) => {
|
|
104
|
+
const url = typeof input === "string"
|
|
105
|
+
? input
|
|
106
|
+
: input instanceof URL
|
|
107
|
+
? input.href
|
|
108
|
+
: input.url;
|
|
109
|
+
try {
|
|
110
|
+
const res = await orig(input, init);
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
writeJson("warn", {
|
|
113
|
+
msg: "fetch non-2xx",
|
|
114
|
+
url,
|
|
115
|
+
method: init?.method ?? "GET",
|
|
116
|
+
status: res.status,
|
|
117
|
+
statusText: res.statusText,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const f = formatErrorChain(err);
|
|
124
|
+
writeJson("error", {
|
|
125
|
+
msg: "fetch threw",
|
|
126
|
+
url,
|
|
127
|
+
method: init?.method ?? "GET",
|
|
128
|
+
error: f.message,
|
|
129
|
+
chain: f.chain,
|
|
130
|
+
});
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
writeJson("info", { msg: "DEBUG_FETCH enabled — wrapping globalThis.fetch" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"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"}
|