@cjhyy/code-shell-server 0.9.10 → 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.
- package/dist/index.panels.d.ts +8 -0
- package/dist/index.panels.js +8 -0
- package/dist/panels/bridge-contract.d.ts +72 -0
- package/dist/panels/bridge-contract.js +92 -0
- package/dist/panels/connections.d.ts +31 -0
- package/dist/panels/connections.js +118 -0
- package/dist/panels/process-entry.d.ts +2 -0
- package/dist/panels/process-entry.js +52 -0
- package/dist/panels/process-service.d.ts +61 -0
- package/dist/panels/process-service.js +367 -33
- package/dist/panels/process-state.d.ts +58 -0
- package/dist/panels/process-state.js +87 -0
- package/dist/panels/resources/directories.d.ts +12 -0
- package/dist/panels/resources/directories.js +89 -0
- package/dist/panels/resources/library.d.ts +63 -0
- package/dist/panels/resources/library.js +423 -0
- package/dist/panels/resources/service.d.ts +45 -0
- package/dist/panels/resources/service.js +317 -0
- package/dist/panels/resources/storage.d.ts +10 -0
- package/dist/panels/resources/storage.js +102 -0
- package/dist/panels/resources/types.d.ts +16 -0
- package/dist/panels/resources/types.js +1 -0
- package/dist/panels/resources/uploads.d.ts +64 -0
- package/dist/panels/resources/uploads.js +527 -0
- package/dist/panels/runtime.d.ts +1 -1
- package/dist/panels/runtime.js +137 -15
- package/dist/panels/tool-executor.d.ts +58 -0
- package/dist/panels/tool-executor.js +421 -0
- package/dist/panels/tool-jobs-storage.d.ts +16 -0
- package/dist/panels/tool-jobs-storage.js +169 -0
- package/dist/panels/tool-jobs.d.ts +103 -0
- package/dist/panels/tool-jobs.js +530 -0
- package/package.json +5 -5
package/dist/panels/runtime.js
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
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:
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
|
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 {};
|