@chainingintention/pi-web-cn 1.202606.3 → 1.202606.5
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 -21
- package/README.md +364 -364
- package/dist/cli.js +32 -32
- package/dist/client/assets/{CodeViewer-B4nxYc0g.js → CodeViewer-DRxmEzh1.js} +1 -1
- package/dist/client/assets/{TerminalPanel-htr2dU1I.js → TerminalPanel-BcGKwlLZ.js} +1 -1
- package/dist/client/assets/{index-BjUH4a8R.js → index-BZE2v69K.js} +149 -141
- package/dist/client/favicon.svg +11 -11
- package/dist/client/index.html +17 -17
- package/dist/client/manifest.webmanifest +24 -24
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/info/package.json +9 -9
- package/dist/pi-web-plugins/info/pi-web-plugin.js +14 -14
- package/dist/pi-web-plugins/updates/package.json +9 -9
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +75 -75
- package/dist/pi-web-plugins/workspace-tasks/config.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/package.json +9 -9
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +10 -10
- package/dist/pi-web-plugins/workspace-tasks/taskRunner.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +58 -58
- package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +1 -1
- package/dist/server/app.js +48 -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/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 +320 -26
- package/dist/server/sessions/piSessionService.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 +54 -8
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +12 -3
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/terminals/terminalService.js +48 -4
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +17 -4
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/pathSafety.js +9 -2
- package/dist/server/workspaces/pathSafety.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +12 -12
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +18 -0
- package/docs/assets/favicon.svg +11 -11
- package/docs/plugins.md +762 -762
- package/extensions/pi-web.ts +133 -133
- package/install.sh +5 -5
- package/package.json +127 -127
- package/plugin-api/unstable.d.ts +1 -1
- package/plugin-api.d.ts +1 -1
|
@@ -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,6 +11,9 @@ 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
|
}
|
|
@@ -20,17 +27,47 @@ function defaultCreateAgentRuntime(createRuntime, options) {
|
|
|
20
27
|
}
|
|
21
28
|
function createDefaultRuntimeFactory(authStorage, modelRegistry) {
|
|
22
29
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
return withRuntimeCreationEnvironment({}, async () => {
|
|
31
|
+
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
32
|
+
const customTools = [createPiWebEditToolDefinition(cwd)];
|
|
33
|
+
const options = sessionStartEvent === undefined
|
|
34
|
+
? { services, sessionManager, customTools }
|
|
35
|
+
: { services, sessionManager, sessionStartEvent, customTools };
|
|
36
|
+
const result = await createAgentSessionFromServices(options);
|
|
37
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createManagementRuntimeFactory(authStorage, modelRegistry, managementContext) {
|
|
42
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
43
|
+
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
44
|
+
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
45
|
+
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
46
|
+
const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
|
|
47
|
+
const options = sessionStartEvent === undefined
|
|
48
|
+
? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
|
|
49
|
+
: { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
|
|
50
|
+
// @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
|
|
51
|
+
const result = await createAgentSessionFromServices(options);
|
|
52
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
53
|
+
});
|
|
30
54
|
};
|
|
31
55
|
}
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
export { managementAgentToolNames };
|
|
57
|
+
export function createManagementSandboxToolDefinitions(cwd, context) {
|
|
58
|
+
const operations = createManagedFileOperations(cwd);
|
|
59
|
+
return [
|
|
60
|
+
createReadToolDefinition(cwd, { operations: operations.read }),
|
|
61
|
+
createWriteToolDefinition(cwd, { operations: operations.write }),
|
|
62
|
+
createPiWebEditToolDefinition(cwd, operations.edit),
|
|
63
|
+
createLsToolDefinition(cwd, { operations: operations.ls }),
|
|
64
|
+
createGrepToolDefinition(cwd, { operations: operations.grep }),
|
|
65
|
+
createFindToolDefinition(cwd, { operations: operations.find }),
|
|
66
|
+
createManagedPythonToolDefinition(cwd, context),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
function createPiWebEditToolDefinition(cwd, operations) {
|
|
70
|
+
const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
|
|
34
71
|
return defineTool({
|
|
35
72
|
name: editTool.name,
|
|
36
73
|
label: editTool.label,
|
|
@@ -50,6 +87,211 @@ function createPiWebEditToolDefinition(cwd) {
|
|
|
50
87
|
},
|
|
51
88
|
});
|
|
52
89
|
}
|
|
90
|
+
const pythonSchema = Type.Object({
|
|
91
|
+
code: Type.String({ description: "Python code to run in the managed project workspace" }),
|
|
92
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
|
|
93
|
+
});
|
|
94
|
+
function createManagedPythonToolDefinition(cwd, context) {
|
|
95
|
+
return defineTool({
|
|
96
|
+
name: "python",
|
|
97
|
+
label: "python",
|
|
98
|
+
description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
|
|
99
|
+
promptSnippet: "Run Python code in the current project",
|
|
100
|
+
promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
|
|
101
|
+
parameters: pythonSchema,
|
|
102
|
+
async execute(_toolCallId, params, signal) {
|
|
103
|
+
const configuredPython = context.sandbox?.pythonExecutable?.trim();
|
|
104
|
+
const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
|
|
105
|
+
const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
|
|
106
|
+
const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
|
|
107
|
+
const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
|
|
108
|
+
const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
|
|
109
|
+
return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function runManagedPython(options) {
|
|
114
|
+
const root = await fsRealpath(options.cwd);
|
|
115
|
+
const invocation = createBubblewrapPythonInvocation({
|
|
116
|
+
bubblewrapExecutable: options.bubblewrapExecutable,
|
|
117
|
+
pythonExecutable: options.pythonExecutable,
|
|
118
|
+
workspaceRoot: root,
|
|
119
|
+
env: options.env,
|
|
120
|
+
readOnlyPaths: await readableBubblewrapPaths(),
|
|
121
|
+
});
|
|
122
|
+
const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
123
|
+
const unavailable = bubblewrapUnavailableReason(result.output);
|
|
124
|
+
if (unavailable !== undefined) {
|
|
125
|
+
return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
|
|
126
|
+
}
|
|
127
|
+
return pythonToolResult(result.code, result.output);
|
|
128
|
+
}
|
|
129
|
+
async function runManagedPythonFallback(options) {
|
|
130
|
+
const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
|
|
131
|
+
const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
132
|
+
return pythonToolResult(result.code, result.output);
|
|
133
|
+
}
|
|
134
|
+
async function runPythonProcess(options) {
|
|
135
|
+
return new Promise((resolvePromise, reject) => {
|
|
136
|
+
if (options.signal?.aborted === true) {
|
|
137
|
+
reject(new Error("Operation aborted"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
141
|
+
let stdout = "";
|
|
142
|
+
let stderr = "";
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
child.kill();
|
|
145
|
+
reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
|
|
146
|
+
}, options.timeoutMs);
|
|
147
|
+
const onAbort = () => {
|
|
148
|
+
child.kill();
|
|
149
|
+
reject(new Error("Operation aborted"));
|
|
150
|
+
};
|
|
151
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
152
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
153
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
154
|
+
child.on("error", (error) => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
157
|
+
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
158
|
+
reject(new Error("Python sandbox is unavailable"));
|
|
159
|
+
else
|
|
160
|
+
reject(error);
|
|
161
|
+
});
|
|
162
|
+
child.on("close", (codeValue) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
165
|
+
const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
|
|
166
|
+
resolvePromise({ code: codeValue, output });
|
|
167
|
+
});
|
|
168
|
+
child.stdin.end(options.code);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function pythonToolResult(codeValue, output) {
|
|
172
|
+
const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
|
|
173
|
+
return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
|
|
174
|
+
}
|
|
175
|
+
async function readableBubblewrapPaths() {
|
|
176
|
+
const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
|
|
177
|
+
try {
|
|
178
|
+
await fsAccess(path, constants.R_OK);
|
|
179
|
+
return path;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
return paths.filter(isDefined);
|
|
186
|
+
}
|
|
187
|
+
function createManagedFileOperations(cwd) {
|
|
188
|
+
const read = {
|
|
189
|
+
readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
|
|
190
|
+
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
|
|
191
|
+
};
|
|
192
|
+
const write = {
|
|
193
|
+
writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
|
|
194
|
+
mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
|
|
195
|
+
};
|
|
196
|
+
const edit = {
|
|
197
|
+
readFile: read.readFile,
|
|
198
|
+
writeFile: write.writeFile,
|
|
199
|
+
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
|
|
200
|
+
};
|
|
201
|
+
const ls = {
|
|
202
|
+
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
203
|
+
stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
|
|
204
|
+
readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
|
|
205
|
+
};
|
|
206
|
+
const grep = {
|
|
207
|
+
isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
|
|
208
|
+
readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
|
|
209
|
+
};
|
|
210
|
+
const find = {
|
|
211
|
+
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
212
|
+
glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
|
|
213
|
+
};
|
|
214
|
+
return { read, write, edit, ls, grep, find };
|
|
215
|
+
}
|
|
216
|
+
async function pathExistsInside(rootPath, targetPath) {
|
|
217
|
+
try {
|
|
218
|
+
await assertExistingInside(rootPath, targetPath);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function managedGlob(rootPath, pattern, searchPath, limit) {
|
|
226
|
+
const root = await fsRealpath(rootPath);
|
|
227
|
+
const start = await assertExistingInside(root, searchPath);
|
|
228
|
+
const regex = globPatternToRegExp(pattern);
|
|
229
|
+
const results = [];
|
|
230
|
+
async function walk(dir) {
|
|
231
|
+
if (results.length >= limit)
|
|
232
|
+
return;
|
|
233
|
+
const entries = await fsReaddir(dir, { withFileTypes: true });
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (results.length >= limit)
|
|
236
|
+
return;
|
|
237
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
238
|
+
continue;
|
|
239
|
+
const fullPath = resolve(dir, entry.name);
|
|
240
|
+
const safePath = await assertExistingInside(root, fullPath);
|
|
241
|
+
const rel = relative(start, safePath).split(sep).join("/");
|
|
242
|
+
if (entry.isDirectory()) {
|
|
243
|
+
await walk(safePath);
|
|
244
|
+
}
|
|
245
|
+
else if (regex.test(rel)) {
|
|
246
|
+
results.push(safePath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await walk(start);
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
function globPatternToRegExp(pattern) {
|
|
254
|
+
const escaped = pattern
|
|
255
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
256
|
+
.replace(/\*\*/g, "\0")
|
|
257
|
+
.replace(/\*/g, "[^/]*")
|
|
258
|
+
.replace(/\?/g, "[^/]")
|
|
259
|
+
.replace(/\0/g, ".*");
|
|
260
|
+
return new RegExp(`^${escaped}$`);
|
|
261
|
+
}
|
|
262
|
+
async function assertExistingInside(rootPath, targetPath) {
|
|
263
|
+
const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
|
|
264
|
+
ensurePathInside(root, target);
|
|
265
|
+
return target;
|
|
266
|
+
}
|
|
267
|
+
async function assertWritableInside(rootPath, targetPath) {
|
|
268
|
+
try {
|
|
269
|
+
return await assertExistingInside(rootPath, targetPath);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
const parent = await assertExistingInside(rootPath, dirname(targetPath));
|
|
273
|
+
const target = resolve(parent, basename(targetPath));
|
|
274
|
+
ensurePathInside(await fsRealpath(rootPath), target);
|
|
275
|
+
return target;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function assertPotentialInside(rootPath, targetPath) {
|
|
279
|
+
const root = await fsRealpath(rootPath);
|
|
280
|
+
const target = resolve(targetPath);
|
|
281
|
+
ensurePathInside(root, target);
|
|
282
|
+
return target;
|
|
283
|
+
}
|
|
284
|
+
function ensurePathInside(root, target) {
|
|
285
|
+
const rel = relative(root, target);
|
|
286
|
+
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
|
|
287
|
+
return;
|
|
288
|
+
throw new Error("Path is outside the managed project sandbox");
|
|
289
|
+
}
|
|
290
|
+
function truncateToolOutput(value, limit = 64_000) {
|
|
291
|
+
if (value.length <= limit)
|
|
292
|
+
return value;
|
|
293
|
+
return `${value.slice(0, limit)}\n[output truncated]`;
|
|
294
|
+
}
|
|
53
295
|
export class PiSessionService {
|
|
54
296
|
constructor(events, deps = {}) {
|
|
55
297
|
this.events = events;
|
|
@@ -58,6 +300,7 @@ export class PiSessionService {
|
|
|
58
300
|
this.compactionPromptQueues = new Map();
|
|
59
301
|
this.compactionDrainTimers = new Map();
|
|
60
302
|
this.authLossWarnings = new Set();
|
|
303
|
+
this.managementContexts = new Map();
|
|
61
304
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
62
305
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
63
306
|
this.sessionManager = deps.sessionManager ?? SessionManager;
|
|
@@ -88,6 +331,7 @@ export class PiSessionService {
|
|
|
88
331
|
this.activities.clear();
|
|
89
332
|
this.compactionPromptQueues.clear();
|
|
90
333
|
this.authLossWarnings.clear();
|
|
334
|
+
this.managementContexts.clear();
|
|
91
335
|
await Promise.all(activeSessions.map(async (active) => {
|
|
92
336
|
active.unsubscribe();
|
|
93
337
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -110,9 +354,11 @@ export class PiSessionService {
|
|
|
110
354
|
.filter(isDefined);
|
|
111
355
|
return [...unarchivedSessions, ...archivedSessions];
|
|
112
356
|
}
|
|
113
|
-
async start(cwd) {
|
|
114
|
-
const active = await this.create(this.sessionManager.create(cwd), cwd);
|
|
357
|
+
async start(cwd, managementContext) {
|
|
358
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, managementContext);
|
|
115
359
|
const { session } = active.runtime;
|
|
360
|
+
if (managementContext !== undefined)
|
|
361
|
+
this.managementContexts.set(session.sessionId, managementContext);
|
|
116
362
|
return {
|
|
117
363
|
id: session.sessionId,
|
|
118
364
|
path: session.sessionFile ?? "",
|
|
@@ -200,9 +446,9 @@ export class PiSessionService {
|
|
|
200
446
|
}
|
|
201
447
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
202
448
|
}
|
|
203
|
-
async prompt(sessionId, text, streamingBehavior) {
|
|
449
|
+
async prompt(sessionId, text, streamingBehavior, managementContext) {
|
|
204
450
|
await this.assertWritable(sessionId);
|
|
205
|
-
const session = await this.getOrOpen(sessionId);
|
|
451
|
+
const session = await this.getOrOpen(sessionId, managementContext);
|
|
206
452
|
this.maybeGenerateSessionName(session, text);
|
|
207
453
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
208
454
|
const behavior = isQueued ? streamingBehavior ?? "followUp" : undefined;
|
|
@@ -236,8 +482,12 @@ export class PiSessionService {
|
|
|
236
482
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
237
483
|
this.publishStatus(session);
|
|
238
484
|
}
|
|
239
|
-
async shell(sessionId, text) {
|
|
485
|
+
async shell(sessionId, text, managementContext) {
|
|
240
486
|
await this.assertWritable(sessionId);
|
|
487
|
+
this.assertManagedSessionAccess(sessionId, managementContext);
|
|
488
|
+
const effectiveContext = managementContext ?? this.managementContexts.get(sessionId);
|
|
489
|
+
if (effectiveContext !== undefined && !managementToolAllowed(effectiveContext, "shell"))
|
|
490
|
+
throw new Error("Shell commands are disabled in management embed mode");
|
|
241
491
|
const active = await this.getActive(sessionId);
|
|
242
492
|
const { session } = active.runtime;
|
|
243
493
|
const isExcluded = text.startsWith("!!");
|
|
@@ -271,8 +521,9 @@ export class PiSessionService {
|
|
|
271
521
|
this.publishStatus(session);
|
|
272
522
|
});
|
|
273
523
|
}
|
|
274
|
-
async runCommand(sessionId, text) {
|
|
524
|
+
async runCommand(sessionId, text, managementContext) {
|
|
275
525
|
await this.assertWritable(sessionId);
|
|
526
|
+
this.assertManagedSessionAccess(sessionId, managementContext);
|
|
276
527
|
return this.commandService.run(sessionId, text);
|
|
277
528
|
}
|
|
278
529
|
async respondToCommand(sessionId, requestId, value) {
|
|
@@ -285,8 +536,23 @@ export class PiSessionService {
|
|
|
285
536
|
throw new Error("Stop current session activity before archiving");
|
|
286
537
|
const archiveInput = await this.archiveInputForSession(session);
|
|
287
538
|
await this.closeActive(session.sessionId);
|
|
539
|
+
this.managementContexts.delete(session.sessionId);
|
|
288
540
|
await this.archiveStore.archive(archiveInput);
|
|
289
541
|
}
|
|
542
|
+
assertManagedSessionAccess(sessionId, context) {
|
|
543
|
+
const existing = this.managementContexts.get(sessionId);
|
|
544
|
+
if (context === undefined || existing === undefined)
|
|
545
|
+
return;
|
|
546
|
+
if (existing.user.rootUserId !== context.user.rootUserId)
|
|
547
|
+
throw new Error("Session is outside the managed embed authorization scope");
|
|
548
|
+
}
|
|
549
|
+
rememberManagedSessionAccess(sessionId, context) {
|
|
550
|
+
if (context === undefined)
|
|
551
|
+
return;
|
|
552
|
+
this.assertManagedSessionAccess(sessionId, context);
|
|
553
|
+
if (!this.managementContexts.has(sessionId))
|
|
554
|
+
this.managementContexts.set(sessionId, context);
|
|
555
|
+
}
|
|
290
556
|
async archiveTree(sessionId) {
|
|
291
557
|
const session = await this.getOrOpen(sessionId);
|
|
292
558
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
@@ -417,6 +683,7 @@ export class PiSessionService {
|
|
|
417
683
|
if (!active)
|
|
418
684
|
return;
|
|
419
685
|
this.active.delete(sessionId);
|
|
686
|
+
this.managementContexts.delete(sessionId);
|
|
420
687
|
this.activities.delete(sessionId);
|
|
421
688
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
422
689
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -434,23 +701,41 @@ export class PiSessionService {
|
|
|
434
701
|
if (await this.archiveStore.isArchived(sessionId))
|
|
435
702
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
436
703
|
}
|
|
437
|
-
async getOrOpen(sessionId) {
|
|
438
|
-
return (await this.getActive(sessionId)).runtime.session;
|
|
704
|
+
async getOrOpen(sessionId, managementContext) {
|
|
705
|
+
return (await this.getActive(sessionId, managementContext)).runtime.session;
|
|
439
706
|
}
|
|
440
|
-
async getActive(sessionId) {
|
|
707
|
+
async getActive(sessionId, managementContext) {
|
|
441
708
|
const active = this.active.get(sessionId);
|
|
442
|
-
if (active)
|
|
709
|
+
if (active) {
|
|
710
|
+
const activeSessionId = active.runtime.session.sessionId;
|
|
711
|
+
const existingContext = this.managementContexts.get(activeSessionId);
|
|
712
|
+
if (managementContext !== undefined && existingContext === undefined) {
|
|
713
|
+
const sessionFile = active.runtime.session.sessionFile;
|
|
714
|
+
if (sessionFile === undefined || sessionFile === "")
|
|
715
|
+
throw new Error("Managed embed session must be persisted before it can be resumed safely");
|
|
716
|
+
const activeCwd = active.runtime.session.sessionManager.getCwd();
|
|
717
|
+
await this.closeActive(activeSessionId);
|
|
718
|
+
return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
|
|
719
|
+
}
|
|
720
|
+
this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
|
|
443
721
|
return active;
|
|
722
|
+
}
|
|
444
723
|
const archived = await this.archiveStore.get(sessionId);
|
|
445
724
|
if (archived?.archivePath !== undefined)
|
|
446
|
-
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
|
|
725
|
+
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext);
|
|
447
726
|
const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
|
|
448
727
|
if (!match)
|
|
449
728
|
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
|
|
729
|
+
return this.create(this.sessionManager.open(match.path), match.cwd, managementContext);
|
|
730
|
+
}
|
|
731
|
+
async create(sessionManager, cwd, managementContext) {
|
|
732
|
+
const createRuntime = managementContext === undefined
|
|
733
|
+
? this.createRuntime
|
|
734
|
+
: createManagementRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, managementContext);
|
|
735
|
+
const runtimeOptions = managementContext === undefined
|
|
736
|
+
? { cwd, agentDir: this.agentDir, sessionManager }
|
|
737
|
+
: { cwd, agentDir: this.agentDir, sessionManager, managementContext };
|
|
738
|
+
const runtime = await this.createAgentRuntime(createRuntime, runtimeOptions);
|
|
454
739
|
const active = { runtime, unsubscribe: noop };
|
|
455
740
|
this.bindRuntime(active);
|
|
456
741
|
runtime.setRebindSession(() => {
|
|
@@ -458,6 +743,7 @@ export class PiSessionService {
|
|
|
458
743
|
return Promise.resolve();
|
|
459
744
|
});
|
|
460
745
|
this.active.set(runtime.session.sessionId, active);
|
|
746
|
+
this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
|
|
461
747
|
this.publishStatus(runtime.session);
|
|
462
748
|
return active;
|
|
463
749
|
}
|
|
@@ -467,6 +753,11 @@ export class PiSessionService {
|
|
|
467
753
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
468
754
|
if (candidate === active) {
|
|
469
755
|
this.active.delete(sessionId);
|
|
756
|
+
const context = this.managementContexts.get(sessionId);
|
|
757
|
+
if (context !== undefined && sessionId !== session.sessionId) {
|
|
758
|
+
this.managementContexts.delete(sessionId);
|
|
759
|
+
this.managementContexts.set(session.sessionId, context);
|
|
760
|
+
}
|
|
470
761
|
if (sessionId !== session.sessionId)
|
|
471
762
|
this.clearCompactionPromptQueue(sessionId);
|
|
472
763
|
}
|
|
@@ -890,6 +1181,9 @@ function archivedTimestamp(record) {
|
|
|
890
1181
|
function isDefined(value) {
|
|
891
1182
|
return value !== undefined;
|
|
892
1183
|
}
|
|
1184
|
+
function isNodeErrorWithCode(error, code) {
|
|
1185
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1186
|
+
}
|
|
893
1187
|
async function clearParentSession(sessionFile) {
|
|
894
1188
|
const content = await readFile(sessionFile, "utf8");
|
|
895
1189
|
const newlineIndex = content.indexOf("\n");
|