@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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/client/assets/{CodeViewer-BNKhIElN.js → CodeViewer-8znVN61S.js} +1 -1
  3. package/dist/client/assets/{TerminalPanel-VPiiPQfC.js → TerminalPanel-DrdWnF1y.js} +1 -1
  4. package/dist/client/assets/index-BiGrW6IC.js +2169 -0
  5. package/dist/client/index.html +1 -1
  6. package/dist/config.js +72 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/plugin-api.d.ts +17 -11
  9. package/dist/server/app.js +55 -17
  10. package/dist/server/app.js.map +1 -1
  11. package/dist/server/configRoutes.js +77 -0
  12. package/dist/server/configRoutes.js.map +1 -1
  13. package/dist/server/gitRoutes.js +16 -3
  14. package/dist/server/gitRoutes.js.map +1 -1
  15. package/dist/server/machines/machinePluginProxyRoutes.js +179 -0
  16. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -0
  17. package/dist/server/machines/machineProxyRoutes.js +1 -0
  18. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  19. package/dist/server/managementEmbed.js +205 -0
  20. package/dist/server/managementEmbed.js.map +1 -0
  21. package/dist/server/sessiond/sessionProxyRoutes.js +66 -8
  22. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  23. package/dist/server/sessions/managementPermissionSystem.js +94 -0
  24. package/dist/server/sessions/managementPermissionSystem.js.map +1 -0
  25. package/dist/server/sessions/managementSandbox.js +156 -0
  26. package/dist/server/sessions/managementSandbox.js.map +1 -0
  27. package/dist/server/sessions/piSessionService.js +339 -31
  28. package/dist/server/sessions/piSessionService.js.map +1 -1
  29. package/dist/server/sessions/sessionNameGenerator.js +2 -0
  30. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  31. package/dist/server/sessions/sessionRoutes.js +9 -4
  32. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  33. package/dist/server/terminalProxyRoutes.js +64 -8
  34. package/dist/server/terminalProxyRoutes.js.map +1 -1
  35. package/dist/server/terminals/terminalRoutes.js +23 -3
  36. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  37. package/dist/server/terminals/terminalService.js +54 -4
  38. package/dist/server/terminals/terminalService.js.map +1 -1
  39. package/dist/server/workspaceExplorerRoutes.js +103 -4
  40. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  41. package/dist/server/workspaces/fileOperationService.js +95 -0
  42. package/dist/server/workspaces/fileOperationService.js.map +1 -0
  43. package/dist/server/workspaces/fileUploadService.js +23 -0
  44. package/dist/server/workspaces/fileUploadService.js.map +1 -0
  45. package/dist/server/workspaces/pathSafety.js +9 -2
  46. package/dist/server/workspaces/pathSafety.js.map +1 -1
  47. package/dist/server/workspaces/workspaceDeletionRoutes.js +127 -0
  48. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -0
  49. package/dist/sessiond/sessionDaemonClient.js +12 -12
  50. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  51. package/dist/shared/apiTypes.d.ts +30 -0
  52. package/dist/shared/federatedRoutes.js +9 -0
  53. package/dist/shared/federatedRoutes.js.map +1 -1
  54. package/dist/shared/machinePluginIds.js +41 -0
  55. package/dist/shared/machinePluginIds.js.map +1 -0
  56. package/dist/shared/workspaceDeletion.js +12 -0
  57. package/dist/shared/workspaceDeletion.js.map +1 -0
  58. package/docs/plugins.md +88 -12
  59. package/package.json +1 -1
  60. package/dist/client/assets/index-Csx3hC75.js +0 -1994
@@ -1,5 +1,9 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
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
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
24
- const customTools = [createPiWebEditToolDefinition(cwd)];
25
- const options = sessionStartEvent === undefined
26
- ? { services, sessionManager, customTools }
27
- : { services, sessionManager, sessionStartEvent, customTools };
28
- const result = await createAgentSessionFromServices(options);
29
- return { ...result, services, diagnostics: services.diagnostics };
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
- function createPiWebEditToolDefinition(cwd) {
33
- const editTool = createEditToolDefinition(cwd);
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, text);
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 ? streamingBehavior ?? "followUp" : undefined;
209
- if (isQueued && this.hasQueuedMessageText(session, text)) {
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, text, behavior ?? "followUp");
475
+ this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp");
216
476
  return;
217
477
  }
218
- void this.submitPrompt(session, text, behavior);
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 runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
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");