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,354 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as nodePty from "node-pty";
|
|
3
|
+
import { config } from "./config.js";
|
|
4
|
+
/** Rolling output buffer cap per PTY (in bytes). 256 KB ≈ ~3000 lines of typical shell output. */
|
|
5
|
+
const OUTPUT_BUFFER_BYTES = 256 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Time a detached PTY (no WS attached) is held alive before being reaped.
|
|
8
|
+
* Sourced from `config.terminalIdleReapMs` so operators in resource-
|
|
9
|
+
* constrained deployments can shorten the window, AND so the integration
|
|
10
|
+
* test can pin it down to ~200 ms to actually exercise the reap path
|
|
11
|
+
* within a test budget.
|
|
12
|
+
*/
|
|
13
|
+
const IDLE_REAP_MS = config.terminalIdleReapMs;
|
|
14
|
+
const ptys = new Map();
|
|
15
|
+
function defaultShell() {
|
|
16
|
+
return process.env.SHELL ?? "/bin/sh";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find an existing PTY for `tabId` within `projectId`. Returns
|
|
20
|
+
* undefined if none exists (caller should spawn) or if a PTY with
|
|
21
|
+
* that tabId belongs to a DIFFERENT project (caller should treat as
|
|
22
|
+
* "no match" — never reattach across projects, that would expose
|
|
23
|
+
* one project's shell to another).
|
|
24
|
+
*/
|
|
25
|
+
export function findPtyByTabId(tabId, projectId) {
|
|
26
|
+
for (const entry of ptys.values()) {
|
|
27
|
+
if (entry.managed.tabId !== tabId)
|
|
28
|
+
continue;
|
|
29
|
+
if (entry.managed.projectId !== projectId)
|
|
30
|
+
continue;
|
|
31
|
+
return entry.managed;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function spawnPty(opts) {
|
|
36
|
+
const shell = opts.shell ?? defaultShell();
|
|
37
|
+
const cols = opts.cols ?? 80;
|
|
38
|
+
const rows = opts.rows ?? 24;
|
|
39
|
+
const env = opts.env ?? process.env;
|
|
40
|
+
const proc = nodePty.spawn(shell, [], {
|
|
41
|
+
name: "xterm-color",
|
|
42
|
+
cols,
|
|
43
|
+
rows,
|
|
44
|
+
cwd: opts.cwd,
|
|
45
|
+
env: filterEnv(env),
|
|
46
|
+
});
|
|
47
|
+
const ptyId = randomUUID();
|
|
48
|
+
const managed = {
|
|
49
|
+
ptyId,
|
|
50
|
+
tabId: opts.tabId,
|
|
51
|
+
projectId: opts.projectId,
|
|
52
|
+
process: proc,
|
|
53
|
+
cwd: opts.cwd,
|
|
54
|
+
};
|
|
55
|
+
const entry = {
|
|
56
|
+
managed,
|
|
57
|
+
dataDisposable: undefined,
|
|
58
|
+
closeActiveSocket: undefined,
|
|
59
|
+
idleTimer: undefined,
|
|
60
|
+
buffer: [],
|
|
61
|
+
bufferBytes: 0,
|
|
62
|
+
};
|
|
63
|
+
ptys.set(ptyId, entry);
|
|
64
|
+
// Always-on output capture, independent of any attached socket —
|
|
65
|
+
// that way disconnected periods still accumulate the rolling
|
|
66
|
+
// buffer for the next reattach to replay.
|
|
67
|
+
const captureDisposable = proc.onData((chunk) => {
|
|
68
|
+
appendToBuffer(entry, chunk);
|
|
69
|
+
});
|
|
70
|
+
proc.onExit(() => {
|
|
71
|
+
captureDisposable.dispose();
|
|
72
|
+
ptys.delete(ptyId);
|
|
73
|
+
if (entry.idleTimer !== undefined)
|
|
74
|
+
clearTimeout(entry.idleTimer);
|
|
75
|
+
});
|
|
76
|
+
return managed;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Attach a socket-style sink to a managed PTY. Replays the rolling
|
|
80
|
+
* output buffer immediately, then forwards every subsequent
|
|
81
|
+
* `onData` chunk to `onData(chunk)`. Returns a detach function the
|
|
82
|
+
* caller MUST invoke on socket close — without this, the prior
|
|
83
|
+
* sink keeps receiving bytes and a reattach can't replace it.
|
|
84
|
+
*
|
|
85
|
+
* Cancels any pending idle reaper — the PTY is back in active use.
|
|
86
|
+
*
|
|
87
|
+
* `replayBytes` lets the caller request only the tail of the
|
|
88
|
+
* buffer (e.g. xterm already has prior scrollback locally and only
|
|
89
|
+
* wants the last ~16 KB). Pass `Infinity` (default) to replay all.
|
|
90
|
+
*/
|
|
91
|
+
export function attachSink(ptyId, onData, replayBytes = OUTPUT_BUFFER_BYTES, closeActiveSocket) {
|
|
92
|
+
const entry = ptys.get(ptyId);
|
|
93
|
+
if (entry === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (entry.idleTimer !== undefined) {
|
|
96
|
+
clearTimeout(entry.idleTimer);
|
|
97
|
+
entry.idleTimer = undefined;
|
|
98
|
+
}
|
|
99
|
+
// Replace any existing data sink so a stale reconnect never
|
|
100
|
+
// double-delivers chunks. The previous detach() the caller
|
|
101
|
+
// captured still works (it disposes whatever disposable it
|
|
102
|
+
// captured), but the route should always call the latest detach.
|
|
103
|
+
if (entry.dataDisposable !== undefined) {
|
|
104
|
+
entry.dataDisposable.dispose();
|
|
105
|
+
entry.dataDisposable = undefined;
|
|
106
|
+
}
|
|
107
|
+
// Close the previously-attached WS, if any. Without this, two
|
|
108
|
+
// browsers with the same tabId both keep their input handlers wired
|
|
109
|
+
// up to write to the PTY; only the most recently attached one sees
|
|
110
|
+
// output, but BOTH can send input — interleaved keystrokes corrupt
|
|
111
|
+
// line-edit state. Closing the predecessor's WS lets the route's
|
|
112
|
+
// close handler unwire the input listener cleanly.
|
|
113
|
+
if (entry.closeActiveSocket !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
entry.closeActiveSocket();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// socket already gone — fine
|
|
119
|
+
}
|
|
120
|
+
entry.closeActiveSocket = undefined;
|
|
121
|
+
}
|
|
122
|
+
entry.closeActiveSocket = closeActiveSocket;
|
|
123
|
+
// Replay only the tail the caller asked for. The buffer is a
|
|
124
|
+
// chunk array; flatten just enough from the right edge to hit
|
|
125
|
+
// `replayBytes`. Edge case: replayBytes <= 0 → skip replay.
|
|
126
|
+
if (replayBytes > 0 && entry.bufferBytes > 0) {
|
|
127
|
+
let remaining = Math.min(replayBytes, entry.bufferBytes);
|
|
128
|
+
const tail = [];
|
|
129
|
+
for (let i = entry.buffer.length - 1; i >= 0 && remaining > 0; i--) {
|
|
130
|
+
const chunk = entry.buffer[i];
|
|
131
|
+
if (chunk.byteLength <= remaining) {
|
|
132
|
+
tail.unshift(chunk);
|
|
133
|
+
remaining -= chunk.byteLength;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
tail.unshift(chunk.subarray(chunk.byteLength - remaining));
|
|
137
|
+
remaining = 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const chunk of tail)
|
|
141
|
+
onData(chunk.toString("utf8"));
|
|
142
|
+
}
|
|
143
|
+
const live = entry.managed.process.onData((chunk) => {
|
|
144
|
+
onData(chunk);
|
|
145
|
+
});
|
|
146
|
+
entry.dataDisposable = live;
|
|
147
|
+
return () => {
|
|
148
|
+
if (entry.dataDisposable === live) {
|
|
149
|
+
live.dispose();
|
|
150
|
+
entry.dataDisposable = undefined;
|
|
151
|
+
// Only clear closeActiveSocket if WE are still the active
|
|
152
|
+
// attachment — a newer attachSink may have already swapped a
|
|
153
|
+
// different socket in.
|
|
154
|
+
if (entry.closeActiveSocket === closeActiveSocket) {
|
|
155
|
+
entry.closeActiveSocket = undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// A newer attach replaced ours; nothing to do.
|
|
160
|
+
}
|
|
161
|
+
// Start the idle reaper. If a fresh attach arrives within
|
|
162
|
+
// IDLE_REAP_MS the timer is cancelled in the next attachSink.
|
|
163
|
+
if (entry.idleTimer === undefined && ptys.has(ptyId)) {
|
|
164
|
+
entry.idleTimer = setTimeout(() => {
|
|
165
|
+
entry.idleTimer = undefined;
|
|
166
|
+
killPty(ptyId);
|
|
167
|
+
}, IDLE_REAP_MS);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function appendToBuffer(entry, chunk) {
|
|
172
|
+
const buf = Buffer.from(chunk, "utf8");
|
|
173
|
+
entry.buffer.push(buf);
|
|
174
|
+
entry.bufferBytes += buf.byteLength;
|
|
175
|
+
// Evict from the front until we're under cap. Keeps memory
|
|
176
|
+
// bounded across multi-hour shells running noisy output (npm
|
|
177
|
+
// install, pip install, etc.).
|
|
178
|
+
while (entry.bufferBytes > OUTPUT_BUFFER_BYTES && entry.buffer.length > 0) {
|
|
179
|
+
const head = entry.buffer.shift();
|
|
180
|
+
entry.bufferBytes -= head.byteLength;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function getPty(ptyId) {
|
|
184
|
+
return ptys.get(ptyId)?.managed;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Grace window between SIGTERM and SIGKILL. Long enough for a
|
|
188
|
+
* well-behaved shell to clean up (zsh trap, bash exit handler), short
|
|
189
|
+
* enough that an unkillable shell doesn't linger past a deploy /
|
|
190
|
+
* shutdown for noticeable time.
|
|
191
|
+
*/
|
|
192
|
+
const SIGKILL_GRACE_MS = 2_000;
|
|
193
|
+
export function killPty(ptyId) {
|
|
194
|
+
const entry = ptys.get(ptyId);
|
|
195
|
+
if (entry === undefined)
|
|
196
|
+
return false;
|
|
197
|
+
ptys.delete(ptyId);
|
|
198
|
+
if (entry.idleTimer !== undefined)
|
|
199
|
+
clearTimeout(entry.idleTimer);
|
|
200
|
+
if (entry.dataDisposable !== undefined)
|
|
201
|
+
entry.dataDisposable.dispose();
|
|
202
|
+
// Try SIGTERM first (gives the shell a chance to flush, run trap
|
|
203
|
+
// handlers, release tty). Schedule a SIGKILL fallback for trapped
|
|
204
|
+
// / unresponsive shells. Without the fallback, a `trap '' TERM`
|
|
205
|
+
// bash leaves an orphan process holding the PTY fd.
|
|
206
|
+
let killed = false;
|
|
207
|
+
const exitDisposable = entry.managed.process.onExit(() => {
|
|
208
|
+
killed = true;
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
entry.managed.process.kill("SIGTERM");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// already exited between get + kill; nothing to do
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
exitDisposable.dispose();
|
|
219
|
+
if (killed)
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
entry.managed.process.kill("SIGKILL");
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// already exited
|
|
226
|
+
}
|
|
227
|
+
}, SIGKILL_GRACE_MS).unref();
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
export function ptyCount() {
|
|
231
|
+
return ptys.size;
|
|
232
|
+
}
|
|
233
|
+
export function disposeAllPtys() {
|
|
234
|
+
for (const ptyId of Array.from(ptys.keys())) {
|
|
235
|
+
killPty(ptyId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let exitHandlerInstalled = false;
|
|
239
|
+
/**
|
|
240
|
+
* Install a `process.on("exit")` last-resort SIGTERM-all handler.
|
|
241
|
+
* Idempotent.
|
|
242
|
+
*
|
|
243
|
+
* The previous version of this module called `installExitHandler()`
|
|
244
|
+
* at module load, which meant any unit test that imported the module
|
|
245
|
+
* also installed the handler — and the handler couldn't be undone.
|
|
246
|
+
* Tests that fork child processes ended up with the wrong handler
|
|
247
|
+
* count and unpredictable shutdown behavior. The fix: require an
|
|
248
|
+
* explicit `installPtyExitHandler()` call from `index.ts` (the
|
|
249
|
+
* production entry point); tests that import `pty-manager` for unit
|
|
250
|
+
* coverage skip the install.
|
|
251
|
+
*/
|
|
252
|
+
export function installPtyExitHandler() {
|
|
253
|
+
if (exitHandlerInstalled)
|
|
254
|
+
return;
|
|
255
|
+
exitHandlerInstalled = true;
|
|
256
|
+
process.on("exit", () => {
|
|
257
|
+
for (const entry of ptys.values()) {
|
|
258
|
+
try {
|
|
259
|
+
entry.managed.process.kill("SIGTERM");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Process already gone — fine. We're in the exit handler so
|
|
263
|
+
// there's no point trying anything more aggressive.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
ptys.clear();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Allowlist of env-var names the PTY shell (and the `!` exec route)
|
|
271
|
+
* may inherit from the pi-forge process. Everything else is dropped.
|
|
272
|
+
*
|
|
273
|
+
* Allowlist instead of denylist because the threat model is "any
|
|
274
|
+
* secret an operator has in their host env leaks into the shell." A
|
|
275
|
+
* denylist of named secrets is fail-open — every newly-named provider
|
|
276
|
+
* key (`SOMEPROVIDER_API_KEY`), kube credential, cloud token, etc.
|
|
277
|
+
* leaks until we add it by name. An allowlist is fail-safe: anything
|
|
278
|
+
* not on this list is hidden by default; operators with a legitimate
|
|
279
|
+
* need pass specific vars through via `TERMINAL_PASSTHROUGH_ENV`.
|
|
280
|
+
*
|
|
281
|
+
* What's on the list and why:
|
|
282
|
+
* PATH, HOME, USER, LOGNAME, SHELL — required for a usable shell
|
|
283
|
+
* PWD — the shell sets it but inheriting
|
|
284
|
+
* avoids a transient empty value
|
|
285
|
+
* TERM, COLORTERM — xterm rendering / 256-color
|
|
286
|
+
* TERMINFO — fallback terminfo lookup
|
|
287
|
+
* LANG, LC_* — locale (UTF-8, sorting, dates)
|
|
288
|
+
* TZ — timezone-aware tools (`date`)
|
|
289
|
+
* HOSTNAME — prompt customization (`\h` in PS1)
|
|
290
|
+
* EDITOR, PAGER, VISUAL — interactive defaults (git
|
|
291
|
+
* commit, less, etc.)
|
|
292
|
+
*
|
|
293
|
+
* Conspicuously NOT on the list (must be opt-in via
|
|
294
|
+
* `TERMINAL_PASSTHROUGH_ENV` if the operator wants them in-shell):
|
|
295
|
+
* - pi-forge secrets: JWT_SECRET, API_KEY, UI_PASSWORD
|
|
296
|
+
* - Provider keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
|
|
297
|
+
* - Cloud credentials: AWS_*, GOOGLE_APPLICATION_CREDENTIALS,
|
|
298
|
+
* GH_TOKEN, GITHUB_TOKEN, KUBECONFIG, ...
|
|
299
|
+
* - Anything else — assume it's sensitive until proven harmless.
|
|
300
|
+
*/
|
|
301
|
+
const TERMINAL_ENV_ALLOWLIST = new Set([
|
|
302
|
+
"PATH",
|
|
303
|
+
"HOME",
|
|
304
|
+
"USER",
|
|
305
|
+
"LOGNAME",
|
|
306
|
+
"SHELL",
|
|
307
|
+
"PWD",
|
|
308
|
+
"TERM",
|
|
309
|
+
"COLORTERM",
|
|
310
|
+
"TERMINFO",
|
|
311
|
+
"LANG",
|
|
312
|
+
"TZ",
|
|
313
|
+
"HOSTNAME",
|
|
314
|
+
"EDITOR",
|
|
315
|
+
"PAGER",
|
|
316
|
+
"VISUAL",
|
|
317
|
+
]);
|
|
318
|
+
/**
|
|
319
|
+
* Variable names matching this prefix-style pattern are also allowed
|
|
320
|
+
* — covers the locale family (`LC_ALL`, `LC_CTYPE`, `LC_MESSAGES`, …)
|
|
321
|
+
* without enumerating every variant.
|
|
322
|
+
*/
|
|
323
|
+
const TERMINAL_ENV_ALLOWLIST_PREFIXES = ["LC_"];
|
|
324
|
+
function isAllowedByDefault(name) {
|
|
325
|
+
if (TERMINAL_ENV_ALLOWLIST.has(name))
|
|
326
|
+
return true;
|
|
327
|
+
for (const prefix of TERMINAL_ENV_ALLOWLIST_PREFIXES) {
|
|
328
|
+
if (name.startsWith(prefix))
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function filterEnv(env) {
|
|
334
|
+
const passthrough = new Set(config.terminalPassthroughEnv);
|
|
335
|
+
const out = {};
|
|
336
|
+
for (const [k, v] of Object.entries(env)) {
|
|
337
|
+
if (typeof v !== "string")
|
|
338
|
+
continue;
|
|
339
|
+
if (!isAllowedByDefault(k) && !passthrough.has(k))
|
|
340
|
+
continue;
|
|
341
|
+
out[k] = v;
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Re-export so non-PTY callers (the user-bash `!` exec route) get the
|
|
347
|
+
* same env allowlist without having to re-implement it. The PTY and
|
|
348
|
+
* the one-shot user-bash share an identical threat model: an
|
|
349
|
+
* authenticated browser user must not be able to `echo
|
|
350
|
+
* $JWT_SECRET` (or any other host-env secret) from a shell the
|
|
351
|
+
* pi-forge spawned on their behalf.
|
|
352
|
+
*/
|
|
353
|
+
export const scrubbedEnv = () => filterEnv(process.env);
|
|
354
|
+
//# sourceMappingURL=pty-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pty-manager.js","sourceRoot":"","sources":["../src/pty-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared response schemas used across multiple route files. Extracting these
|
|
3
|
+
* keeps the wire shape consistent — e.g. /sessions, /sessions/:id, and /fork
|
|
4
|
+
* all return the same liveSummary fields rather than each route declaring its
|
|
5
|
+
* own subset.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Standard error envelope for 4xx/5xx responses. `error` is required so
|
|
9
|
+
* generated SDK clients can rely on its presence (no extra null-check).
|
|
10
|
+
* `message` is optional context for callers that want a human-readable hint.
|
|
11
|
+
*/
|
|
12
|
+
export const errorSchema = {
|
|
13
|
+
type: "object",
|
|
14
|
+
required: ["error"],
|
|
15
|
+
properties: {
|
|
16
|
+
error: { type: "string" },
|
|
17
|
+
message: { type: "string" },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Live session summary — the shape returned by routes that produce a single
|
|
22
|
+
* session metadata object (POST /sessions, GET /sessions/:id, POST /fork).
|
|
23
|
+
* `name` is optional because not every session has a user-defined display
|
|
24
|
+
* name; consumers should treat its absence as "no name set."
|
|
25
|
+
*/
|
|
26
|
+
export const liveSummarySchema = {
|
|
27
|
+
type: "object",
|
|
28
|
+
required: [
|
|
29
|
+
"sessionId",
|
|
30
|
+
"projectId",
|
|
31
|
+
"workspacePath",
|
|
32
|
+
"createdAt",
|
|
33
|
+
"lastActivityAt",
|
|
34
|
+
"isLive",
|
|
35
|
+
"messageCount",
|
|
36
|
+
"isStreaming",
|
|
37
|
+
],
|
|
38
|
+
properties: {
|
|
39
|
+
sessionId: { type: "string" },
|
|
40
|
+
projectId: { type: "string" },
|
|
41
|
+
workspacePath: { type: "string" },
|
|
42
|
+
createdAt: { type: "string", format: "date-time" },
|
|
43
|
+
lastActivityAt: { type: "string", format: "date-time" },
|
|
44
|
+
isLive: { type: "boolean" },
|
|
45
|
+
name: { type: "string" },
|
|
46
|
+
messageCount: { type: "integer", minimum: 0 },
|
|
47
|
+
isStreaming: { type: "boolean" },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Build a wire-shaped LiveSession summary, omitting `name` when unset so the
|
|
52
|
+
* serializer doesn't emit an explicit undefined.
|
|
53
|
+
*
|
|
54
|
+
* `isLive` defaults to `true` because most callers (POST /sessions, /fork,
|
|
55
|
+
* /sessions/:id when in-memory) are returning a live session. Disk-only
|
|
56
|
+
* callers should pass `isLive: false` explicitly.
|
|
57
|
+
*/
|
|
58
|
+
export function liveSummaryBody(args) {
|
|
59
|
+
const out = {
|
|
60
|
+
sessionId: args.sessionId,
|
|
61
|
+
projectId: args.projectId,
|
|
62
|
+
workspacePath: args.workspacePath,
|
|
63
|
+
createdAt: args.createdAt.toISOString(),
|
|
64
|
+
lastActivityAt: args.lastActivityAt.toISOString(),
|
|
65
|
+
isLive: args.isLive ?? true,
|
|
66
|
+
messageCount: args.messageCount,
|
|
67
|
+
isStreaming: args.isStreaming,
|
|
68
|
+
};
|
|
69
|
+
if (args.name !== undefined)
|
|
70
|
+
out.name = args.name;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=_schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../src/routes/_schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,OAAO,CAAC;IACnB,UAAU,EAAE;QACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC5B;CACO,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE;QACR,WAAW;QACX,WAAW;QACX,eAAe;QACf,WAAW;QACX,gBAAgB;QAChB,QAAQ;QACR,cAAc;QACd,aAAa;KACd;IACD,UAAU,EAAE;QACV,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC7B,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC7B,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACjC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QAClD,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QACvD,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QAC3B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACxB,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC7C,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;KACjC;CACO,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAU/B;IACC,MAAM,GAAG,GAA4B;QACnC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,WAAW,EAAE;QACjD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;QAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { config, authEnabled } from "../config.js";
|
|
2
|
+
import { extractBearer, generateToken, passwordConfigured, persistPassword, verifyPasswordWithSource, verifyToken, } from "../auth.js";
|
|
3
|
+
import { errorSchema } from "./_schemas.js";
|
|
4
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
5
|
+
const MAX_PASSWORD_LENGTH = 1024;
|
|
6
|
+
export const authRoutes = async (fastify) => {
|
|
7
|
+
fastify.get("/auth/status", {
|
|
8
|
+
config: { public: true },
|
|
9
|
+
schema: {
|
|
10
|
+
description: "Returns whether auth is required to call protected routes.",
|
|
11
|
+
tags: ["auth"],
|
|
12
|
+
security: [],
|
|
13
|
+
response: {
|
|
14
|
+
200: {
|
|
15
|
+
type: "object",
|
|
16
|
+
required: ["authEnabled"],
|
|
17
|
+
properties: {
|
|
18
|
+
authEnabled: { type: "boolean" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}, async () => ({ authEnabled: authEnabled() }));
|
|
24
|
+
fastify.post("/auth/login", {
|
|
25
|
+
config: {
|
|
26
|
+
public: true,
|
|
27
|
+
rateLimit: {
|
|
28
|
+
max: config.auth.loginRateLimitMax,
|
|
29
|
+
timeWindow: config.auth.loginRateLimitWindowMs,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
schema: {
|
|
33
|
+
description: "Exchange a password for a short-lived JWT. Returns 401 if the password is wrong, " +
|
|
34
|
+
"or 503 if browser password auth is not configured (no UI_PASSWORD set and no " +
|
|
35
|
+
"stored password hash). When the password matched the env-supplied " +
|
|
36
|
+
"UI_PASSWORD AND no stored hash exists yet, `mustChangePassword` is true on " +
|
|
37
|
+
"the response and the issued token may only call POST /auth/change-password.",
|
|
38
|
+
tags: ["auth"],
|
|
39
|
+
security: [],
|
|
40
|
+
body: {
|
|
41
|
+
type: "object",
|
|
42
|
+
required: ["password"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
password: { type: "string", minLength: 1, maxLength: MAX_PASSWORD_LENGTH },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
response: {
|
|
49
|
+
200: {
|
|
50
|
+
type: "object",
|
|
51
|
+
required: ["token", "expiresAt", "mustChangePassword"],
|
|
52
|
+
properties: {
|
|
53
|
+
token: { type: "string" },
|
|
54
|
+
expiresAt: { type: "string", format: "date-time" },
|
|
55
|
+
mustChangePassword: { type: "boolean" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
401: errorSchema,
|
|
59
|
+
503: errorSchema,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}, async (req, reply) => {
|
|
63
|
+
if (!passwordConfigured()) {
|
|
64
|
+
return reply.code(503).send({
|
|
65
|
+
error: "ui_password_not_configured",
|
|
66
|
+
message: "browser login is disabled (no UI_PASSWORD set and no stored password hash)",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const { password } = req.body;
|
|
70
|
+
const result = await verifyPasswordWithSource(password);
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
return reply.code(401).send({
|
|
73
|
+
error: "invalid_password",
|
|
74
|
+
message: "the password did not match",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const mustChangePassword = result.source === "env" && config.auth.requirePasswordChange;
|
|
78
|
+
const issued = generateToken({ mustChangePassword });
|
|
79
|
+
return { ...issued, mustChangePassword };
|
|
80
|
+
});
|
|
81
|
+
// Change-password is `public: true` at the route-config level so the
|
|
82
|
+
// global `must_change_password` gate (in index.ts) doesn't refuse a
|
|
83
|
+
// token that was issued specifically to call THIS endpoint. We
|
|
84
|
+
// enforce auth manually inside the handler.
|
|
85
|
+
fastify.post("/auth/change-password", {
|
|
86
|
+
config: {
|
|
87
|
+
public: true,
|
|
88
|
+
rateLimit: {
|
|
89
|
+
max: config.auth.loginRateLimitMax,
|
|
90
|
+
timeWindow: config.auth.loginRateLimitWindowMs,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
schema: {
|
|
94
|
+
description: "Verify the current password, persist a new scrypt hash to " +
|
|
95
|
+
"${FORGE_DATA_DIR}/password-hash, and issue a fresh JWT " +
|
|
96
|
+
"(mustChangePassword=false). Once a stored hash exists the env " +
|
|
97
|
+
"UI_PASSWORD is ignored on subsequent logins. Requires a valid " +
|
|
98
|
+
"JWT (initial-login `mustChangePassword:true` tokens are accepted).",
|
|
99
|
+
tags: ["auth"],
|
|
100
|
+
body: {
|
|
101
|
+
type: "object",
|
|
102
|
+
required: ["currentPassword", "newPassword"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties: {
|
|
105
|
+
currentPassword: {
|
|
106
|
+
type: "string",
|
|
107
|
+
minLength: 1,
|
|
108
|
+
maxLength: MAX_PASSWORD_LENGTH,
|
|
109
|
+
},
|
|
110
|
+
newPassword: {
|
|
111
|
+
type: "string",
|
|
112
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
113
|
+
maxLength: MAX_PASSWORD_LENGTH,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
response: {
|
|
118
|
+
200: {
|
|
119
|
+
type: "object",
|
|
120
|
+
required: ["token", "expiresAt", "mustChangePassword"],
|
|
121
|
+
properties: {
|
|
122
|
+
token: { type: "string" },
|
|
123
|
+
expiresAt: { type: "string", format: "date-time" },
|
|
124
|
+
mustChangePassword: { type: "boolean" },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
400: errorSchema,
|
|
128
|
+
401: errorSchema,
|
|
129
|
+
503: errorSchema,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}, async (req, reply) => {
|
|
133
|
+
// Manual auth check — the route is `public: true` so the global
|
|
134
|
+
// hook doesn't run, but we still require a valid JWT here.
|
|
135
|
+
const presented = extractBearer(req.headers.authorization);
|
|
136
|
+
if (presented === undefined || verifyToken(presented) === undefined) {
|
|
137
|
+
return reply.code(401).send({ error: "auth_required" });
|
|
138
|
+
}
|
|
139
|
+
if (!passwordConfigured()) {
|
|
140
|
+
return reply.code(503).send({
|
|
141
|
+
error: "ui_password_not_configured",
|
|
142
|
+
message: "password auth is not configured on this server",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const { currentPassword, newPassword } = req.body;
|
|
146
|
+
const verify = await verifyPasswordWithSource(currentPassword);
|
|
147
|
+
if (!verify.ok) {
|
|
148
|
+
return reply.code(401).send({
|
|
149
|
+
error: "invalid_password",
|
|
150
|
+
message: "the current password did not match",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (currentPassword === newPassword) {
|
|
154
|
+
return reply.code(400).send({
|
|
155
|
+
error: "password_unchanged",
|
|
156
|
+
message: "new password must differ from the current one",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
await persistPassword(newPassword);
|
|
160
|
+
const issued = generateToken({ mustChangePassword: false });
|
|
161
|
+
return { ...issued, mustChangePassword: false };
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
|