@cjhyy/code-shell-server 0.9.21 → 0.9.23

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 (104) hide show
  1. package/README.md +45 -1
  2. package/dist/bin/code-shell-settings-recovery.d.ts +2 -0
  3. package/dist/bin/code-shell-settings-recovery.js +4 -0
  4. package/dist/desktop-web/http-api.js +14 -0
  5. package/dist/environment-identity.d.ts +4 -0
  6. package/dist/environment-identity.js +67 -0
  7. package/dist/hub/auth-store.d.ts +2 -0
  8. package/dist/hub/auth-store.js +7 -0
  9. package/dist/index.auth.d.ts +3 -0
  10. package/dist/index.auth.js +2 -0
  11. package/dist/index.links.d.ts +1 -0
  12. package/dist/index.links.js +1 -0
  13. package/dist/index.panels.d.ts +7 -0
  14. package/dist/index.panels.js +7 -0
  15. package/dist/index.remote-relay.d.ts +5 -0
  16. package/dist/index.remote-relay.js +6 -0
  17. package/dist/index.serve.d.ts +2 -0
  18. package/dist/index.serve.js +2 -0
  19. package/dist/links/http.d.ts +3 -0
  20. package/dist/links/http.js +11 -2
  21. package/dist/links/remote-configuration.d.ts +4 -0
  22. package/dist/links/remote-configuration.js +32 -0
  23. package/dist/links/service.d.ts +6 -1
  24. package/dist/links/service.js +241 -19
  25. package/dist/mobile-remote/mobile-upload-service.d.ts +4 -0
  26. package/dist/mobile-remote/mobile-upload-service.js +69 -11
  27. package/dist/mobile-remote/remote-host-manager.d.ts +10 -5
  28. package/dist/mobile-remote/remote-host-manager.js +53 -5
  29. package/dist/panels/agent-task-service.js +9 -2
  30. package/dist/panels/automations.d.ts +49 -0
  31. package/dist/panels/automations.js +134 -0
  32. package/dist/panels/bridge-contract.d.ts +5 -3
  33. package/dist/panels/bridge-contract.js +11 -1
  34. package/dist/panels/directory-bookmarks.d.ts +9 -1
  35. package/dist/panels/directory-bookmarks.js +64 -5
  36. package/dist/panels/execution-gate.d.ts +26 -0
  37. package/dist/panels/execution-gate.js +70 -0
  38. package/dist/panels/http.d.ts +26 -2
  39. package/dist/panels/http.js +7 -1
  40. package/dist/panels/hub-automations.d.ts +31 -0
  41. package/dist/panels/hub-automations.js +352 -0
  42. package/dist/panels/management-http.d.ts +20 -2
  43. package/dist/panels/management-http.js +15 -2
  44. package/dist/panels/management.d.ts +26 -4
  45. package/dist/panels/management.js +382 -39
  46. package/dist/panels/package-mutation.d.ts +8 -0
  47. package/dist/panels/package-mutation.js +23 -0
  48. package/dist/panels/process-service.d.ts +3 -0
  49. package/dist/panels/process-service.js +77 -4
  50. package/dist/panels/resources/http.d.ts +11 -0
  51. package/dist/panels/resources/http.js +74 -0
  52. package/dist/panels/resources/references.d.ts +5 -0
  53. package/dist/panels/resources/references.js +26 -0
  54. package/dist/panels/runtime-services.js +16 -3
  55. package/dist/panels/runtime.d.ts +16 -4
  56. package/dist/panels/runtime.js +522 -64
  57. package/dist/panels/shared-tool-jobs.d.ts +40 -0
  58. package/dist/panels/shared-tool-jobs.js +37 -0
  59. package/dist/panels/storage-store.d.ts +21 -0
  60. package/dist/panels/storage-store.js +43 -0
  61. package/dist/panels/task-cookie-host.d.ts +26 -0
  62. package/dist/panels/task-cookie-host.js +145 -0
  63. package/dist/panels/task-cookies.d.ts +51 -0
  64. package/dist/panels/task-cookies.js +183 -0
  65. package/dist/panels/tool-executor.d.ts +21 -5
  66. package/dist/panels/tool-executor.js +171 -30
  67. package/dist/panels/tool-jobs-storage.d.ts +2 -0
  68. package/dist/panels/tool-jobs-storage.js +34 -0
  69. package/dist/panels/tool-jobs.d.ts +49 -3
  70. package/dist/panels/tool-jobs.js +270 -15
  71. package/dist/panels/types.d.ts +43 -0
  72. package/dist/project-runtime/backup.d.ts +26 -0
  73. package/dist/project-runtime/backup.js +500 -0
  74. package/dist/project-runtime/control-server.d.ts +4 -0
  75. package/dist/project-runtime/control-server.js +19 -1
  76. package/dist/project-runtime/docker-provider.d.ts +4 -0
  77. package/dist/project-runtime/docker-provider.js +16 -1
  78. package/dist/project-runtime/managed-entry.js +4 -0
  79. package/dist/project-runtime/registry.d.ts +2 -0
  80. package/dist/project-runtime/registry.js +15 -0
  81. package/dist/project-runtime/seccomp.d.ts +8 -0
  82. package/dist/project-runtime/seccomp.js +72 -0
  83. package/dist/project-runtime/types.d.ts +2 -0
  84. package/dist/remote-relay/byte-stream.d.ts +9 -0
  85. package/dist/remote-relay/byte-stream.js +71 -0
  86. package/dist/remote-relay/connector.d.ts +27 -0
  87. package/dist/remote-relay/connector.js +197 -0
  88. package/dist/remote-relay/local-stream.d.ts +10 -0
  89. package/dist/remote-relay/local-stream.js +120 -0
  90. package/dist/remote-relay/protocol.d.ts +39 -0
  91. package/dist/remote-relay/protocol.js +64 -0
  92. package/dist/serve/automation-worker.d.ts +16 -0
  93. package/dist/serve/automation-worker.js +126 -0
  94. package/dist/serve/cli.d.ts +3 -0
  95. package/dist/serve/cli.js +17 -0
  96. package/dist/serve/headless-server.d.ts +3 -0
  97. package/dist/serve/headless-server.js +140 -10
  98. package/dist/serve/settings-recovery-cli.d.ts +29 -0
  99. package/dist/serve/settings-recovery-cli.js +128 -0
  100. package/dist/session-http-responses.d.ts +7 -0
  101. package/dist/session-http-responses.js +40 -0
  102. package/dist/worker-bridge-core.d.ts +9 -1
  103. package/dist/worker-bridge-core.js +45 -0
  104. package/package.json +15 -6
@@ -271,6 +271,7 @@ export class PanelAppProcessService {
271
271
  await this.authorize(owner, epoch);
272
272
  const info = await stat(resolved);
273
273
  await this.authorize(owner, epoch);
274
+ await input.validate?.();
274
275
  this.assertLive(owner, epoch);
275
276
  if (!info.isFile())
276
277
  throw new Error("sealed process input is not a file");
@@ -283,6 +284,7 @@ export class PanelAppProcessService {
283
284
  argumentName: input.argumentName,
284
285
  path: resolved,
285
286
  cleanup: input.cleanup ?? (() => undefined),
287
+ validate: input.validate,
286
288
  });
287
289
  return { handle };
288
290
  }
@@ -391,6 +393,31 @@ export class PanelAppProcessService {
391
393
  }
392
394
  const rawArgs = validateArguments(input.args);
393
395
  const fileArgumentHandles = validateFileArgumentHandles(input.fileArgumentHandles);
396
+ const validateInputs = async () => {
397
+ for (const handle of fileArgumentHandles) {
398
+ const grant = this.fileArguments.get(handle);
399
+ if (!grant || !sameProcessOwner(grant, owner))
400
+ throw new Error("sealed process input was revoked");
401
+ try {
402
+ await grant.validate?.();
403
+ }
404
+ catch {
405
+ if (this.fileArguments.get(handle) === grant) {
406
+ this.fileArguments.delete(handle);
407
+ try {
408
+ grant.cleanup();
409
+ }
410
+ catch {
411
+ /* Revocation still takes effect. */
412
+ }
413
+ }
414
+ throw new Error("sealed process input authorization changed; authorize it again");
415
+ }
416
+ this.assertLive(owner, epoch);
417
+ if (this.fileArguments.get(handle) !== grant)
418
+ throw new Error("sealed process input was revoked");
419
+ }
420
+ };
394
421
  const sealedArgs = [];
395
422
  for (const handle of fileArgumentHandles) {
396
423
  const grant = this.fileArguments.get(handle);
@@ -411,6 +438,8 @@ export class PanelAppProcessService {
411
438
  throw new Error(`Panel App may run at most ${MAX_CONCURRENT_PROCESSES} processes at once`);
412
439
  }
413
440
  // Admission is reserved before the first await, including a user's approval dialog.
441
+ const releaseExecution = this.options.acquireExecution?.(owner);
442
+ let processOwnsExecution = false;
414
443
  this.reservations.set(owner.guestId, reserved + 1);
415
444
  try {
416
445
  await this.authorize(owner, epoch);
@@ -463,6 +492,8 @@ export class PanelAppProcessService {
463
492
  if ((await verifyProcessEntry(entry.path, entry.sha256)) !== entry.identity)
464
493
  throw new Error("reviewed package entry was replaced; resolve it again");
465
494
  }
495
+ await validateInputs();
496
+ await this.authorize(owner, epoch);
466
497
  const env = safeProcessEnv(this.options.env ?? process.env, this.options.extraPathDirectories?.() ?? []);
467
498
  this.assertLive(owner, epoch);
468
499
  if (this.executables.get(input.executableHandle) !== executable ||
@@ -570,29 +601,71 @@ export class PanelAppProcessService {
570
601
  }),
571
602
  };
572
603
  this.processes.set(processId, running);
604
+ let checkingInputs = false;
605
+ const inputChecks = fileArgumentHandles.some((handle) => this.fileArguments.get(handle)?.validate)
606
+ ? setInterval(() => {
607
+ if (checkingInputs || this.processes.get(processId) !== running || running.termination)
608
+ return;
609
+ checkingInputs = true;
610
+ void validateInputs()
611
+ .catch(() => {
612
+ if (this.processes.get(processId) !== running || running.termination)
613
+ return;
614
+ emit("process.output", {
615
+ processId,
616
+ stream: "stderr",
617
+ text: "Process input authorization changed; CodeShell stopped it.\n",
618
+ });
619
+ this.terminate(running);
620
+ })
621
+ .finally(() => {
622
+ checkingInputs = false;
623
+ });
624
+ }, 1000)
625
+ : undefined;
626
+ inputChecks?.unref();
573
627
  child.once("close", (code, signal) => {
574
628
  void (async () => {
575
629
  clearTimeout(lifetime);
630
+ if (inputChecks)
631
+ clearInterval(inputChecks);
576
632
  // Cancellation also waits for the process group, not just its leader.
577
633
  await running.termination;
578
- finishExit({ code, signal });
634
+ let finalCode = code;
635
+ if (inputChecks && this.processes.get(processId) === running) {
636
+ try {
637
+ await validateInputs();
638
+ }
639
+ catch {
640
+ finalCode = code === 0 ? 1 : code;
641
+ emit("process.output", {
642
+ processId,
643
+ stream: "stderr",
644
+ text: "Process input authorization changed; results were rejected.\n",
645
+ });
646
+ }
647
+ }
648
+ finishExit({ code: finalCode, signal });
579
649
  this.retiring.delete(processId);
580
650
  if (this.processes.get(processId)?.child !== child)
581
651
  return;
582
652
  this.processes.delete(processId);
583
653
  record.status = "exited";
584
654
  record.exitedAt = this.now();
585
- record.code = code;
655
+ record.code = finalCode;
586
656
  record.signal = signal;
587
- emit("process.exit", { processId, code, signal });
657
+ emit("process.exit", { processId, code: finalCode, signal });
588
658
  // Keep the receipt only after the operating system reports the real close.
589
659
  if (!this.closed && (this.guestEpochs.get(owner.guestId) ?? 0) === epoch)
590
660
  this.receipts.add(record);
591
- })();
661
+ })().finally(() => releaseExecution?.());
592
662
  });
663
+ processOwnsExecution = true;
593
664
  return { processId, executable: executable.name };
594
665
  }
595
666
  finally {
667
+ if (!processOwnsExecution)
668
+ releaseExecution?.();
596
669
  const remaining = (this.reservations.get(owner.guestId) ?? 1) - 1;
597
670
  if (remaining)
598
671
  this.reservations.set(owner.guestId, remaining);
@@ -0,0 +1,11 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { PanelResourceService } from "./service.js";
3
+ import type { ResourceScope } from "./types.js";
4
+ /** Browser byte transport; scope and reader are resolved by the authenticated Host. */
5
+ export declare function servePanelResource(request: IncomingMessage, response: ServerResponse, options: {
6
+ service: PanelResourceService;
7
+ scope: ResourceScope;
8
+ id: string;
9
+ download: boolean;
10
+ isAuthorized(): Promise<boolean>;
11
+ }): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { Readable } from "node:stream";
2
+ import { pipeline } from "node:stream/promises";
3
+ /** Browser byte transport; scope and reader are resolved by the authenticated Host. */
4
+ export async function servePanelResource(request, response, options) {
5
+ let source = null;
6
+ const authorize = async () => {
7
+ if (request.aborted || response.destroyed || !(await options.isAuthorized()))
8
+ throw new Error("Resource authorization expired");
9
+ };
10
+ try {
11
+ await authorize();
12
+ if (!["GET", "HEAD"].includes(request.method ?? "")) {
13
+ response.writeHead(405, { Allow: "GET, HEAD" }).end();
14
+ return;
15
+ }
16
+ const asset = await options.service.get(options.scope, options.id);
17
+ const result = await options.service.openRead(options.scope, options.id, {
18
+ range: request.headers.range,
19
+ method: asset.bytes === 0 ? "HEAD" : request.method,
20
+ });
21
+ source = result.body;
22
+ await authorize();
23
+ const headers = { ...result.headers, "Referrer-Policy": "no-referrer" };
24
+ if (options.download)
25
+ headers["Content-Disposition"] =
26
+ `attachment; filename*=UTF-8''${encodeURIComponent(asset.name).replace(/['()*!]/g, (value) => `%${value.charCodeAt(0).toString(16).toUpperCase()}`)}`;
27
+ response.writeHead(result.status, headers);
28
+ if (!source) {
29
+ response.end();
30
+ return;
31
+ }
32
+ let checking = false;
33
+ const timer = setInterval(() => {
34
+ if (checking)
35
+ return;
36
+ checking = true;
37
+ void authorize()
38
+ .catch(() => {
39
+ source?.destroy();
40
+ response.destroy();
41
+ })
42
+ .finally(() => {
43
+ checking = false;
44
+ });
45
+ }, 250);
46
+ timer.unref();
47
+ try {
48
+ await pipeline(Readable.from((async function* () {
49
+ for await (const chunk of source) {
50
+ await authorize();
51
+ yield chunk;
52
+ }
53
+ })(), { objectMode: false }), response);
54
+ }
55
+ finally {
56
+ clearInterval(timer);
57
+ }
58
+ }
59
+ catch {
60
+ if (response.headersSent || response.destroyed)
61
+ response.destroy();
62
+ else
63
+ response
64
+ .writeHead(404, {
65
+ "Content-Type": "text/plain; charset=utf-8",
66
+ "Cache-Control": "no-store",
67
+ "X-Content-Type-Options": "nosniff",
68
+ })
69
+ .end(request.method === "HEAD" ? undefined : "文件不可用或访问授权已失效,请重新打开面板。");
70
+ }
71
+ finally {
72
+ source?.destroy();
73
+ }
74
+ }
@@ -30,6 +30,11 @@ export declare class ExternalResourceReferences {
30
30
  createFromDirectory(scope: ResourceScope, root: string, path: string, options?: ExternalReferenceOptions): Promise<ExternalResourceReference>;
31
31
  createFromSelectedPath(scope: ResourceScope, path: string, options?: ExternalReferenceOptions): Promise<ExternalResourceReference>;
32
32
  get(scope: ResourceScope, id: string): Promise<ExternalResourceReference>;
33
+ /**
34
+ * Host-only: the verified absolute location of a reference, for a read-only
35
+ * handoff to a reviewed tool. Never return it to a Guest.
36
+ */
37
+ location(scope: ResourceScope, id: string, options?: ExternalReferenceOptions): Promise<string>;
33
38
  relinkFromDirectory(scope: ResourceScope, id: string, root: string, path: string, options?: ExternalReferenceOptions): Promise<ExternalResourceReference>;
34
39
  relinkFromSelectedPath(scope: ResourceScope, id: string, path: string, options?: ExternalReferenceOptions): Promise<ExternalResourceReference>;
35
40
  forget(scope: ResourceScope, id: string): Promise<{
@@ -270,6 +270,32 @@ export class ExternalResourceReferences {
270
270
  }
271
271
  });
272
272
  }
273
+ /**
274
+ * Host-only: the verified absolute location of a reference, for a read-only
275
+ * handoff to a reviewed tool. Never return it to a Guest.
276
+ */
277
+ location(scope, id, options = {}) {
278
+ return this.locked(scope, async () => {
279
+ const record = await this.record(scope, id);
280
+ const source = await this.checked(scope, record, options);
281
+ try {
282
+ await source.verify();
283
+ return join(record.root, record.path);
284
+ }
285
+ catch (error) {
286
+ // A drive may disappear after checked() returns. Preserve revocation
287
+ // and abort semantics before hiding native errors containing its path.
288
+ await this.authorize(scope, options);
289
+ if (error instanceof ReferenceUnavailable)
290
+ throw error;
291
+ const code = error.code;
292
+ throw new ReferenceUnavailable(["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(code ?? "") ? "missing" : "changed");
293
+ }
294
+ finally {
295
+ await source.close();
296
+ }
297
+ });
298
+ }
273
299
  relinkFromDirectory(scope, id, root, path, options = {}) {
274
300
  return this.locked(scope, async () => {
275
301
  const previous = await this.record(scope, id);
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
3
3
  import { chmod, link, lstat, mkdir, open, opendir, realpath, rename, rm, writeFile, } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  import { acquireLockOnPath } from "@cjhyy/code-shell-core/internal";
6
- import { DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES, panelAppStorageKey, panelAppStoragePath, preparePanelAppStorage, readPanelAppStorage, writePanelAppStorage, } from "./storage-store.js";
6
+ import { DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES, panelAppStorageSnapshot, panelAppStorageChange, applyPanelAppStorageChange, panelAppStorageKey, panelAppStoragePath, preparePanelAppStorage, readPanelAppStorage, writePanelAppStorage, } from "./storage-store.js";
7
7
  const MAX_READ_BYTES = 480 * 1024;
8
8
  const MAX_WRITE_BYTES = 384 * 1024;
9
9
  const MAX_LIST_ENTRIES = 200;
@@ -21,6 +21,8 @@ const TEXT_EXTENSIONS = new Set([
21
21
  ]);
22
22
  const METHOD_PERMISSIONS = {
23
23
  "storage.get": "storage",
24
+ "storage.getSnapshot": "storage",
25
+ "storage.compareAndSet": "storage",
24
26
  "storage.set": "storage",
25
27
  "storage.delete": "storage",
26
28
  "workspace.info": "workspace.info",
@@ -134,10 +136,15 @@ export class PanelRuntimeServices {
134
136
  const key = panelAppStorageKey(params);
135
137
  const file = panelAppStoragePath(this.dataDir, scope.appId, scope.projectPath);
136
138
  const quota = DEFAULT_PANEL_APP_STORAGE_QUOTA_BYTES;
137
- if (method === "storage.get") {
139
+ if (method === "storage.get" || method === "storage.getSnapshot") {
138
140
  const state = await readPanelAppStorage(file, quota);
139
- return Object.hasOwn(state, key) ? state[key] : null;
141
+ return method === "storage.getSnapshot"
142
+ ? panelAppStorageSnapshot(state, key)
143
+ : Object.hasOwn(state, key)
144
+ ? state[key]
145
+ : null;
140
146
  }
147
+ const change = method === "storage.compareAndSet" ? panelAppStorageChange(params) : null;
141
148
  const encoded = method === "storage.set" ? JSON.stringify(params?.value) : null;
142
149
  if (method === "storage.set" && encoded === undefined)
143
150
  throw new Error("Panel App storage only accepts JSON values");
@@ -147,6 +154,12 @@ export class PanelRuntimeServices {
147
154
  const release = await storageLock(file);
148
155
  try {
149
156
  const state = await readPanelAppStorage(file, quota);
157
+ if (change) {
158
+ const result = applyPanelAppStorageChange(state, change);
159
+ if (result.updated)
160
+ await writePanelAppStorage(file, state, quota, () => authorized(scope));
161
+ return result;
162
+ }
150
163
  const existed = Object.hasOwn(state, key);
151
164
  if (method === "storage.set")
152
165
  state[key] = JSON.parse(encoded);
@@ -1,8 +1,15 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { type InstalledPanelApp } from "@cjhyy/code-shell-core";
3
- import { type PanelManagementOptions } from "./management.js";
3
+ import type { SharedPanelToolHost } from "./shared-tool-jobs.js";
4
+ import { type PanelDirectoryAuthorizer } from "./directory-bookmarks.js";
4
5
  import type { PanelSnapshot } from "./types.js";
5
- export declare const panelWebCompatibility: NonNullable<PanelManagementOptions["compatibility"]>;
6
+ import { type PanelAutomationHost } from "./automations.js";
7
+ export declare const panelWebCompatibility: (app: Pick<InstalledPanelApp, "permissions">, options?: {
8
+ automations?: boolean;
9
+ }) => {
10
+ supported: boolean;
11
+ reasons: string[];
12
+ };
6
13
  export interface PanelTaskScope {
7
14
  instanceId: string;
8
15
  ownerId: string;
@@ -24,16 +31,21 @@ export interface PanelTaskHost {
24
31
  export interface PanelRuntimeOptions {
25
32
  cwd: string;
26
33
  bindingCwd?: string;
34
+ projectPackages?: boolean;
27
35
  dataDir: string;
28
36
  host: "hub" | "desktop";
29
37
  publicPathPrefix?: string;
30
- snapshot: () => Promise<PanelSnapshot>;
38
+ snapshot: (appId?: string) => Promise<PanelSnapshot>;
31
39
  ownerId: (request: IncomingMessage) => Promise<string | undefined>;
32
40
  isAuthorized: (request: IncomingMessage) => Promise<boolean>;
33
41
  /** Host/test seam. HTTP callers never provide install paths or this catalog. */
34
- listInstalled?: () => Promise<InstalledPanelApp[]>;
42
+ listInstalled?: (appId?: string) => Promise<InstalledPanelApp[]>;
35
43
  now?: () => number;
36
44
  agentTasks?: PanelTaskHost;
45
+ automations?: PanelAutomationHost;
46
+ /** Reuse the Desktop coordinator; this transport never owns its lifetime. */
47
+ sharedToolJobs?: SharedPanelToolHost;
48
+ authorizePanelDirectory?: PanelDirectoryAuthorizer;
37
49
  createAgentTasks?: (hooks: {
38
50
  onPanelAction(scope: PanelTaskScope, input: Record<string, unknown>): Promise<unknown>;
39
51
  }) => PanelTaskHost;