@chainingintention/pi-web-cn 1.202606.4 → 1.202606.6
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 +1 -1
- package/dist/client/assets/{CodeViewer-BNKhIElN.js → CodeViewer-8znVN61S.js} +1 -1
- package/dist/client/assets/{TerminalPanel-VPiiPQfC.js → TerminalPanel-DrdWnF1y.js} +1 -1
- package/dist/client/assets/index-BiGrW6IC.js +2169 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/plugin-api.d.ts +17 -11
- package/dist/server/app.js +55 -17
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +77 -0
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/gitRoutes.js +16 -3
- package/dist/server/gitRoutes.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +179 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -0
- package/dist/server/machines/machineProxyRoutes.js +1 -0
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/managementEmbed.js +205 -0
- package/dist/server/managementEmbed.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +66 -8
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessions/managementPermissionSystem.js +94 -0
- package/dist/server/sessions/managementPermissionSystem.js.map +1 -0
- package/dist/server/sessions/managementSandbox.js +156 -0
- package/dist/server/sessions/managementSandbox.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +339 -31
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +2 -0
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +9 -4
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/terminalProxyRoutes.js +64 -8
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +23 -3
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/terminals/terminalService.js +54 -4
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +103 -4
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileOperationService.js +95 -0
- package/dist/server/workspaces/fileOperationService.js.map +1 -0
- package/dist/server/workspaces/fileUploadService.js +23 -0
- package/dist/server/workspaces/fileUploadService.js.map +1 -0
- package/dist/server/workspaces/pathSafety.js +9 -2
- package/dist/server/workspaces/pathSafety.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +127 -0
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -0
- package/dist/sessiond/sessionDaemonClient.js +12 -12
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +30 -0
- package/dist/shared/federatedRoutes.js +9 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +41 -0
- package/dist/shared/machinePluginIds.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +12 -0
- package/dist/shared/workspaceDeletion.js.map +1 -0
- package/docs/plugins.md +88 -12
- package/package.json +1 -1
- package/dist/client/assets/index-Csx3hC75.js +0 -1994
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access as fsAccess, mkdir as fsMkdir, readFile, readdir as fsReaddir, realpath as fsRealpath, stat as fsStat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "typebox";
|
|
3
7
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
4
8
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
5
9
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
@@ -7,12 +11,27 @@ import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
|
7
11
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
8
12
|
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
9
13
|
import { computeEditPreview } from "./editPreview.js";
|
|
14
|
+
import { managementToolAllowed } from "../managementEmbed.js";
|
|
15
|
+
import { bubblewrapUnavailableReason, createBubblewrapPythonInvocation, createManagedPythonFallbackPrelude, createManagedSandboxEnvironment, DEFAULT_BUBBLEWRAP_PATHS } from "./managementSandbox.js";
|
|
16
|
+
import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
|
|
10
17
|
function noop() {
|
|
11
18
|
// Intentionally empty default unsubscribe callback.
|
|
12
19
|
}
|
|
13
20
|
function authLossWarningKey(sessionId, provider, modelId) {
|
|
14
21
|
return `${sessionId}:${provider}/${modelId}`;
|
|
15
22
|
}
|
|
23
|
+
function requirePromptText(value) {
|
|
24
|
+
if (typeof value !== "string")
|
|
25
|
+
throw new Error("Prompt text is required");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function parsePromptStreamingBehavior(value) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (value === "steer" || value === "followUp")
|
|
32
|
+
return value;
|
|
33
|
+
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
34
|
+
}
|
|
16
35
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
17
36
|
if (!(options.sessionManager instanceof SessionManager))
|
|
18
37
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
@@ -20,17 +39,47 @@ function defaultCreateAgentRuntime(createRuntime, options) {
|
|
|
20
39
|
}
|
|
21
40
|
function createDefaultRuntimeFactory(authStorage, modelRegistry) {
|
|
22
41
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
return withRuntimeCreationEnvironment({}, async () => {
|
|
43
|
+
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
44
|
+
const customTools = [createPiWebEditToolDefinition(cwd)];
|
|
45
|
+
const options = sessionStartEvent === undefined
|
|
46
|
+
? { services, sessionManager, customTools }
|
|
47
|
+
: { services, sessionManager, sessionStartEvent, customTools };
|
|
48
|
+
const result = await createAgentSessionFromServices(options);
|
|
49
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createManagementRuntimeFactory(authStorage, modelRegistry, managementContext) {
|
|
54
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
55
|
+
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
56
|
+
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
57
|
+
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
58
|
+
const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
|
|
59
|
+
const options = sessionStartEvent === undefined
|
|
60
|
+
? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
|
|
61
|
+
: { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
|
|
62
|
+
// @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
|
|
63
|
+
const result = await createAgentSessionFromServices(options);
|
|
64
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
65
|
+
});
|
|
30
66
|
};
|
|
31
67
|
}
|
|
32
|
-
|
|
33
|
-
|
|
68
|
+
export { managementAgentToolNames };
|
|
69
|
+
export function createManagementSandboxToolDefinitions(cwd, context) {
|
|
70
|
+
const operations = createManagedFileOperations(cwd);
|
|
71
|
+
return [
|
|
72
|
+
createReadToolDefinition(cwd, { operations: operations.read }),
|
|
73
|
+
createWriteToolDefinition(cwd, { operations: operations.write }),
|
|
74
|
+
createPiWebEditToolDefinition(cwd, operations.edit),
|
|
75
|
+
createLsToolDefinition(cwd, { operations: operations.ls }),
|
|
76
|
+
createGrepToolDefinition(cwd, { operations: operations.grep }),
|
|
77
|
+
createFindToolDefinition(cwd, { operations: operations.find }),
|
|
78
|
+
createManagedPythonToolDefinition(cwd, context),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
function createPiWebEditToolDefinition(cwd, operations) {
|
|
82
|
+
const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
|
|
34
83
|
return defineTool({
|
|
35
84
|
name: editTool.name,
|
|
36
85
|
label: editTool.label,
|
|
@@ -50,6 +99,211 @@ function createPiWebEditToolDefinition(cwd) {
|
|
|
50
99
|
},
|
|
51
100
|
});
|
|
52
101
|
}
|
|
102
|
+
const pythonSchema = Type.Object({
|
|
103
|
+
code: Type.String({ description: "Python code to run in the managed project workspace" }),
|
|
104
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
|
|
105
|
+
});
|
|
106
|
+
function createManagedPythonToolDefinition(cwd, context) {
|
|
107
|
+
return defineTool({
|
|
108
|
+
name: "python",
|
|
109
|
+
label: "python",
|
|
110
|
+
description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
|
|
111
|
+
promptSnippet: "Run Python code in the current project",
|
|
112
|
+
promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
|
|
113
|
+
parameters: pythonSchema,
|
|
114
|
+
async execute(_toolCallId, params, signal) {
|
|
115
|
+
const configuredPython = context.sandbox?.pythonExecutable?.trim();
|
|
116
|
+
const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
|
|
117
|
+
const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
|
|
118
|
+
const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
|
|
119
|
+
const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
|
|
120
|
+
const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
|
|
121
|
+
return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function runManagedPython(options) {
|
|
126
|
+
const root = await fsRealpath(options.cwd);
|
|
127
|
+
const invocation = createBubblewrapPythonInvocation({
|
|
128
|
+
bubblewrapExecutable: options.bubblewrapExecutable,
|
|
129
|
+
pythonExecutable: options.pythonExecutable,
|
|
130
|
+
workspaceRoot: root,
|
|
131
|
+
env: options.env,
|
|
132
|
+
readOnlyPaths: await readableBubblewrapPaths(),
|
|
133
|
+
});
|
|
134
|
+
const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
135
|
+
const unavailable = bubblewrapUnavailableReason(result.output);
|
|
136
|
+
if (unavailable !== undefined) {
|
|
137
|
+
return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
|
|
138
|
+
}
|
|
139
|
+
return pythonToolResult(result.code, result.output);
|
|
140
|
+
}
|
|
141
|
+
async function runManagedPythonFallback(options) {
|
|
142
|
+
const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
|
|
143
|
+
const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
144
|
+
return pythonToolResult(result.code, result.output);
|
|
145
|
+
}
|
|
146
|
+
async function runPythonProcess(options) {
|
|
147
|
+
return new Promise((resolvePromise, reject) => {
|
|
148
|
+
if (options.signal?.aborted === true) {
|
|
149
|
+
reject(new Error("Operation aborted"));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
153
|
+
let stdout = "";
|
|
154
|
+
let stderr = "";
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
child.kill();
|
|
157
|
+
reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
|
|
158
|
+
}, options.timeoutMs);
|
|
159
|
+
const onAbort = () => {
|
|
160
|
+
child.kill();
|
|
161
|
+
reject(new Error("Operation aborted"));
|
|
162
|
+
};
|
|
163
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
165
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
166
|
+
child.on("error", (error) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
169
|
+
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
170
|
+
reject(new Error("Python sandbox is unavailable"));
|
|
171
|
+
else
|
|
172
|
+
reject(error);
|
|
173
|
+
});
|
|
174
|
+
child.on("close", (codeValue) => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
177
|
+
const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
|
|
178
|
+
resolvePromise({ code: codeValue, output });
|
|
179
|
+
});
|
|
180
|
+
child.stdin.end(options.code);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function pythonToolResult(codeValue, output) {
|
|
184
|
+
const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
|
|
185
|
+
return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
|
|
186
|
+
}
|
|
187
|
+
async function readableBubblewrapPaths() {
|
|
188
|
+
const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
|
|
189
|
+
try {
|
|
190
|
+
await fsAccess(path, constants.R_OK);
|
|
191
|
+
return path;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}));
|
|
197
|
+
return paths.filter(isDefined);
|
|
198
|
+
}
|
|
199
|
+
function createManagedFileOperations(cwd) {
|
|
200
|
+
const read = {
|
|
201
|
+
readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
|
|
202
|
+
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
|
|
203
|
+
};
|
|
204
|
+
const write = {
|
|
205
|
+
writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
|
|
206
|
+
mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
|
|
207
|
+
};
|
|
208
|
+
const edit = {
|
|
209
|
+
readFile: read.readFile,
|
|
210
|
+
writeFile: write.writeFile,
|
|
211
|
+
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
|
|
212
|
+
};
|
|
213
|
+
const ls = {
|
|
214
|
+
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
215
|
+
stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
|
|
216
|
+
readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
|
|
217
|
+
};
|
|
218
|
+
const grep = {
|
|
219
|
+
isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
|
|
220
|
+
readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
|
|
221
|
+
};
|
|
222
|
+
const find = {
|
|
223
|
+
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
224
|
+
glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
|
|
225
|
+
};
|
|
226
|
+
return { read, write, edit, ls, grep, find };
|
|
227
|
+
}
|
|
228
|
+
async function pathExistsInside(rootPath, targetPath) {
|
|
229
|
+
try {
|
|
230
|
+
await assertExistingInside(rootPath, targetPath);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function managedGlob(rootPath, pattern, searchPath, limit) {
|
|
238
|
+
const root = await fsRealpath(rootPath);
|
|
239
|
+
const start = await assertExistingInside(root, searchPath);
|
|
240
|
+
const regex = globPatternToRegExp(pattern);
|
|
241
|
+
const results = [];
|
|
242
|
+
async function walk(dir) {
|
|
243
|
+
if (results.length >= limit)
|
|
244
|
+
return;
|
|
245
|
+
const entries = await fsReaddir(dir, { withFileTypes: true });
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (results.length >= limit)
|
|
248
|
+
return;
|
|
249
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
250
|
+
continue;
|
|
251
|
+
const fullPath = resolve(dir, entry.name);
|
|
252
|
+
const safePath = await assertExistingInside(root, fullPath);
|
|
253
|
+
const rel = relative(start, safePath).split(sep).join("/");
|
|
254
|
+
if (entry.isDirectory()) {
|
|
255
|
+
await walk(safePath);
|
|
256
|
+
}
|
|
257
|
+
else if (regex.test(rel)) {
|
|
258
|
+
results.push(safePath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
await walk(start);
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
function globPatternToRegExp(pattern) {
|
|
266
|
+
const escaped = pattern
|
|
267
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
268
|
+
.replace(/\*\*/g, "\0")
|
|
269
|
+
.replace(/\*/g, "[^/]*")
|
|
270
|
+
.replace(/\?/g, "[^/]")
|
|
271
|
+
.replace(/\0/g, ".*");
|
|
272
|
+
return new RegExp(`^${escaped}$`);
|
|
273
|
+
}
|
|
274
|
+
async function assertExistingInside(rootPath, targetPath) {
|
|
275
|
+
const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
|
|
276
|
+
ensurePathInside(root, target);
|
|
277
|
+
return target;
|
|
278
|
+
}
|
|
279
|
+
async function assertWritableInside(rootPath, targetPath) {
|
|
280
|
+
try {
|
|
281
|
+
return await assertExistingInside(rootPath, targetPath);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
const parent = await assertExistingInside(rootPath, dirname(targetPath));
|
|
285
|
+
const target = resolve(parent, basename(targetPath));
|
|
286
|
+
ensurePathInside(await fsRealpath(rootPath), target);
|
|
287
|
+
return target;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function assertPotentialInside(rootPath, targetPath) {
|
|
291
|
+
const root = await fsRealpath(rootPath);
|
|
292
|
+
const target = resolve(targetPath);
|
|
293
|
+
ensurePathInside(root, target);
|
|
294
|
+
return target;
|
|
295
|
+
}
|
|
296
|
+
function ensurePathInside(root, target) {
|
|
297
|
+
const rel = relative(root, target);
|
|
298
|
+
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
|
|
299
|
+
return;
|
|
300
|
+
throw new Error("Path is outside the managed project sandbox");
|
|
301
|
+
}
|
|
302
|
+
function truncateToolOutput(value, limit = 64_000) {
|
|
303
|
+
if (value.length <= limit)
|
|
304
|
+
return value;
|
|
305
|
+
return `${value.slice(0, limit)}\n[output truncated]`;
|
|
306
|
+
}
|
|
53
307
|
export class PiSessionService {
|
|
54
308
|
constructor(events, deps = {}) {
|
|
55
309
|
this.events = events;
|
|
@@ -58,6 +312,7 @@ export class PiSessionService {
|
|
|
58
312
|
this.compactionPromptQueues = new Map();
|
|
59
313
|
this.compactionDrainTimers = new Map();
|
|
60
314
|
this.authLossWarnings = new Set();
|
|
315
|
+
this.managementContexts = new Map();
|
|
61
316
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
62
317
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
63
318
|
this.sessionManager = deps.sessionManager ?? SessionManager;
|
|
@@ -88,6 +343,7 @@ export class PiSessionService {
|
|
|
88
343
|
this.activities.clear();
|
|
89
344
|
this.compactionPromptQueues.clear();
|
|
90
345
|
this.authLossWarnings.clear();
|
|
346
|
+
this.managementContexts.clear();
|
|
91
347
|
await Promise.all(activeSessions.map(async (active) => {
|
|
92
348
|
active.unsubscribe();
|
|
93
349
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -110,9 +366,11 @@ export class PiSessionService {
|
|
|
110
366
|
.filter(isDefined);
|
|
111
367
|
return [...unarchivedSessions, ...archivedSessions];
|
|
112
368
|
}
|
|
113
|
-
async start(cwd) {
|
|
114
|
-
const active = await this.create(this.sessionManager.create(cwd), cwd);
|
|
369
|
+
async start(cwd, managementContext) {
|
|
370
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, managementContext);
|
|
115
371
|
const { session } = active.runtime;
|
|
372
|
+
if (managementContext !== undefined)
|
|
373
|
+
this.managementContexts.set(session.sessionId, managementContext);
|
|
116
374
|
return {
|
|
117
375
|
id: session.sessionId,
|
|
118
376
|
path: session.sessionFile ?? "",
|
|
@@ -200,22 +458,24 @@ export class PiSessionService {
|
|
|
200
458
|
}
|
|
201
459
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
202
460
|
}
|
|
203
|
-
async prompt(sessionId, text, streamingBehavior) {
|
|
461
|
+
async prompt(sessionId, text, streamingBehavior, managementContext) {
|
|
462
|
+
const promptText = requirePromptText(text);
|
|
463
|
+
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
204
464
|
await this.assertWritable(sessionId);
|
|
205
|
-
const session = await this.getOrOpen(sessionId);
|
|
206
|
-
this.maybeGenerateSessionName(session,
|
|
465
|
+
const session = await this.getOrOpen(sessionId, managementContext);
|
|
466
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
207
467
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
208
|
-
const behavior = isQueued ?
|
|
209
|
-
if (isQueued && this.hasQueuedMessageText(session,
|
|
468
|
+
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
469
|
+
if (isQueued && this.hasQueuedMessageText(session, promptText)) {
|
|
210
470
|
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
211
471
|
this.publishStatus(session);
|
|
212
472
|
return;
|
|
213
473
|
}
|
|
214
474
|
if (session.isCompacting) {
|
|
215
|
-
this.enqueuePromptDuringCompaction(session,
|
|
475
|
+
this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp");
|
|
216
476
|
return;
|
|
217
477
|
}
|
|
218
|
-
void this.submitPrompt(session,
|
|
478
|
+
void this.submitPrompt(session, promptText, behavior);
|
|
219
479
|
}
|
|
220
480
|
submitPrompt(session, text, behavior) {
|
|
221
481
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
@@ -236,8 +496,12 @@ export class PiSessionService {
|
|
|
236
496
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
237
497
|
this.publishStatus(session);
|
|
238
498
|
}
|
|
239
|
-
async shell(sessionId, text) {
|
|
499
|
+
async shell(sessionId, text, managementContext) {
|
|
240
500
|
await this.assertWritable(sessionId);
|
|
501
|
+
this.assertManagedSessionAccess(sessionId, managementContext);
|
|
502
|
+
const effectiveContext = managementContext ?? this.managementContexts.get(sessionId);
|
|
503
|
+
if (effectiveContext !== undefined && !managementToolAllowed(effectiveContext, "shell"))
|
|
504
|
+
throw new Error("Shell commands are disabled in management embed mode");
|
|
241
505
|
const active = await this.getActive(sessionId);
|
|
242
506
|
const { session } = active.runtime;
|
|
243
507
|
const isExcluded = text.startsWith("!!");
|
|
@@ -271,8 +535,9 @@ export class PiSessionService {
|
|
|
271
535
|
this.publishStatus(session);
|
|
272
536
|
});
|
|
273
537
|
}
|
|
274
|
-
async runCommand(sessionId, text) {
|
|
538
|
+
async runCommand(sessionId, text, managementContext) {
|
|
275
539
|
await this.assertWritable(sessionId);
|
|
540
|
+
this.assertManagedSessionAccess(sessionId, managementContext);
|
|
276
541
|
return this.commandService.run(sessionId, text);
|
|
277
542
|
}
|
|
278
543
|
async respondToCommand(sessionId, requestId, value) {
|
|
@@ -285,8 +550,23 @@ export class PiSessionService {
|
|
|
285
550
|
throw new Error("Stop current session activity before archiving");
|
|
286
551
|
const archiveInput = await this.archiveInputForSession(session);
|
|
287
552
|
await this.closeActive(session.sessionId);
|
|
553
|
+
this.managementContexts.delete(session.sessionId);
|
|
288
554
|
await this.archiveStore.archive(archiveInput);
|
|
289
555
|
}
|
|
556
|
+
assertManagedSessionAccess(sessionId, context) {
|
|
557
|
+
const existing = this.managementContexts.get(sessionId);
|
|
558
|
+
if (context === undefined || existing === undefined)
|
|
559
|
+
return;
|
|
560
|
+
if (existing.user.rootUserId !== context.user.rootUserId)
|
|
561
|
+
throw new Error("Session is outside the managed embed authorization scope");
|
|
562
|
+
}
|
|
563
|
+
rememberManagedSessionAccess(sessionId, context) {
|
|
564
|
+
if (context === undefined)
|
|
565
|
+
return;
|
|
566
|
+
this.assertManagedSessionAccess(sessionId, context);
|
|
567
|
+
if (!this.managementContexts.has(sessionId))
|
|
568
|
+
this.managementContexts.set(sessionId, context);
|
|
569
|
+
}
|
|
290
570
|
async archiveTree(sessionId) {
|
|
291
571
|
const session = await this.getOrOpen(sessionId);
|
|
292
572
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
@@ -417,6 +697,7 @@ export class PiSessionService {
|
|
|
417
697
|
if (!active)
|
|
418
698
|
return;
|
|
419
699
|
this.active.delete(sessionId);
|
|
700
|
+
this.managementContexts.delete(sessionId);
|
|
420
701
|
this.activities.delete(sessionId);
|
|
421
702
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
422
703
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -434,23 +715,41 @@ export class PiSessionService {
|
|
|
434
715
|
if (await this.archiveStore.isArchived(sessionId))
|
|
435
716
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
436
717
|
}
|
|
437
|
-
async getOrOpen(sessionId) {
|
|
438
|
-
return (await this.getActive(sessionId)).runtime.session;
|
|
718
|
+
async getOrOpen(sessionId, managementContext) {
|
|
719
|
+
return (await this.getActive(sessionId, managementContext)).runtime.session;
|
|
439
720
|
}
|
|
440
|
-
async getActive(sessionId) {
|
|
721
|
+
async getActive(sessionId, managementContext) {
|
|
441
722
|
const active = this.active.get(sessionId);
|
|
442
|
-
if (active)
|
|
723
|
+
if (active) {
|
|
724
|
+
const activeSessionId = active.runtime.session.sessionId;
|
|
725
|
+
const existingContext = this.managementContexts.get(activeSessionId);
|
|
726
|
+
if (managementContext !== undefined && existingContext === undefined) {
|
|
727
|
+
const sessionFile = active.runtime.session.sessionFile;
|
|
728
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
729
|
+
throw new Error("Managed embed session must be persisted before it can be resumed safely");
|
|
730
|
+
const activeCwd = active.runtime.session.sessionManager.getCwd();
|
|
731
|
+
await this.closeActive(activeSessionId);
|
|
732
|
+
return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
|
|
733
|
+
}
|
|
734
|
+
this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
|
|
443
735
|
return active;
|
|
736
|
+
}
|
|
444
737
|
const archived = await this.archiveStore.get(sessionId);
|
|
445
738
|
if (archived?.archivePath !== undefined)
|
|
446
|
-
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
|
|
739
|
+
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext);
|
|
447
740
|
const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
|
|
448
741
|
if (!match)
|
|
449
742
|
throw new Error("Session not found");
|
|
450
|
-
return this.create(this.sessionManager.open(match.path), match.cwd);
|
|
451
|
-
}
|
|
452
|
-
async create(sessionManager, cwd) {
|
|
453
|
-
const
|
|
743
|
+
return this.create(this.sessionManager.open(match.path), match.cwd, managementContext);
|
|
744
|
+
}
|
|
745
|
+
async create(sessionManager, cwd, managementContext) {
|
|
746
|
+
const createRuntime = managementContext === undefined
|
|
747
|
+
? this.createRuntime
|
|
748
|
+
: createManagementRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, managementContext);
|
|
749
|
+
const runtimeOptions = managementContext === undefined
|
|
750
|
+
? { cwd, agentDir: this.agentDir, sessionManager }
|
|
751
|
+
: { cwd, agentDir: this.agentDir, sessionManager, managementContext };
|
|
752
|
+
const runtime = await this.createAgentRuntime(createRuntime, runtimeOptions);
|
|
454
753
|
const active = { runtime, unsubscribe: noop };
|
|
455
754
|
this.bindRuntime(active);
|
|
456
755
|
runtime.setRebindSession(() => {
|
|
@@ -458,6 +757,7 @@ export class PiSessionService {
|
|
|
458
757
|
return Promise.resolve();
|
|
459
758
|
});
|
|
460
759
|
this.active.set(runtime.session.sessionId, active);
|
|
760
|
+
this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
|
|
461
761
|
this.publishStatus(runtime.session);
|
|
462
762
|
return active;
|
|
463
763
|
}
|
|
@@ -467,6 +767,11 @@ export class PiSessionService {
|
|
|
467
767
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
468
768
|
if (candidate === active) {
|
|
469
769
|
this.active.delete(sessionId);
|
|
770
|
+
const context = this.managementContexts.get(sessionId);
|
|
771
|
+
if (context !== undefined && sessionId !== session.sessionId) {
|
|
772
|
+
this.managementContexts.delete(sessionId);
|
|
773
|
+
this.managementContexts.set(session.sessionId, context);
|
|
774
|
+
}
|
|
470
775
|
if (sessionId !== session.sessionId)
|
|
471
776
|
this.clearCompactionPromptQueue(sessionId);
|
|
472
777
|
}
|
|
@@ -890,6 +1195,9 @@ function archivedTimestamp(record) {
|
|
|
890
1195
|
function isDefined(value) {
|
|
891
1196
|
return value !== undefined;
|
|
892
1197
|
}
|
|
1198
|
+
function isNodeErrorWithCode(error, code) {
|
|
1199
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1200
|
+
}
|
|
893
1201
|
async function clearParentSession(sessionFile) {
|
|
894
1202
|
const content = await readFile(sessionFile, "utf8");
|
|
895
1203
|
const newlineIndex = content.indexOf("\n");
|