@cjhyy/code-shell-server 0.9.9 → 0.9.11

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 (35) hide show
  1. package/dist/index.panels.d.ts +8 -0
  2. package/dist/index.panels.js +8 -0
  3. package/dist/panels/bridge-contract.d.ts +72 -0
  4. package/dist/panels/bridge-contract.js +92 -0
  5. package/dist/panels/connections.d.ts +31 -0
  6. package/dist/panels/connections.js +118 -0
  7. package/dist/panels/process-entry.d.ts +2 -0
  8. package/dist/panels/process-entry.js +52 -0
  9. package/dist/panels/process-service.d.ts +61 -0
  10. package/dist/panels/process-service.js +367 -33
  11. package/dist/panels/process-state.d.ts +58 -0
  12. package/dist/panels/process-state.js +87 -0
  13. package/dist/panels/resources/directories.d.ts +12 -0
  14. package/dist/panels/resources/directories.js +89 -0
  15. package/dist/panels/resources/library.d.ts +63 -0
  16. package/dist/panels/resources/library.js +423 -0
  17. package/dist/panels/resources/service.d.ts +45 -0
  18. package/dist/panels/resources/service.js +317 -0
  19. package/dist/panels/resources/storage.d.ts +10 -0
  20. package/dist/panels/resources/storage.js +102 -0
  21. package/dist/panels/resources/types.d.ts +16 -0
  22. package/dist/panels/resources/types.js +1 -0
  23. package/dist/panels/resources/uploads.d.ts +64 -0
  24. package/dist/panels/resources/uploads.js +527 -0
  25. package/dist/panels/runtime.d.ts +1 -1
  26. package/dist/panels/runtime.js +137 -15
  27. package/dist/panels/tool-executor.d.ts +58 -0
  28. package/dist/panels/tool-executor.js +421 -0
  29. package/dist/panels/tool-jobs-storage.d.ts +16 -0
  30. package/dist/panels/tool-jobs-storage.js +169 -0
  31. package/dist/panels/tool-jobs.d.ts +103 -0
  32. package/dist/panels/tool-jobs.js +530 -0
  33. package/dist/sessions-service.d.ts +2 -0
  34. package/dist/sessions-service.js +8 -0
  35. package/package.json +5 -5
@@ -4,6 +4,9 @@ import { lstat, mkdir, open, opendir, realpath } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, extname, join, resolve, sep } from "node:path";
6
6
  import { listInstalledPanelApps, validateToolArgsStrict, } from "@cjhyy/code-shell-core";
7
+ import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, panelRuntimeCapabilities, PanelBridgeError, panelBridgeFailure, } from "./bridge-contract.js";
8
+ import { PanelResourceService } from "./resources/service.js";
9
+ import { panelConnections, panelConnectionIds, materializePanelConnections, } from "./connections.js";
7
10
  import { PanelRuntimeServices } from "./runtime-services.js";
8
11
  import { handlePanelProcessDirectory } from "./process-files.js";
9
12
  import { PanelAppProcessService, panelExecutableDirectories, panelProcessInfo, } from "./process-service.js";
@@ -24,13 +27,10 @@ const METHODS = [
24
27
  "external.open",
25
28
  "agent.submitPrompt",
26
29
  "notifications.send",
27
- "process.find",
28
- "process.info",
29
- "process.spawn",
30
- "process.cancel",
31
- "filesystem.getKnownDirectory",
32
- "filesystem.pickDirectory",
33
- "filesystem.openDirectory",
30
+ ...panelProcessMethods,
31
+ ...panelResourceMethods,
32
+ "credentials.connections.list",
33
+ "credentials.connections.authorizeProcess",
34
34
  "agent.task.models",
35
35
  "agent.task.start",
36
36
  "agent.task.list",
@@ -50,6 +50,8 @@ const PERMISSIONS = new Set([
50
50
  "agent.submitPrompt",
51
51
  "notifications.send",
52
52
  "process",
53
+ "resources",
54
+ "credentials.connections",
53
55
  "agent.task",
54
56
  ]);
55
57
  const MIME = {
@@ -78,6 +80,10 @@ export const panelWebCompatibility = (app) => ({
78
80
  ],
79
81
  });
80
82
  function methodPermission(method) {
83
+ if (method.startsWith("resources."))
84
+ return "resources";
85
+ if (method.startsWith("credentials.connections."))
86
+ return "credentials.connections";
81
87
  if (method.startsWith("storage."))
82
88
  return "storage";
83
89
  if (method.startsWith("process.") || method.startsWith("filesystem."))
@@ -253,7 +259,7 @@ function bridgeScript(id, origin) {
253
259
  const item = pending.get(data.requestId);
254
260
  if (!item) return;
255
261
  pending.delete(data.requestId); clearTimeout(item.timer);
256
- data.error ? item.reject(new Error(String(data.error))) : item.resolve(data.result);
262
+ data.error ? item.reject(Object.assign(new Error(String(data.error)), { code: typeof data.code === "string" ? data.code : "OPERATION_FAILED", ...(typeof data.retryAfterMs === "number" ? { retryAfterMs: data.retryAfterMs } : {}) })) : item.resolve(data.result);
257
263
  } else if (data.type === "codeshell-panel:event") {
258
264
  if (data.event === "tools.invoke") {
259
265
  const call = data.payload;
@@ -280,6 +286,7 @@ function bridgeScript(id, origin) {
280
286
  value: Object.freeze({
281
287
  getContext: () => ask("context.get"),
282
288
  call: ask,
289
+ callResult: (method, params) => ask(method, params).then(value => ({ ok: true, value }), error => ({ ok: false, error: { code: error.code || "OPERATION_FAILED", message: error.message, ...(typeof error.retryAfterMs === "number" ? { retryAfterMs: error.retryAfterMs } : {}) } })),
283
290
  on: (name, listener) => {
284
291
  if (!["context.changed", "process.output", "process.exit", "agent.task.changed", "media.job.changed"].includes(name) || typeof listener !== "function") {
285
292
  throw new Error("Invalid event listener");
@@ -360,6 +367,16 @@ export function createPanelRuntime(options) {
360
367
  const byGuest = new Map();
361
368
  const processes = new PanelAppProcessService({
362
369
  approvalScope: "guest",
370
+ resolvePackageEntry: async (owner, name) => {
371
+ const grant = byGuest.get(owner.guestId);
372
+ const entry = grant?.app.nativeEntries?.[name];
373
+ if (!grant ||
374
+ !entry ||
375
+ !(await authorized(grant)) ||
376
+ entry.sha256 !== grant.files.get(entry.entry))
377
+ throw new PanelBridgeError("REVOKED", "Installed tool entry is unavailable or changed");
378
+ return { path: join(grant.root, entry.entry), sha256: entry.sha256 };
379
+ },
363
380
  extraPathDirectories: () => panelExecutableDirectories(join(options.dataDir, "panel-bin"), { home: homedir() }),
364
381
  isOwnerAuthorized: async (owner) => {
365
382
  const grant = byGuest.get(owner.guestId);
@@ -371,6 +388,18 @@ export function createPanelRuntime(options) {
371
388
  (await confirm(grant, `允许 ${input.appTitle} 运行 ${input.executable}?`, `程序将在服务端运行,可使用当前运行用户的文件与网络权限。\n程序:${input.executablePath}`)));
372
389
  },
373
390
  });
391
+ const resources = new PanelResourceService({
392
+ rootDirectory: join(options.dataDir, "panel-app-media"),
393
+ isScopeAuthorized: async (scope) => {
394
+ for (const grant of grants.values())
395
+ if (grant.app.id === scope.appId &&
396
+ scope.projectPath === (options.bindingCwd ?? options.cwd) &&
397
+ grant.app.permissions.includes("resources") &&
398
+ (await authorized(grant)))
399
+ return true;
400
+ return false;
401
+ },
402
+ });
374
403
  const agentTasks = options.createAgentTasks?.({ onPanelAction }) ?? options.agentTasks;
375
404
  const reaper = setInterval(() => {
376
405
  for (const grant of grants.values())
@@ -631,11 +660,31 @@ export function createPanelRuntime(options) {
631
660
  visible: true,
632
661
  theme: input.theme === "dark" ? "dark" : "light",
633
662
  locale: typeof input.locale === "string" ? input.locale.slice(0, 32) : "zh-CN",
634
- apiVersion: agentTasks ? 9 : 7,
663
+ apiVersion: panelRuntimeApiVersion,
664
+ capabilities: panelRuntimeCapabilities({
665
+ process: app.permissions.includes("process"),
666
+ resources: app.permissions.includes("resources") ? resources.capabilities() : undefined,
667
+ limits: {
668
+ maxParamsBytes: 3 * 1024 * 1024,
669
+ maxResultBytes: 3 * 1024 * 1024,
670
+ rateWindowMs: 60000,
671
+ maxCallsPerWindow: 240,
672
+ maxTransferCallsPerWindow: 2048,
673
+ callTimeoutMs: 60000,
674
+ consentTimeoutMs: 50000,
675
+ },
676
+ }),
635
677
  host: options.host,
636
678
  availableMethods: METHODS.filter((method) => {
637
679
  if (method.startsWith("agent.task.") && !agentTasks)
638
680
  return false;
681
+ if ([
682
+ "resources.materialize",
683
+ "resources.capture",
684
+ "credentials.connections.authorizeProcess",
685
+ ].includes(method) &&
686
+ !app.permissions.includes("process"))
687
+ return false;
639
688
  if (method.startsWith("tools."))
640
689
  return !!app.agent?.tools.length;
641
690
  return (method === "context.get" || app.permissions.includes(methodPermission(method)));
@@ -665,6 +714,7 @@ export function createPanelRuntime(options) {
665
714
  context,
666
715
  expiresAt: now() + GRANT_TTL,
667
716
  calls: [],
717
+ transfers: [],
668
718
  events: [],
669
719
  eventCounter: 0,
670
720
  eventBytes: 0,
@@ -699,10 +749,20 @@ export function createPanelRuntime(options) {
699
749
  const method = input.method, params = input.params;
700
750
  if (typeof method !== "string" || !METHODS.includes(method))
701
751
  error(501, "这个面板功能尚未接入网页版,请在桌面客户端使用:" + String(method).slice(0, 80));
702
- grant.calls = grant.calls.filter((time) => time > now() - 60_000);
703
- if (grant.calls.length >= 240)
704
- error(429, "面板请求过于频繁。");
705
- grant.calls.push(now());
752
+ const transfer = [
753
+ "resources.upload.write",
754
+ "resources.read",
755
+ "process.write",
756
+ "process.get",
757
+ ].includes(method);
758
+ const history = (transfer ? grant.transfers : grant.calls).filter((time) => time > now() - 60000);
759
+ if (history.length >= (transfer ? 2048 : 240))
760
+ throw new PanelBridgeError("RATE_LIMITED", "面板请求过于频繁。", Math.max(1, history[0] + 60000 - now()));
761
+ history.push(now());
762
+ if (transfer)
763
+ grant.transfers = history;
764
+ else
765
+ grant.calls = history;
706
766
  if (method === "context.get")
707
767
  return grant.context;
708
768
  if (method.startsWith("tools.")) {
@@ -718,6 +778,44 @@ export function createPanelRuntime(options) {
718
778
  const permission = methodPermission(method);
719
779
  if (!grant.app.permissions.includes(permission))
720
780
  error(403, "面板未声明这个权限。");
781
+ const processOwner = {
782
+ guestId: grant.guestId,
783
+ appId: grant.app.id,
784
+ appTitle: grant.app.title.default,
785
+ revision: grant.revision,
786
+ send: (event, payload) => emit(grant, event, payload),
787
+ };
788
+ if (method.startsWith("resources.")) {
789
+ if (["resources.materialize", "resources.capture"].includes(method) &&
790
+ !grant.app.permissions.includes("process"))
791
+ error(403, "资源工具交接需要process权限。");
792
+ return resources.dispatch({ appId: grant.app.id, projectPath: options.bindingCwd ?? options.cwd }, method, params, { resolveDirectory: (handle) => processes.directoryPath(processOwner, handle) });
793
+ }
794
+ if (method === "credentials.connections.list")
795
+ return panelConnections(options.cwd);
796
+ if (method === "credentials.connections.authorizeProcess") {
797
+ if (!grant.app.permissions.includes("process"))
798
+ error(403, "连接交接需要process权限。");
799
+ const value = params;
800
+ const ids = panelConnectionIds(value?.connectionIds);
801
+ if (typeof value.executableHandle !== "string" || typeof value.argumentName !== "string")
802
+ error(400, "连接交接参数无效。");
803
+ if (!(await confirm(grant, "允许面板工具使用这些连接?", ids.join(", "))))
804
+ error(403, "已取消连接交接。");
805
+ const sealed = await materializePanelConnections(join(options.dataDir, "panel-sealed"), options.cwd, ids);
806
+ try {
807
+ return await processes.grantFileArgument(processOwner, {
808
+ executableHandle: value.executableHandle,
809
+ argumentName: value.argumentName,
810
+ path: sealed.path,
811
+ cleanup: sealed.cleanup,
812
+ });
813
+ }
814
+ catch (error) {
815
+ sealed.cleanup();
816
+ throw error;
817
+ }
818
+ }
721
819
  if (method.startsWith("agent.task.")) {
722
820
  if (!agentTasks)
723
821
  error(501, "当前宿主未提供独立面板任务。");
@@ -738,6 +836,14 @@ export function createPanelRuntime(options) {
738
836
  return processes.findExecutable(owner, params);
739
837
  if (method === "process.info")
740
838
  return panelProcessInfo();
839
+ if (method === "process.resolveEntry")
840
+ return processes.resolveEntry(owner, params);
841
+ if (method === "process.get")
842
+ return processes.get(owner, params);
843
+ if (method === "process.write")
844
+ return processes.write(owner, params);
845
+ if (method === "process.end")
846
+ return processes.end(owner, params);
741
847
  if (method === "process.spawn")
742
848
  return processes.start(owner, params);
743
849
  if (method === "process.cancel")
@@ -864,10 +970,10 @@ export function createPanelRuntime(options) {
864
970
  for (const grant of grants.values())
865
971
  remove(grant);
866
972
  processes.close();
867
- void agentTasks?.close();
868
973
  assets.clear();
869
974
  for (const preparation of preparing)
870
975
  preparation.cancelled = true;
976
+ return Promise.all([resources.shutdown(), Promise.resolve(agentTasks?.close())]).then(() => undefined);
871
977
  },
872
978
  async handle(request, response) {
873
979
  const url = new URL(request.url ?? "/", "http://localhost");
@@ -965,19 +1071,35 @@ export function createPanelRuntime(options) {
965
1071
  const value = await call(grant, await body(request));
966
1072
  if (!(await authorized(grant)))
967
1073
  error(410, "面板授权已失效,请重新打开。");
1074
+ if (Buffer.byteLength(JSON.stringify(value) ?? "null") > 3 * 1024 * 1024)
1075
+ throw new PanelBridgeError("RESULT_TOO_LARGE", "Panel result exceeds the response limit");
968
1076
  json(response, 200, value);
969
1077
  }
970
1078
  else
971
1079
  error(405, "不支持这个面板请求。");
972
1080
  }
973
1081
  catch (cause) {
974
- let status = cause instanceof PanelManagementError ? cause.status : 400;
1082
+ let status = cause instanceof PanelManagementError
1083
+ ? cause.status
1084
+ : cause instanceof PanelBridgeError && cause.code === "RATE_LIMITED"
1085
+ ? 429
1086
+ : 400;
975
1087
  if (activeGrant && !(await options.isAuthorized(request)))
976
1088
  status = 401;
977
1089
  else if (activeGrant && !(await authorized(activeGrant)))
978
1090
  status = 410;
979
1091
  json(response, status, {
980
1092
  error: cause instanceof Error ? cause.message : "面板请求失败。",
1093
+ code: status === 410
1094
+ ? "REVOKED"
1095
+ : status === 403
1096
+ ? "PERMISSION_DENIED"
1097
+ : status === 501
1098
+ ? "NOT_SUPPORTED"
1099
+ : panelBridgeFailure(cause).__codeshellPanelError.code,
1100
+ ...(cause instanceof PanelBridgeError && cause.retryAfterMs !== undefined
1101
+ ? { retryAfterMs: cause.retryAfterMs }
1102
+ : {}),
981
1103
  });
982
1104
  }
983
1105
  return true;
@@ -0,0 +1,58 @@
1
+ import { PanelAppProcessService, type PanelProcessOwner } from "./process-service.js";
2
+ import { PanelResourceService } from "./resources/service.js";
3
+ interface Scope {
4
+ appId: string;
5
+ projectPath: string;
6
+ revision: string;
7
+ }
8
+ interface Job {
9
+ id: string;
10
+ scope: Scope;
11
+ entry: {
12
+ name: string;
13
+ sha256: string;
14
+ };
15
+ input: unknown;
16
+ }
17
+ interface ExecutionContext {
18
+ workDir: string;
19
+ signal: AbortSignal;
20
+ reportProgress(progress: {
21
+ fraction?: number;
22
+ stage?: string;
23
+ message?: string;
24
+ }): Promise<void>;
25
+ }
26
+ interface ToolInput {
27
+ request: Record<string, unknown>;
28
+ resources?: Array<{
29
+ assetId: string;
30
+ path: string;
31
+ }>;
32
+ directoryArguments?: Array<{
33
+ argumentName: string;
34
+ directory: "job" | "app-data";
35
+ path?: string;
36
+ }>;
37
+ connectionIds?: string[];
38
+ connectionArgument?: string;
39
+ }
40
+ export interface PanelToolExecutorOptions {
41
+ processes: PanelAppProcessService;
42
+ resources: PanelResourceService;
43
+ /** Registered before any grants. The Host owns lifetime, approval and immutable app identity. */
44
+ owner(job: Job, send: PanelProcessOwner["send"]): PanelProcessOwner;
45
+ releaseOwner(owner: PanelProcessOwner): void;
46
+ appDataDirectory(scope: Scope): Promise<string>;
47
+ authorize(scope: Scope): Promise<void>;
48
+ authorizeConnections(scope: Scope): Promise<void>;
49
+ sealedRoot: string;
50
+ }
51
+ /** The only background processor: launch a reviewed tool and transport bounded JSON/files. */
52
+ export declare function createPanelToolExecutor(options: PanelToolExecutorOptions): {
53
+ prepareInput(scope: Scope, raw: unknown, workDir: string, signal: AbortSignal): Promise<ToolInput>;
54
+ execute(job: Job, context: ExecutionContext): Promise<{
55
+ artifacts?: unknown;
56
+ }>;
57
+ };
58
+ export {};