@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.
Files changed (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +364 -364
  3. package/dist/cli.js +32 -32
  4. package/dist/client/assets/{CodeViewer-B4nxYc0g.js → CodeViewer-DRxmEzh1.js} +1 -1
  5. package/dist/client/assets/{TerminalPanel-htr2dU1I.js → TerminalPanel-BcGKwlLZ.js} +1 -1
  6. package/dist/client/assets/{index-BjUH4a8R.js → index-BZE2v69K.js} +149 -141
  7. package/dist/client/favicon.svg +11 -11
  8. package/dist/client/index.html +17 -17
  9. package/dist/client/manifest.webmanifest +24 -24
  10. package/dist/config.js +72 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/pi-web-plugins/info/package.json +9 -9
  13. package/dist/pi-web-plugins/info/pi-web-plugin.js +14 -14
  14. package/dist/pi-web-plugins/updates/package.json +9 -9
  15. package/dist/pi-web-plugins/updates/pi-web-plugin.js +75 -75
  16. package/dist/pi-web-plugins/workspace-tasks/config.js +1 -1
  17. package/dist/pi-web-plugins/workspace-tasks/package.json +9 -9
  18. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +10 -10
  19. package/dist/pi-web-plugins/workspace-tasks/taskRunner.js +1 -1
  20. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +58 -58
  21. package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +1 -1
  22. package/dist/server/app.js +48 -17
  23. package/dist/server/app.js.map +1 -1
  24. package/dist/server/configRoutes.js +77 -0
  25. package/dist/server/configRoutes.js.map +1 -1
  26. package/dist/server/gitRoutes.js +16 -3
  27. package/dist/server/gitRoutes.js.map +1 -1
  28. package/dist/server/managementEmbed.js +205 -0
  29. package/dist/server/managementEmbed.js.map +1 -0
  30. package/dist/server/sessiond/sessionProxyRoutes.js +66 -8
  31. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  32. package/dist/server/sessions/managementPermissionSystem.js +94 -0
  33. package/dist/server/sessions/managementPermissionSystem.js.map +1 -0
  34. package/dist/server/sessions/managementSandbox.js +156 -0
  35. package/dist/server/sessions/managementSandbox.js.map +1 -0
  36. package/dist/server/sessions/piSessionService.js +320 -26
  37. package/dist/server/sessions/piSessionService.js.map +1 -1
  38. package/dist/server/sessions/sessionRoutes.js +9 -4
  39. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  40. package/dist/server/terminalProxyRoutes.js +54 -8
  41. package/dist/server/terminalProxyRoutes.js.map +1 -1
  42. package/dist/server/terminals/terminalRoutes.js +12 -3
  43. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  44. package/dist/server/terminals/terminalService.js +48 -4
  45. package/dist/server/terminals/terminalService.js.map +1 -1
  46. package/dist/server/workspaceExplorerRoutes.js +17 -4
  47. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  48. package/dist/server/workspaces/pathSafety.js +9 -2
  49. package/dist/server/workspaces/pathSafety.js.map +1 -1
  50. package/dist/sessiond/sessionDaemonClient.js +12 -12
  51. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  52. package/dist/shared/apiTypes.d.ts +18 -0
  53. package/docs/assets/favicon.svg +11 -11
  54. package/docs/plugins.md +762 -762
  55. package/extensions/pi-web.ts +133 -133
  56. package/install.sh +5 -5
  57. package/package.json +127 -127
  58. package/plugin-api/unstable.d.ts +1 -1
  59. package/plugin-api.d.ts +1 -1
@@ -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,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
- 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 };
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
- function createPiWebEditToolDefinition(cwd) {
33
- const editTool = createEditToolDefinition(cwd);
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 runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
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");