@cjhyy/code-shell-server 0.9.8 → 0.9.10

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 (111) hide show
  1. package/README.md +85 -23
  2. package/dist/bin/code-shell-project-runtime.d.ts +2 -0
  3. package/dist/bin/code-shell-project-runtime.js +8 -0
  4. package/dist/desktop-web/http-api.d.ts +43 -0
  5. package/dist/desktop-web/http-api.js +376 -0
  6. package/dist/hub/approval-lease.d.ts +30 -0
  7. package/dist/hub/approval-lease.js +89 -0
  8. package/dist/hub/auth-http.d.ts +23 -0
  9. package/dist/hub/auth-http.js +259 -0
  10. package/dist/hub/auth-store.d.ts +52 -0
  11. package/dist/hub/auth-store.js +316 -0
  12. package/dist/hub/configuration.d.ts +64 -0
  13. package/dist/hub/configuration.js +555 -0
  14. package/dist/hub/files.d.ts +9 -0
  15. package/dist/hub/files.js +278 -0
  16. package/dist/hub/mcp-configuration.d.ts +51 -0
  17. package/dist/hub/mcp-configuration.js +558 -0
  18. package/dist/hub/mcp-probe.d.ts +24 -0
  19. package/dist/hub/mcp-probe.js +392 -0
  20. package/dist/hub/model-probe.d.ts +20 -0
  21. package/dist/hub/model-probe.js +193 -0
  22. package/dist/hub/outbound-transport.d.ts +36 -0
  23. package/dist/hub/outbound-transport.js +122 -0
  24. package/dist/hub/run-replay.d.ts +42 -0
  25. package/dist/hub/run-replay.js +89 -0
  26. package/dist/hub/session-management.d.ts +36 -0
  27. package/dist/hub/session-management.js +405 -0
  28. package/dist/hub/skills-management.d.ts +43 -0
  29. package/dist/hub/skills-management.js +487 -0
  30. package/dist/hub/uploads.d.ts +22 -0
  31. package/dist/hub/uploads.js +241 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.desktop-web.d.ts +2 -0
  34. package/dist/index.desktop-web.js +2 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.links.d.ts +6 -0
  37. package/dist/index.links.js +6 -0
  38. package/dist/index.panels.d.ts +9 -0
  39. package/dist/index.panels.js +8 -0
  40. package/dist/index.serve.d.ts +1 -0
  41. package/dist/index.serve.js +1 -0
  42. package/dist/index.storage.d.ts +2 -0
  43. package/dist/index.storage.js +2 -0
  44. package/dist/links/catalog.d.ts +10 -0
  45. package/dist/links/catalog.js +41 -0
  46. package/dist/links/device-oauth.d.ts +52 -0
  47. package/dist/links/device-oauth.js +255 -0
  48. package/dist/links/http.d.ts +34 -0
  49. package/dist/links/http.js +144 -0
  50. package/dist/links/service.d.ts +54 -0
  51. package/dist/links/service.js +578 -0
  52. package/dist/links/types.d.ts +6 -0
  53. package/dist/links/types.js +1 -0
  54. package/dist/mobile-remote/mobile-client-event-validator.js +5 -2
  55. package/dist/mobile-remote/pending-approvals.js +2 -0
  56. package/dist/mobile-remote/remote-host-manager.d.ts +8 -0
  57. package/dist/mobile-remote/remote-host-manager.js +90 -15
  58. package/dist/mobile-remote/room-manager.js +17 -5
  59. package/dist/panels/agent-task-host.d.ts +32 -0
  60. package/dist/panels/agent-task-host.js +457 -0
  61. package/dist/panels/agent-task-models.d.ts +27 -0
  62. package/dist/panels/agent-task-models.js +51 -0
  63. package/dist/panels/agent-task-service.d.ts +105 -0
  64. package/dist/panels/agent-task-service.js +283 -0
  65. package/dist/panels/http.d.ts +34 -0
  66. package/dist/panels/http.js +37 -0
  67. package/dist/panels/hub-binding.d.ts +12 -0
  68. package/dist/panels/hub-binding.js +174 -0
  69. package/dist/panels/management-http.d.ts +29 -0
  70. package/dist/panels/management-http.js +119 -0
  71. package/dist/panels/management.d.ts +39 -0
  72. package/dist/panels/management.js +494 -0
  73. package/dist/panels/process-files.d.ts +14 -0
  74. package/dist/panels/process-files.js +299 -0
  75. package/dist/panels/process-service.d.ts +96 -0
  76. package/dist/panels/process-service.js +501 -0
  77. package/dist/panels/runtime-services.d.ts +24 -0
  78. package/dist/panels/runtime-services.js +462 -0
  79. package/dist/panels/runtime.d.ts +51 -0
  80. package/dist/panels/runtime.js +1063 -0
  81. package/dist/panels/storage-store.d.ts +9 -0
  82. package/dist/panels/storage-store.js +154 -0
  83. package/dist/panels/types.d.ts +52 -0
  84. package/dist/panels/types.js +1 -0
  85. package/dist/project-runtime/control-server.d.ts +19 -0
  86. package/dist/project-runtime/control-server.js +300 -0
  87. package/dist/project-runtime/docker-provider.d.ts +33 -0
  88. package/dist/project-runtime/docker-provider.js +487 -0
  89. package/dist/project-runtime/lock.d.ts +6 -0
  90. package/dist/project-runtime/lock.js +43 -0
  91. package/dist/project-runtime/managed-entry.d.ts +10 -0
  92. package/dist/project-runtime/managed-entry.js +113 -0
  93. package/dist/project-runtime/manager.d.ts +40 -0
  94. package/dist/project-runtime/manager.js +249 -0
  95. package/dist/project-runtime/proxy.d.ts +22 -0
  96. package/dist/project-runtime/proxy.js +706 -0
  97. package/dist/project-runtime/registry.d.ts +54 -0
  98. package/dist/project-runtime/registry.js +286 -0
  99. package/dist/project-runtime/types.d.ts +43 -0
  100. package/dist/project-runtime/types.js +1 -0
  101. package/dist/serve/cli.d.ts +5 -0
  102. package/dist/serve/cli.js +82 -23
  103. package/dist/serve/headless-server.d.ts +11 -0
  104. package/dist/serve/headless-server.js +737 -83
  105. package/dist/session-titles-store.d.ts +17 -0
  106. package/dist/session-titles-store.js +174 -0
  107. package/dist/sessions-service.d.ts +2 -0
  108. package/dist/sessions-service.js +8 -0
  109. package/dist/static-files.js +1 -0
  110. package/dist/worker-bridge-core.js +17 -6
  111. package/package.json +19 -5
@@ -0,0 +1,501 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { constants } from "node:fs";
4
+ import { access, realpath, stat } from "node:fs/promises";
5
+ import { basename, delimiter, extname, join } from "node:path";
6
+ import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
7
+ const EXECUTABLE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}$/;
8
+ const FILE_ARGUMENT_NAME = /^--[a-zA-Z][a-zA-Z0-9-]{0,63}$/;
9
+ const MAX_ARGUMENTS = 256;
10
+ const MAX_ARGUMENT_LENGTH = 8_192;
11
+ const MAX_ARGUMENT_BYTES = 64 * 1024;
12
+ const MAX_CONCURRENT_PROCESSES = 3;
13
+ const MAX_FILE_ARGUMENTS = 8;
14
+ const MAX_EVENT_CHARS = 16_384;
15
+ const MAX_PROCESS_OUTPUT_BYTES = 4 * 1024 * 1024;
16
+ const PROCESS_LIFETIME_MS = 24 * 60 * 60 * 1_000;
17
+ export function panelProcessInfo(platform = process.platform, arch = process.arch, report = process.report?.getReport()) {
18
+ if (platform !== "linux")
19
+ return { platform, arch };
20
+ const header = report && typeof report === "object"
21
+ ? report.header
22
+ : undefined;
23
+ const libc = typeof header?.glibcVersionRuntime === "string" && header.glibcVersionRuntime
24
+ ? "glibc"
25
+ : "musl";
26
+ return { platform, arch, libc };
27
+ }
28
+ async function executableFingerprint(path) {
29
+ const info = await stat(path);
30
+ if (!info.isFile())
31
+ throw new Error("resolved executable is no longer a regular file");
32
+ return createHash("sha256")
33
+ .update(String(info.dev))
34
+ .update("\0")
35
+ .update(String(info.ino))
36
+ .update("\0")
37
+ .update(String(info.size))
38
+ .update("\0")
39
+ .update(String(info.mtimeMs))
40
+ .update("\0")
41
+ .update(String(info.mode))
42
+ .digest("hex");
43
+ }
44
+ export function panelExecutableDirectories(managedBin, options = {}) {
45
+ const platform = options.platform ?? process.platform;
46
+ const env = options.env ?? process.env;
47
+ const directories = [managedBin];
48
+ if (platform === "darwin") {
49
+ if (options.home)
50
+ directories.push(join(options.home, ".local", "bin"));
51
+ directories.push("/opt/homebrew/bin", "/usr/local/bin");
52
+ }
53
+ else if (platform === "linux") {
54
+ if (options.home)
55
+ directories.push(join(options.home, ".local", "bin"));
56
+ directories.push("/home/linuxbrew/.linuxbrew/bin", "/usr/local/bin");
57
+ }
58
+ else if (platform === "win32") {
59
+ const localAppData = env.LOCALAPPDATA || (options.home ? join(options.home, "AppData", "Local") : "");
60
+ if (localAppData) {
61
+ directories.push(join(localAppData, "Microsoft", "WinGet", "Links"), join(localAppData, "Microsoft", "WindowsApps"));
62
+ }
63
+ }
64
+ return [...new Set(directories.filter(Boolean))];
65
+ }
66
+ function safeProcessEnv(source, extraPathDirectories = []) {
67
+ const allowed = [
68
+ "PATH",
69
+ "HOME",
70
+ "USERPROFILE",
71
+ "TMPDIR",
72
+ "TMP",
73
+ "TEMP",
74
+ "LANG",
75
+ "LC_ALL",
76
+ "SystemRoot",
77
+ "WINDIR",
78
+ "SSL_CERT_FILE",
79
+ "SSL_CERT_DIR",
80
+ ];
81
+ const safe = Object.fromEntries(allowed.flatMap((key) => (source[key] === undefined ? [] : [[key, source[key]]])));
82
+ const currentPath = safe.PATH ?? "";
83
+ safe.PATH = [
84
+ ...new Set([
85
+ ...extraPathDirectories.filter(Boolean),
86
+ ...currentPath.split(delimiter).filter(Boolean),
87
+ ]),
88
+ ].join(delimiter);
89
+ return safe;
90
+ }
91
+ function executableCandidates(name, platform, env) {
92
+ if (platform !== "win32" || extname(name))
93
+ return [name];
94
+ const pathExt = env.PATHEXT || ".EXE;.CMD;.BAT;.COM";
95
+ return pathExt
96
+ .split(";")
97
+ .filter(Boolean)
98
+ .map((extension) => `${name}${extension.toLowerCase()}`);
99
+ }
100
+ export async function resolvePanelExecutable(name, options = {}) {
101
+ if (!EXECUTABLE_NAME.test(name))
102
+ throw new Error("invalid executable name");
103
+ const env = options.env ?? process.env;
104
+ const platform = options.platform ?? process.platform;
105
+ const pathValue = env.PATH || "";
106
+ const directories = [
107
+ ...(options.extraPathDirectories ?? []),
108
+ ...pathValue.split(delimiter).filter(Boolean),
109
+ ];
110
+ for (const directory of [...new Set(directories)]) {
111
+ for (const candidateName of executableCandidates(name, platform, env)) {
112
+ const candidate = join(directory, candidateName);
113
+ try {
114
+ await access(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
115
+ const info = await stat(candidate);
116
+ if (info.isFile())
117
+ return await realpath(candidate);
118
+ }
119
+ catch {
120
+ // Keep searching the remaining PATH entries.
121
+ }
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+ function validateArguments(raw) {
127
+ if (!Array.isArray(raw) || raw.length > MAX_ARGUMENTS) {
128
+ throw new Error(`process.spawn accepts at most ${MAX_ARGUMENTS} arguments`);
129
+ }
130
+ const args = raw.map((value) => {
131
+ if (typeof value !== "string" || value.length > MAX_ARGUMENT_LENGTH || value.includes("\0")) {
132
+ throw new Error("process.spawn arguments must be bounded strings without NUL bytes");
133
+ }
134
+ return value;
135
+ });
136
+ if (Buffer.byteLength(JSON.stringify(args), "utf8") > MAX_ARGUMENT_BYTES) {
137
+ throw new Error("process.spawn arguments are too large");
138
+ }
139
+ return args;
140
+ }
141
+ function validateFileArgumentHandles(raw) {
142
+ if (raw === undefined)
143
+ return [];
144
+ if (!Array.isArray(raw) || raw.length > MAX_FILE_ARGUMENTS) {
145
+ throw new Error(`process.spawn accepts at most ${MAX_FILE_ARGUMENTS} sealed file arguments`);
146
+ }
147
+ const handles = raw.map((value) => {
148
+ if (typeof value !== "string" || !value || value.length > 160) {
149
+ throw new Error("process.spawn sealed file argument handles must be bounded strings");
150
+ }
151
+ return value;
152
+ });
153
+ if (new Set(handles).size !== handles.length) {
154
+ throw new Error("process.spawn sealed file argument handles must be unique");
155
+ }
156
+ return handles;
157
+ }
158
+ export class PanelAppProcessService {
159
+ options;
160
+ executables = new Map();
161
+ directories = new Map();
162
+ fileArguments = new Map();
163
+ processes = new Map();
164
+ approvedExecutables = new Set();
165
+ guestEpochs = new Map();
166
+ reservations = new Map();
167
+ closed = false;
168
+ constructor(options) {
169
+ this.options = options;
170
+ }
171
+ epoch(owner) {
172
+ if (this.closed)
173
+ throw new Error("Panel process service is closed");
174
+ return this.guestEpochs.get(owner.guestId) ?? 0;
175
+ }
176
+ assertLive(owner, epoch) {
177
+ if (this.closed || (this.guestEpochs.get(owner.guestId) ?? 0) !== epoch) {
178
+ throw new Error("Panel App process owner is no longer authorized");
179
+ }
180
+ }
181
+ async authorize(owner, epoch) {
182
+ this.assertLive(owner, epoch);
183
+ if (this.options.isOwnerAuthorized && !(await this.options.isOwnerAuthorized(owner))) {
184
+ throw new Error("Panel App process owner is no longer authorized");
185
+ }
186
+ this.assertLive(owner, epoch);
187
+ }
188
+ async findExecutable(owner, params) {
189
+ owner = { ...owner };
190
+ const epoch = this.epoch(owner);
191
+ await this.authorize(owner, epoch);
192
+ const name = params?.name;
193
+ if (typeof name !== "string" || !EXECUTABLE_NAME.test(name)) {
194
+ throw new Error("process.find requires a simple executable name");
195
+ }
196
+ const path = await resolvePanelExecutable(name, {
197
+ ...this.options,
198
+ extraPathDirectories: this.options.extraPathDirectories?.(),
199
+ });
200
+ await this.authorize(owner, epoch);
201
+ this.assertLive(owner, epoch);
202
+ if (!path)
203
+ return { available: false, name };
204
+ const handle = randomUUID();
205
+ this.executables.set(handle, { guestId: owner.guestId, name, path });
206
+ return { available: true, name, handle };
207
+ }
208
+ async grantDirectory(owner, path) {
209
+ owner = { ...owner };
210
+ const epoch = this.epoch(owner);
211
+ await this.authorize(owner, epoch);
212
+ const resolved = await realpath(path);
213
+ await this.authorize(owner, epoch);
214
+ const info = await stat(resolved);
215
+ await this.authorize(owner, epoch);
216
+ this.assertLive(owner, epoch);
217
+ if (!info.isDirectory())
218
+ throw new Error("selected process directory is not a directory");
219
+ const handle = randomUUID();
220
+ this.directories.set(handle, { guestId: owner.guestId, path: resolved });
221
+ return { handle, path: resolved, name: basename(resolved) || resolved };
222
+ }
223
+ executableName(owner, handle) {
224
+ this.epoch(owner);
225
+ if (typeof handle !== "string")
226
+ throw new Error("executable handle is required");
227
+ const executable = this.executables.get(handle);
228
+ if (!executable || executable.guestId !== owner.guestId) {
229
+ throw new Error("executable handle is invalid or belongs to another Panel App");
230
+ }
231
+ return executable.name;
232
+ }
233
+ async grantFileArgument(owner, input) {
234
+ owner = { ...owner };
235
+ const epoch = this.epoch(owner);
236
+ await this.authorize(owner, epoch);
237
+ if (typeof input.executableHandle !== "string") {
238
+ throw new Error("sealed file arguments require an executable handle");
239
+ }
240
+ const executable = this.executables.get(input.executableHandle);
241
+ if (!executable || executable.guestId !== owner.guestId) {
242
+ throw new Error("executable handle is invalid or belongs to another Panel App");
243
+ }
244
+ if (typeof input.argumentName !== "string" || !FILE_ARGUMENT_NAME.test(input.argumentName)) {
245
+ throw new Error("sealed file arguments require a simple long-option name");
246
+ }
247
+ const resolved = await realpath(input.path);
248
+ await this.authorize(owner, epoch);
249
+ const info = await stat(resolved);
250
+ await this.authorize(owner, epoch);
251
+ this.assertLive(owner, epoch);
252
+ if (!info.isFile())
253
+ throw new Error("sealed process input is not a file");
254
+ const handle = randomUUID();
255
+ this.fileArguments.set(handle, {
256
+ guestId: owner.guestId,
257
+ executablePath: executable.path,
258
+ argumentName: input.argumentName,
259
+ path: resolved,
260
+ cleanup: input.cleanup ?? (() => undefined),
261
+ });
262
+ return { handle };
263
+ }
264
+ directoryPath(owner, handle) {
265
+ this.epoch(owner);
266
+ if (typeof handle !== "string")
267
+ throw new Error("directory handle is required");
268
+ const grant = this.directories.get(handle);
269
+ if (!grant || grant.guestId !== owner.guestId) {
270
+ throw new Error("directory handle is invalid or belongs to another Panel App");
271
+ }
272
+ return grant.path;
273
+ }
274
+ async start(owner, params) {
275
+ owner = { ...owner };
276
+ const epoch = this.epoch(owner);
277
+ const input = params;
278
+ if (!input || typeof input.executableHandle !== "string") {
279
+ throw new Error("process.spawn requires an executable handle from process.find");
280
+ }
281
+ const executable = this.executables.get(input.executableHandle);
282
+ if (!executable || executable.guestId !== owner.guestId) {
283
+ throw new Error("executable handle is invalid or belongs to another Panel App");
284
+ }
285
+ const cwd = this.directoryPath(owner, input.directoryHandle);
286
+ const rawArgs = validateArguments(input.args);
287
+ const fileArgumentHandles = validateFileArgumentHandles(input.fileArgumentHandles);
288
+ const sealedArgs = [];
289
+ for (const handle of fileArgumentHandles) {
290
+ const grant = this.fileArguments.get(handle);
291
+ if (!grant || grant.guestId !== owner.guestId) {
292
+ throw new Error("sealed file argument handle is invalid or belongs to another Panel App");
293
+ }
294
+ if (grant.executablePath !== executable.path) {
295
+ throw new Error("sealed file argument is bound to a different executable");
296
+ }
297
+ sealedArgs.push(grant.argumentName, grant.path);
298
+ }
299
+ const args = validateArguments([...sealedArgs, ...rawArgs]);
300
+ const active = [...this.processes.values()].filter((process) => process.guestId === owner.guestId).length;
301
+ const reserved = this.reservations.get(owner.guestId) ?? 0;
302
+ if (active + reserved >= MAX_CONCURRENT_PROCESSES) {
303
+ throw new Error(`Panel App may run at most ${MAX_CONCURRENT_PROCESSES} processes at once`);
304
+ }
305
+ // Admission is reserved before the first await, including a user's approval dialog.
306
+ this.reservations.set(owner.guestId, reserved + 1);
307
+ try {
308
+ await this.authorize(owner, epoch);
309
+ const scope = {
310
+ appId: owner.appId,
311
+ revision: owner.revision,
312
+ executablePath: executable.path,
313
+ executableFingerprint: await executableFingerprint(executable.path),
314
+ };
315
+ await this.authorize(owner, epoch);
316
+ const guestScoped = this.options.approvalScope === "guest";
317
+ const approvalKey = (guestScoped ? `${owner.guestId}\0${epoch}\0` : "") +
318
+ `${scope.appId}\0${scope.revision}\0${scope.executablePath}\0${scope.executableFingerprint}`;
319
+ let approved = this.approvedExecutables.has(approvalKey);
320
+ if (!approved && !guestScoped && this.options.isExecutionApproved) {
321
+ approved = await this.options.isExecutionApproved(scope).catch(() => false);
322
+ await this.authorize(owner, epoch);
323
+ }
324
+ if (!approved) {
325
+ const allowed = await this.options.confirmExecution({
326
+ guestId: owner.guestId,
327
+ appId: owner.appId,
328
+ appTitle: owner.appTitle,
329
+ executable: executable.name,
330
+ executablePath: executable.path,
331
+ });
332
+ await this.authorize(owner, epoch);
333
+ if (!allowed)
334
+ throw new Error(`User denied running ${executable.name}`);
335
+ if (!guestScoped) {
336
+ await this.options.rememberExecutionApproval?.(scope).catch(() => undefined);
337
+ await this.authorize(owner, epoch);
338
+ }
339
+ }
340
+ // A binary replaced while a dialog was open must earn a new approval.
341
+ if ((await executableFingerprint(executable.path)) !== scope.executableFingerprint) {
342
+ throw new Error("resolved executable changed; find it and approve it again");
343
+ }
344
+ await this.authorize(owner, epoch);
345
+ const env = safeProcessEnv(this.options.env ?? process.env, this.options.extraPathDirectories?.() ?? []);
346
+ this.assertLive(owner, epoch);
347
+ if (this.executables.get(input.executableHandle) !== executable ||
348
+ this.directoryPath(owner, input.directoryHandle) !== cwd) {
349
+ throw new Error("Panel App process handles were revoked");
350
+ }
351
+ this.approvedExecutables.add(approvalKey);
352
+ const processId = randomUUID();
353
+ const child = spawn(executable.path, args, {
354
+ cwd,
355
+ detached: process.platform !== "win32",
356
+ env,
357
+ shell: false,
358
+ stdio: ["ignore", "pipe", "pipe"],
359
+ windowsHide: true,
360
+ });
361
+ const sendOutput = (stream, chunk) => {
362
+ const running = this.processes.get(processId);
363
+ if (running?.child !== child || running.outputTruncated)
364
+ return;
365
+ const text = String(chunk);
366
+ running.outputBytes += Buffer.byteLength(text, "utf8");
367
+ if (running.outputBytes > MAX_PROCESS_OUTPUT_BYTES) {
368
+ running.outputTruncated = true;
369
+ owner.send("process.output", {
370
+ processId,
371
+ stream: "stderr",
372
+ text: `Process output exceeded ${MAX_PROCESS_OUTPUT_BYTES} bytes; CodeShell stopped it.\n`,
373
+ });
374
+ this.terminate(running);
375
+ return;
376
+ }
377
+ for (let offset = 0; offset < text.length; offset += MAX_EVENT_CHARS) {
378
+ owner.send("process.output", {
379
+ processId,
380
+ stream,
381
+ text: text.slice(offset, offset + MAX_EVENT_CHARS),
382
+ });
383
+ }
384
+ };
385
+ child.stdout?.on("data", (chunk) => sendOutput("stdout", chunk));
386
+ child.stderr?.on("data", (chunk) => sendOutput("stderr", chunk));
387
+ child.once("error", (error) => {
388
+ if (this.processes.get(processId)?.child !== child)
389
+ return;
390
+ owner.send("process.output", {
391
+ processId,
392
+ stream: "stderr",
393
+ text: `${error.message}\n`,
394
+ });
395
+ });
396
+ const lifetime = setTimeout(() => {
397
+ const running = this.processes.get(processId);
398
+ if (running)
399
+ this.terminate(running);
400
+ }, PROCESS_LIFETIME_MS);
401
+ lifetime.unref();
402
+ this.processes.set(processId, {
403
+ guestId: owner.guestId,
404
+ child,
405
+ lifetime,
406
+ outputBytes: 0,
407
+ outputTruncated: false,
408
+ });
409
+ child.once("close", (code, signal) => {
410
+ clearTimeout(lifetime);
411
+ if (this.processes.get(processId)?.child !== child)
412
+ return;
413
+ this.processes.delete(processId);
414
+ owner.send("process.exit", { processId, code, signal });
415
+ });
416
+ return { processId, executable: executable.name };
417
+ }
418
+ finally {
419
+ const remaining = (this.reservations.get(owner.guestId) ?? 1) - 1;
420
+ if (remaining)
421
+ this.reservations.set(owner.guestId, remaining);
422
+ else
423
+ this.reservations.delete(owner.guestId);
424
+ }
425
+ }
426
+ cancel(owner, params) {
427
+ const processId = params?.processId;
428
+ if (typeof processId !== "string")
429
+ throw new Error("process.cancel requires processId");
430
+ const running = this.processes.get(processId);
431
+ if (!running || running.guestId !== owner.guestId)
432
+ return { cancelled: false };
433
+ this.terminate(running);
434
+ return { cancelled: true };
435
+ }
436
+ revokeGuest(guestId) {
437
+ this.guestEpochs.set(guestId, (this.guestEpochs.get(guestId) ?? 0) + 1);
438
+ if (this.options.approvalScope === "guest") {
439
+ for (const key of this.approvedExecutables) {
440
+ if (key.startsWith(`${guestId}\0`))
441
+ this.approvedExecutables.delete(key);
442
+ }
443
+ }
444
+ for (const [handle, grant] of this.executables) {
445
+ if (grant.guestId === guestId)
446
+ this.executables.delete(handle);
447
+ }
448
+ for (const [handle, grant] of this.directories) {
449
+ if (grant.guestId === guestId)
450
+ this.directories.delete(handle);
451
+ }
452
+ for (const [processId, running] of this.processes) {
453
+ if (running.guestId !== guestId)
454
+ continue;
455
+ clearTimeout(running.lifetime);
456
+ this.terminate(running);
457
+ this.processes.delete(processId);
458
+ }
459
+ for (const [handle, grant] of this.fileArguments) {
460
+ if (grant.guestId !== guestId)
461
+ continue;
462
+ this.fileArguments.delete(handle);
463
+ try {
464
+ grant.cleanup();
465
+ }
466
+ catch {
467
+ // Credential-backed temporary inputs are best-effort cleanup only.
468
+ }
469
+ }
470
+ }
471
+ /** Permanently shut down all guests, including operations awaiting approval. */
472
+ close() {
473
+ if (this.closed)
474
+ return;
475
+ this.closed = true;
476
+ const guests = new Set([
477
+ ...this.guestEpochs.keys(),
478
+ ...this.reservations.keys(),
479
+ ...[...this.executables.values()].map((grant) => grant.guestId),
480
+ ...[...this.directories.values()].map((grant) => grant.guestId),
481
+ ...[...this.fileArguments.values()].map((grant) => grant.guestId),
482
+ ...[...this.processes.values()].map((running) => running.guestId),
483
+ ]);
484
+ for (const guestId of guests)
485
+ this.revokeGuest(guestId);
486
+ this.approvedExecutables.clear();
487
+ }
488
+ terminate(running) {
489
+ const pid = running.child.pid;
490
+ if (typeof pid === "number" && pid > 1) {
491
+ void killProcessGroup(pid);
492
+ return;
493
+ }
494
+ try {
495
+ running.child.kill("SIGTERM");
496
+ }
497
+ catch {
498
+ // The process already exited.
499
+ }
500
+ }
501
+ }
@@ -0,0 +1,24 @@
1
+ export interface PanelRuntimeScope {
2
+ appId: string;
3
+ cwd: string;
4
+ projectPath: string;
5
+ permissions: readonly string[];
6
+ /** Checks the live owner, its project binding, and the installed revision. */
7
+ isAuthorized: () => Promise<boolean>;
8
+ }
9
+ /** Node-only implementation of the existing Panel SDK's bounded disk operations. */
10
+ export declare class PanelRuntimeServices {
11
+ private readonly roots;
12
+ private readonly dataDir;
13
+ constructor(options: {
14
+ dataDir: string;
15
+ });
16
+ call(input: PanelRuntimeScope, method: string, params?: unknown): Promise<unknown>;
17
+ private storage;
18
+ private root;
19
+ private openPath;
20
+ private read;
21
+ private workspace;
22
+ private ensureDirectory;
23
+ private write;
24
+ }