@cjhyy/code-shell-server 0.9.44 → 0.9.47
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/attachment-service.d.ts +2 -0
- package/dist/attachment-service.js +40 -27
- package/dist/bin/code-shell-link-cleanup.d.ts +2 -0
- package/dist/bin/code-shell-link-cleanup.js +3 -0
- package/dist/desktop-web/http-api.d.ts +3 -0
- package/dist/desktop-web/http-api.js +16 -1
- package/dist/hub/configuration.d.ts +6 -3
- package/dist/hub/configuration.js +19 -6
- package/dist/hub/mcp-probe.d.ts +4 -24
- package/dist/hub/mcp-probe.js +18 -389
- package/dist/hub/model-probe.d.ts +2 -20
- package/dist/hub/model-probe.js +2 -193
- package/dist/hub/run-replay.d.ts +7 -1
- package/dist/hub/run-replay.js +10 -2
- package/dist/hub/session-management.js +18 -53
- package/dist/hub/skills-management.js +6 -8
- package/dist/hub/uploads.d.ts +10 -0
- package/dist/hub/uploads.js +107 -15
- package/dist/index.desktop-web.d.ts +1 -0
- package/dist/index.desktop-web.js +1 -0
- package/dist/index.links.d.ts +3 -0
- package/dist/index.links.js +2 -0
- package/dist/index.panels.d.ts +1 -0
- package/dist/index.panels.js +1 -0
- package/dist/index.serve.d.ts +1 -0
- package/dist/index.serve.js +1 -0
- package/dist/index.storage.d.ts +1 -0
- package/dist/index.storage.js +1 -0
- package/dist/links/catalog.js +2 -0
- package/dist/links/diagnostics.d.ts +60 -0
- package/dist/links/diagnostics.js +184 -0
- package/dist/links/http.d.ts +10 -8
- package/dist/links/http.js +144 -125
- package/dist/links/legacy-cleanup-cli.d.ts +3 -0
- package/dist/links/legacy-cleanup-cli.js +64 -0
- package/dist/links/providers/defaults.d.ts +5 -0
- package/dist/links/providers/defaults.js +782 -0
- package/dist/links/providers/index.d.ts +7 -0
- package/dist/links/providers/index.js +22 -0
- package/dist/links/remote-catalog.d.ts +10 -3
- package/dist/links/remote-catalog.js +76 -29
- package/dist/links/runtime/operation.d.ts +9 -0
- package/dist/links/runtime/operation.js +16 -0
- package/dist/links/service.d.ts +18 -2
- package/dist/links/service.js +593 -62
- package/dist/links/types.d.ts +21 -1
- package/dist/mobile-remote/codex-parse.d.ts +3 -1
- package/dist/mobile-remote/codex-parse.js +45 -11
- package/dist/mobile-remote/codex-room-agent.d.ts +0 -6
- package/dist/mobile-remote/codex-room-agent.js +30 -32
- package/dist/mobile-remote/mobile-chat-turn.d.ts +1 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +21 -0
- package/dist/mobile-remote/mobile-history.d.ts +10 -1
- package/dist/mobile-remote/mobile-history.js +24 -16
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -0
- package/dist/mobile-remote/remote-host-manager.js +50 -13
- package/dist/mobile-remote/resident-agent.js +38 -22
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +106 -22
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -0
- package/dist/mobile-remote/room-process-stop.d.ts +3 -0
- package/dist/mobile-remote/room-process-stop.js +28 -0
- package/dist/model-trace-history.d.ts +29 -0
- package/dist/model-trace-history.js +198 -0
- package/dist/model-trace-http.d.ts +14 -0
- package/dist/model-trace-http.js +105 -0
- package/dist/panels/agent-task-host.js +1 -0
- package/dist/panels/agent-task-service.d.ts +2 -1
- package/dist/panels/agent-task-service.js +10 -3
- package/dist/panels/directory-bookmarks.js +62 -29
- package/dist/panels/execution-gate.d.ts +19 -1
- package/dist/panels/execution-gate.js +33 -5
- package/dist/panels/http.d.ts +1 -0
- package/dist/panels/hub-automations.js +1 -0
- package/dist/panels/hub-binding.js +5 -5
- package/dist/panels/model-trace-channel.d.ts +21 -0
- package/dist/panels/model-trace-channel.js +110 -0
- package/dist/panels/model-traces.d.ts +39 -0
- package/dist/panels/model-traces.js +222 -0
- package/dist/panels/process-service.d.ts +12 -2
- package/dist/panels/process-service.js +49 -6
- package/dist/panels/resources/uploads.js +15 -6
- package/dist/panels/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +66 -8
- package/dist/panels/task-cookies.js +8 -16
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +28 -3
- package/dist/serve/cli.js +8 -1
- package/dist/serve/headless-server.js +68 -14
- package/dist/sessions-service.js +17 -27
- package/dist/worker-bridge-core.js +11 -0
- package/package.json +8 -7
|
@@ -10,9 +10,13 @@ export function panelExecutionProject(path) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export class PanelExecutionBusyError extends Error {
|
|
13
|
+
blockers;
|
|
13
14
|
status = 409;
|
|
14
|
-
constructor(message
|
|
15
|
+
constructor(message,
|
|
16
|
+
/** Present when executions block a change; empty while another change runs. */
|
|
17
|
+
blockers = []) {
|
|
15
18
|
super(message);
|
|
19
|
+
this.blockers = blockers;
|
|
16
20
|
this.name = "PanelExecutionBusyError";
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -25,7 +29,11 @@ export class PanelExecutionGate {
|
|
|
25
29
|
sources = new Set();
|
|
26
30
|
mutations = new Set();
|
|
27
31
|
enter(scope) {
|
|
28
|
-
const held = {
|
|
32
|
+
const held = {
|
|
33
|
+
appId: scope.appId,
|
|
34
|
+
projectPath: panelExecutionProject(scope.projectPath),
|
|
35
|
+
...(scope.holder ? { holder: { ...scope.holder } } : {}),
|
|
36
|
+
};
|
|
29
37
|
if ([...this.mutations].some((matches) => matches(held)))
|
|
30
38
|
throw new PanelExecutionBusyError("面板正在更新,请完成后重试。");
|
|
31
39
|
this.executions.add(held);
|
|
@@ -49,13 +57,33 @@ export class PanelExecutionGate {
|
|
|
49
57
|
this.sources.delete(source);
|
|
50
58
|
};
|
|
51
59
|
}
|
|
60
|
+
/** Executions that a change matching `matches` would wait for, grouped by holder. */
|
|
61
|
+
blockers(matches) {
|
|
62
|
+
const grouped = new Map();
|
|
63
|
+
const scopes = [...this.executions, ...[...this.sources].flatMap((source) => [...source()])];
|
|
64
|
+
for (const scope of scopes) {
|
|
65
|
+
if (!matches(scope))
|
|
66
|
+
continue;
|
|
67
|
+
const blocker = {
|
|
68
|
+
appId: scope.appId,
|
|
69
|
+
projectPath: scope.projectPath,
|
|
70
|
+
...(scope.holder ? { holder: { ...scope.holder } } : {}),
|
|
71
|
+
};
|
|
72
|
+
const key = JSON.stringify(blocker), existing = grouped.get(key);
|
|
73
|
+
if (existing)
|
|
74
|
+
existing.count++;
|
|
75
|
+
else
|
|
76
|
+
grouped.set(key, { ...blocker, count: 1 });
|
|
77
|
+
}
|
|
78
|
+
return [...grouped.values()];
|
|
79
|
+
}
|
|
52
80
|
async mutate(matches, work) {
|
|
53
81
|
if (this.mutations.size)
|
|
54
82
|
throw new PanelExecutionBusyError("另一个面板修改正在进行,请稍后重试。");
|
|
55
83
|
// No await between the snapshot and installing the admission barrier.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new PanelExecutionBusyError("面板还有正在提交、排队或运行的任务,请结束后再修改。");
|
|
84
|
+
const blockers = this.blockers(matches);
|
|
85
|
+
if (blockers.length)
|
|
86
|
+
throw new PanelExecutionBusyError("面板还有正在提交、排队或运行的任务,请结束后再修改。", blockers);
|
|
59
87
|
this.mutations.add(matches);
|
|
60
88
|
try {
|
|
61
89
|
return await work();
|
package/dist/panels/http.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type PanelAgentTaskHostOptions } from "./agent-task-host.js";
|
|
|
7
7
|
import type { PanelProjectScopeBinding } from "./project-scope.js";
|
|
8
8
|
export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
9
9
|
dataDir: string;
|
|
10
|
+
traceStorageDir?: string;
|
|
10
11
|
host: "hub" | "desktop";
|
|
11
12
|
projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
|
|
12
13
|
publicPathPrefix?: string;
|
|
@@ -9,7 +9,7 @@ function refused() {
|
|
|
9
9
|
}
|
|
10
10
|
function metadata(file) {
|
|
11
11
|
try {
|
|
12
|
-
return lstatSync(file);
|
|
12
|
+
return lstatSync(file, { bigint: true });
|
|
13
13
|
}
|
|
14
14
|
catch (error) {
|
|
15
15
|
if (error.code === "ENOENT")
|
|
@@ -20,8 +20,8 @@ function metadata(file) {
|
|
|
20
20
|
function readPointer(file) {
|
|
21
21
|
const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
22
22
|
try {
|
|
23
|
-
const info = fstatSync(descriptor);
|
|
24
|
-
if (!info.isFile() || info.size >
|
|
23
|
+
const info = fstatSync(descriptor, { bigint: true });
|
|
24
|
+
if (!info.isFile() || info.size > 8192n)
|
|
25
25
|
refused();
|
|
26
26
|
const content = readFileSync(descriptor, "utf8");
|
|
27
27
|
if (Buffer.byteLength(content) > 8192 || content.includes("\0"))
|
|
@@ -54,13 +54,13 @@ function sameIdentity(actual, expected) {
|
|
|
54
54
|
}
|
|
55
55
|
function directoryGuard(path) {
|
|
56
56
|
const canonical = realpathSync(path);
|
|
57
|
-
const info = lstatSync(canonical);
|
|
57
|
+
const info = lstatSync(canonical, { bigint: true });
|
|
58
58
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
59
59
|
refused();
|
|
60
60
|
return () => {
|
|
61
61
|
if (realpathSync(path) !== canonical)
|
|
62
62
|
refused();
|
|
63
|
-
const current = lstatSync(canonical);
|
|
63
|
+
const current = lstatSync(canonical, { bigint: true });
|
|
64
64
|
if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(current, info))
|
|
65
65
|
refused();
|
|
66
66
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PanelModelTraceService, type PanelModelTraceOwner } from "./model-traces.js";
|
|
2
|
+
/** A dedicated reviewed-child descriptor; ordinary stdout/stderr never become prompts. */
|
|
3
|
+
export declare class PanelModelTraceChannel {
|
|
4
|
+
private readonly options;
|
|
5
|
+
private readonly decoder;
|
|
6
|
+
private readonly frames;
|
|
7
|
+
private buffer;
|
|
8
|
+
private readonly calls;
|
|
9
|
+
private tail;
|
|
10
|
+
private stopped;
|
|
11
|
+
private pending;
|
|
12
|
+
constructor(options: {
|
|
13
|
+
service: PanelModelTraceService;
|
|
14
|
+
owner: PanelModelTraceOwner;
|
|
15
|
+
isAuthorized(): Promise<boolean>;
|
|
16
|
+
});
|
|
17
|
+
write(chunk: Buffer | string): void;
|
|
18
|
+
finish(status: "cancelled" | "failed"): Promise<void>;
|
|
19
|
+
abort(status?: "cancelled" | "failed"): void;
|
|
20
|
+
private consume;
|
|
21
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
import { createNativeModelTraceFrameDecoder } from "@cjhyy/code-shell-core/internal";
|
|
3
|
+
import { panelModelTraceLimits, } from "./model-traces.js";
|
|
4
|
+
/** A dedicated reviewed-child descriptor; ordinary stdout/stderr never become prompts. */
|
|
5
|
+
export class PanelModelTraceChannel {
|
|
6
|
+
options;
|
|
7
|
+
decoder = new StringDecoder("utf8");
|
|
8
|
+
frames = createNativeModelTraceFrameDecoder();
|
|
9
|
+
buffer = "";
|
|
10
|
+
calls = new Map();
|
|
11
|
+
tail = Promise.resolve();
|
|
12
|
+
stopped = false;
|
|
13
|
+
pending = 0;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
write(chunk) {
|
|
18
|
+
if (this.stopped)
|
|
19
|
+
return;
|
|
20
|
+
this.buffer += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
|
|
21
|
+
let newline;
|
|
22
|
+
while ((newline = this.buffer.indexOf("\n")) >= 0) {
|
|
23
|
+
const physical = this.buffer.slice(0, newline);
|
|
24
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
25
|
+
if (!physical.trim())
|
|
26
|
+
continue;
|
|
27
|
+
let line;
|
|
28
|
+
try {
|
|
29
|
+
if (Buffer.byteLength(physical) > panelModelTraceLimits.maxParamsBytes)
|
|
30
|
+
throw new Error("Oversized model observation");
|
|
31
|
+
line = this.frames.read(physical);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
this.abort("failed");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (line === undefined || !line.trim())
|
|
38
|
+
continue;
|
|
39
|
+
if (++this.pending > 128) {
|
|
40
|
+
this.abort("failed");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.tail = this.tail.then(async () => {
|
|
44
|
+
try {
|
|
45
|
+
if (this.stopped || !(await this.options.isAuthorized())) {
|
|
46
|
+
this.abort();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (this.stopped)
|
|
50
|
+
return;
|
|
51
|
+
this.consume(line);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
this.abort("failed");
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
this.pending--;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (Buffer.byteLength(this.buffer) > panelModelTraceLimits.maxParamsBytes)
|
|
62
|
+
this.abort("failed");
|
|
63
|
+
}
|
|
64
|
+
async finish(status) {
|
|
65
|
+
this.buffer += this.decoder.end();
|
|
66
|
+
if (this.buffer.trim())
|
|
67
|
+
this.write("\n");
|
|
68
|
+
await this.tail;
|
|
69
|
+
this.abort(status);
|
|
70
|
+
}
|
|
71
|
+
abort(status = "cancelled") {
|
|
72
|
+
this.stopped = true;
|
|
73
|
+
this.buffer = "";
|
|
74
|
+
this.calls.clear();
|
|
75
|
+
this.frames.clear();
|
|
76
|
+
this.options.service.cancelOwner(this.options.owner.ownerKey, status);
|
|
77
|
+
}
|
|
78
|
+
consume(line) {
|
|
79
|
+
if (!line.trim())
|
|
80
|
+
return;
|
|
81
|
+
const value = JSON.parse(line);
|
|
82
|
+
if (!value ||
|
|
83
|
+
typeof value !== "object" ||
|
|
84
|
+
Array.isArray(value) ||
|
|
85
|
+
value.version !== 1 ||
|
|
86
|
+
value.type !== "model_trace" ||
|
|
87
|
+
!["start", "finish"].includes(value.phase) ||
|
|
88
|
+
typeof value.callId !== "string" ||
|
|
89
|
+
!/^[a-zA-Z0-9_-]{1,128}$/.test(value.callId))
|
|
90
|
+
throw new Error("Invalid model observation channel message");
|
|
91
|
+
const { version: _version, type: _type, phase, callId, ...params } = value;
|
|
92
|
+
if (Object.hasOwn(params, "id"))
|
|
93
|
+
throw new Error("Child cannot select a Host observation id");
|
|
94
|
+
if (phase === "start") {
|
|
95
|
+
if (this.calls.has(callId))
|
|
96
|
+
throw new Error("Duplicate model observation call");
|
|
97
|
+
const result = this.options.service.call(this.options.owner, "modelTraces.start", params);
|
|
98
|
+
if (!("id" in result))
|
|
99
|
+
throw new Error("Model observation was not started");
|
|
100
|
+
this.calls.set(callId, result.id);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const id = this.calls.get(callId);
|
|
104
|
+
if (!id)
|
|
105
|
+
throw new Error("Unknown model observation call");
|
|
106
|
+
this.options.service.call(this.options.owner, "modelTraces.finish", { ...params, id });
|
|
107
|
+
this.calls.delete(callId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const panelModelTraceMethods: string[];
|
|
2
|
+
export declare const panelModelTraceLimits: Readonly<{
|
|
3
|
+
maxParamsBytes: number;
|
|
4
|
+
maxActivePerOwner: 64;
|
|
5
|
+
maxActive: 2048;
|
|
6
|
+
maxCallsPerMinute: 240;
|
|
7
|
+
maxLifetimeMs: number;
|
|
8
|
+
}>;
|
|
9
|
+
/** Supplied by a verified Host binding; no field is taken from an observation. */
|
|
10
|
+
export interface PanelModelTraceOwner {
|
|
11
|
+
ownerKey: string;
|
|
12
|
+
appId: string;
|
|
13
|
+
revision: string;
|
|
14
|
+
workspacePath: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
traceId?: string;
|
|
17
|
+
taskId?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Write-only Panel observations share the Host's policy, sanitization and history store. */
|
|
20
|
+
export declare class PanelModelTraceService {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly active;
|
|
23
|
+
private readonly calls;
|
|
24
|
+
constructor(options?: {
|
|
25
|
+
storageDir?: string;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
});
|
|
28
|
+
call(owner: PanelModelTraceOwner, method: string, params: unknown): {
|
|
29
|
+
id: string;
|
|
30
|
+
recorded: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
recorded: boolean;
|
|
33
|
+
};
|
|
34
|
+
cancelOwner(ownerKey: string, status?: "cancelled" | "failed"): void;
|
|
35
|
+
closeAll(): void;
|
|
36
|
+
private close;
|
|
37
|
+
private now;
|
|
38
|
+
private prune;
|
|
39
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { recordExternalModelTrace, } from "@cjhyy/code-shell-core/internal";
|
|
4
|
+
import { PanelBridgeError } from "./bridge-contract.js";
|
|
5
|
+
export const panelModelTraceMethods = ["modelTraces.start", "modelTraces.finish"];
|
|
6
|
+
export const panelModelTraceLimits = Object.freeze({
|
|
7
|
+
maxParamsBytes: 256 * 1024,
|
|
8
|
+
maxActivePerOwner: 64,
|
|
9
|
+
maxActive: 2048,
|
|
10
|
+
maxCallsPerMinute: 240,
|
|
11
|
+
maxLifetimeMs: 24 * 60 * 60 * 1000,
|
|
12
|
+
});
|
|
13
|
+
function invalid(message = "Invalid Panel model observation") {
|
|
14
|
+
throw new PanelBridgeError("INVALID_ARGUMENT", message);
|
|
15
|
+
}
|
|
16
|
+
function object(value, keys) {
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
18
|
+
invalid();
|
|
19
|
+
if (Object.keys(value).some((key) => !keys.includes(key)))
|
|
20
|
+
invalid();
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function label(value, optional = false) {
|
|
24
|
+
if (optional && value === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (typeof value !== "string" || !value.trim() || value.length > 200 || /[\x00-\x1f]/.test(value))
|
|
27
|
+
invalid();
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function usage(value) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const input = object(value, [
|
|
34
|
+
"promptTokens",
|
|
35
|
+
"completionTokens",
|
|
36
|
+
"totalTokens",
|
|
37
|
+
"cacheReadTokens",
|
|
38
|
+
"cacheCreationTokens",
|
|
39
|
+
]);
|
|
40
|
+
for (const key of ["promptTokens", "completionTokens", "totalTokens"])
|
|
41
|
+
if (input[key] === undefined)
|
|
42
|
+
invalid("Model usage requires token counts");
|
|
43
|
+
for (const number of Object.values(input))
|
|
44
|
+
if (!Number.isSafeInteger(number) || Number(number) < 0)
|
|
45
|
+
invalid("Invalid model usage");
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
/** Write-only Panel observations share the Host's policy, sanitization and history store. */
|
|
49
|
+
export class PanelModelTraceService {
|
|
50
|
+
options;
|
|
51
|
+
active = new Map();
|
|
52
|
+
calls = new Map();
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
}
|
|
56
|
+
call(owner, method, params) {
|
|
57
|
+
if (!owner.ownerKey ||
|
|
58
|
+
!/^[a-z][a-z0-9-]{0,63}$/.test(owner.appId) ||
|
|
59
|
+
!owner.revision ||
|
|
60
|
+
!isAbsolute(owner.workspacePath))
|
|
61
|
+
invalid("Invalid Host model observation owner");
|
|
62
|
+
if (!panelModelTraceMethods.includes(method))
|
|
63
|
+
invalid("Unsupported model observation method");
|
|
64
|
+
const encoded = JSON.stringify(params);
|
|
65
|
+
if (!encoded || Buffer.byteLength(encoded) > panelModelTraceLimits.maxParamsBytes)
|
|
66
|
+
throw new PanelBridgeError("PARAMS_TOO_LARGE", "Panel model observation exceeds the limit");
|
|
67
|
+
const now = this.now();
|
|
68
|
+
this.prune(now);
|
|
69
|
+
const calls = (this.calls.get(owner.ownerKey) ?? []).filter((time) => now - time < 60_000);
|
|
70
|
+
if (calls.length >= panelModelTraceLimits.maxCallsPerMinute)
|
|
71
|
+
throw new PanelBridgeError("RATE_LIMITED", "Panel model observation rate limit exceeded");
|
|
72
|
+
calls.push(now);
|
|
73
|
+
this.calls.set(owner.ownerKey, calls);
|
|
74
|
+
if (method === "modelTraces.start") {
|
|
75
|
+
const value = object(params, [
|
|
76
|
+
"provider",
|
|
77
|
+
"model",
|
|
78
|
+
"purpose",
|
|
79
|
+
"input",
|
|
80
|
+
"inputState",
|
|
81
|
+
"stream",
|
|
82
|
+
]);
|
|
83
|
+
const provider = label(value.provider);
|
|
84
|
+
const model = label(value.model);
|
|
85
|
+
const purpose = label(value.purpose, true) ?? "model";
|
|
86
|
+
if (value.stream !== undefined && typeof value.stream !== "boolean")
|
|
87
|
+
invalid();
|
|
88
|
+
if (value.inputState !== undefined && value.inputState !== "truncated")
|
|
89
|
+
invalid();
|
|
90
|
+
const body = object(value.input, ["systemPrompt", "messages", "tools", "parameters"]);
|
|
91
|
+
if (body.systemPrompt !== undefined && typeof body.systemPrompt !== "string")
|
|
92
|
+
invalid();
|
|
93
|
+
for (const key of ["messages", "tools"])
|
|
94
|
+
if (body[key] !== undefined && !Array.isArray(body[key]))
|
|
95
|
+
invalid();
|
|
96
|
+
if (body.parameters !== undefined &&
|
|
97
|
+
(!body.parameters || typeof body.parameters !== "object" || Array.isArray(body.parameters)))
|
|
98
|
+
invalid();
|
|
99
|
+
if (this.active.size >= panelModelTraceLimits.maxActive ||
|
|
100
|
+
[...this.active.values()].filter((item) => item.ownerKey === owner.ownerKey).length >=
|
|
101
|
+
panelModelTraceLimits.maxActivePerOwner)
|
|
102
|
+
throw new PanelBridgeError("RATE_LIMITED", "Too many active Panel model observations");
|
|
103
|
+
const input = {
|
|
104
|
+
storageDir: this.options.storageDir,
|
|
105
|
+
workspacePath: owner.workspacePath,
|
|
106
|
+
ephemeral: false,
|
|
107
|
+
sessionId: owner.sessionId,
|
|
108
|
+
parentSessionId: undefined,
|
|
109
|
+
runId: owner.traceId,
|
|
110
|
+
source: `panel:${owner.appId}`,
|
|
111
|
+
purpose: `panel/${purpose}`,
|
|
112
|
+
provider,
|
|
113
|
+
model,
|
|
114
|
+
startedAt: now,
|
|
115
|
+
status: "running",
|
|
116
|
+
stream: value.stream,
|
|
117
|
+
coverage: "logical-input",
|
|
118
|
+
inputState: value.inputState,
|
|
119
|
+
input: {
|
|
120
|
+
systemPrompt: body.systemPrompt ?? "",
|
|
121
|
+
messages: body.messages ?? [],
|
|
122
|
+
tools: body.tools ?? [],
|
|
123
|
+
parameters: {
|
|
124
|
+
...body.parameters,
|
|
125
|
+
codeshellPanel: {
|
|
126
|
+
appId: owner.appId,
|
|
127
|
+
revision: owner.revision,
|
|
128
|
+
...(owner.taskId ? { taskId: owner.taskId } : {}),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const record = recordExternalModelTrace(input);
|
|
134
|
+
const id = record?.id ?? randomUUID();
|
|
135
|
+
// Retain only already-sanitized content. Disabled recording never resumes mid-call.
|
|
136
|
+
this.active.set(id, {
|
|
137
|
+
ownerKey: owner.ownerKey,
|
|
138
|
+
ownerIdentity: JSON.stringify([
|
|
139
|
+
owner.appId,
|
|
140
|
+
owner.revision,
|
|
141
|
+
owner.workspacePath,
|
|
142
|
+
owner.sessionId,
|
|
143
|
+
]),
|
|
144
|
+
recordId: record?.id,
|
|
145
|
+
startedAt: now,
|
|
146
|
+
input: { ...input, input: undefined },
|
|
147
|
+
});
|
|
148
|
+
return { id, recorded: !!record };
|
|
149
|
+
}
|
|
150
|
+
const value = object(params, ["id", "status", "output", "outputState", "error", "usage"]);
|
|
151
|
+
if (typeof value.id !== "string" || !/^[a-f0-9-]{36}$/i.test(value.id))
|
|
152
|
+
invalid();
|
|
153
|
+
const observation = this.active.get(value.id);
|
|
154
|
+
if (!observation ||
|
|
155
|
+
observation.ownerKey !== owner.ownerKey ||
|
|
156
|
+
observation.ownerIdentity !==
|
|
157
|
+
JSON.stringify([owner.appId, owner.revision, owner.workspacePath, owner.sessionId]))
|
|
158
|
+
throw new PanelBridgeError("REVOKED", "Model observation is unavailable for this Panel owner");
|
|
159
|
+
if (!["completed", "failed", "cancelled"].includes(String(value.status)))
|
|
160
|
+
invalid();
|
|
161
|
+
if (value.outputState !== undefined && value.outputState !== "truncated")
|
|
162
|
+
invalid();
|
|
163
|
+
const output = value.output === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: object(value.output, ["text", "thinking", "toolCalls", "stopReason", "data"]);
|
|
166
|
+
for (const key of ["text", "thinking", "stopReason"])
|
|
167
|
+
if (output?.[key] !== undefined && typeof output[key] !== "string")
|
|
168
|
+
invalid();
|
|
169
|
+
if (output?.toolCalls !== undefined && !Array.isArray(output.toolCalls))
|
|
170
|
+
invalid();
|
|
171
|
+
if (value.error !== undefined && typeof value.error !== "string")
|
|
172
|
+
invalid();
|
|
173
|
+
const tokens = usage(value.usage);
|
|
174
|
+
this.active.delete(value.id);
|
|
175
|
+
const record = observation.recordId
|
|
176
|
+
? recordExternalModelTrace({
|
|
177
|
+
...observation.input,
|
|
178
|
+
id: observation.recordId,
|
|
179
|
+
endedAt: now,
|
|
180
|
+
status: value.status,
|
|
181
|
+
output,
|
|
182
|
+
outputState: value.outputState,
|
|
183
|
+
error: value.error,
|
|
184
|
+
usage: tokens,
|
|
185
|
+
})
|
|
186
|
+
: null;
|
|
187
|
+
return { recorded: !!record };
|
|
188
|
+
}
|
|
189
|
+
cancelOwner(ownerKey, status = "cancelled") {
|
|
190
|
+
for (const [id, observation] of this.active)
|
|
191
|
+
if (observation.ownerKey === ownerKey)
|
|
192
|
+
this.close(id, observation, status);
|
|
193
|
+
this.calls.delete(ownerKey);
|
|
194
|
+
}
|
|
195
|
+
closeAll() {
|
|
196
|
+
for (const [id, observation] of this.active)
|
|
197
|
+
this.close(id, observation, "cancelled");
|
|
198
|
+
this.calls.clear();
|
|
199
|
+
}
|
|
200
|
+
close(id, observation, status) {
|
|
201
|
+
this.active.delete(id);
|
|
202
|
+
if (observation.recordId)
|
|
203
|
+
recordExternalModelTrace({
|
|
204
|
+
...observation.input,
|
|
205
|
+
id: observation.recordId,
|
|
206
|
+
endedAt: this.now(),
|
|
207
|
+
status,
|
|
208
|
+
error: status === "failed" ? "Panel process exited before the model call finished" : undefined,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
now() {
|
|
212
|
+
return (this.options.now ?? Date.now)();
|
|
213
|
+
}
|
|
214
|
+
prune(now) {
|
|
215
|
+
for (const [id, observation] of this.active)
|
|
216
|
+
if (now - observation.startedAt > panelModelTraceLimits.maxLifetimeMs)
|
|
217
|
+
this.close(id, observation, "cancelled");
|
|
218
|
+
for (const [owner, calls] of this.calls)
|
|
219
|
+
if (calls.every((time) => now - time >= 60_000))
|
|
220
|
+
this.calls.delete(owner);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PanelModelTraceService, PanelModelTraceOwner } from "./model-traces.js";
|
|
1
2
|
export { processLimits } from "./process-state.js";
|
|
2
3
|
export interface PanelProcessApprovalScope {
|
|
3
4
|
appId: string;
|
|
@@ -13,6 +14,9 @@ export interface PanelProcessOwner {
|
|
|
13
14
|
send(event: "process.output" | "process.exit", payload: Record<string, unknown>): void;
|
|
14
15
|
}
|
|
15
16
|
export interface PanelAppProcessServiceOptions {
|
|
17
|
+
/** Dedicated descriptor only for a verified installed package entry. */
|
|
18
|
+
modelTraces?: PanelModelTraceService;
|
|
19
|
+
modelTraceOwner?(owner: PanelProcessOwner): Omit<PanelModelTraceOwner, "ownerKey"> | undefined;
|
|
16
20
|
/** Host lease from before async preparation until the actual process group has exited. */
|
|
17
21
|
acquireExecution?(owner: PanelProcessOwner): () => void;
|
|
18
22
|
env?: NodeJS.ProcessEnv;
|
|
@@ -159,7 +163,13 @@ export declare class PanelAppProcessService {
|
|
|
159
163
|
}>;
|
|
160
164
|
private sendInput;
|
|
161
165
|
revokeGuest(guestId: number): void;
|
|
162
|
-
/**
|
|
163
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Permanently shut down all guests, including operations awaiting approval.
|
|
168
|
+
* Resolves once every native process group has exited or been force-killed:
|
|
169
|
+
* escalation runs on a Host timer, so a Host that exits earlier would leave
|
|
170
|
+
* a process ignoring SIGTERM running.
|
|
171
|
+
*/
|
|
172
|
+
close(): Promise<void>;
|
|
173
|
+
private shutDown;
|
|
164
174
|
private terminate;
|
|
165
175
|
}
|
|
@@ -4,6 +4,7 @@ import { constants, lstatSync, realpathSync } from "node:fs";
|
|
|
4
4
|
import { access, realpath, stat } from "node:fs/promises";
|
|
5
5
|
import { basename, delimiter, extname, join } from "node:path";
|
|
6
6
|
import { killProcessGroup } from "@cjhyy/code-shell-core/extension";
|
|
7
|
+
import { PanelModelTraceChannel } from "./model-trace-channel.js";
|
|
7
8
|
import { verifyProcessEntry } from "./process-entry.js";
|
|
8
9
|
import { appendProcessEvent, ProcessReceipts, processLimits, sameProcessOwner, } from "./process-state.js";
|
|
9
10
|
export { processLimits } from "./process-state.js";
|
|
@@ -231,7 +232,7 @@ export class PanelAppProcessService {
|
|
|
231
232
|
options.assertCurrent?.();
|
|
232
233
|
await this.authorize(owner, epoch);
|
|
233
234
|
options.assertCurrent?.();
|
|
234
|
-
const info = await stat(resolved);
|
|
235
|
+
const info = await stat(resolved, { bigint: true });
|
|
235
236
|
options.assertCurrent?.();
|
|
236
237
|
await this.authorize(owner, epoch);
|
|
237
238
|
options.assertCurrent?.();
|
|
@@ -327,7 +328,7 @@ export class PanelAppProcessService {
|
|
|
327
328
|
}
|
|
328
329
|
// Final synchronous identity check preserves the existing handle API and avoids
|
|
329
330
|
// an await between validating a cwd and spawning the child into that directory.
|
|
330
|
-
const current = lstatSync(grant.path);
|
|
331
|
+
const current = lstatSync(grant.path, { bigint: true });
|
|
331
332
|
if (!current.isDirectory() ||
|
|
332
333
|
current.isSymbolicLink() ||
|
|
333
334
|
current.dev !== grant.dev ||
|
|
@@ -518,12 +519,22 @@ export class PanelAppProcessService {
|
|
|
518
519
|
if (!this.options.allowAuthorizedProcessWithoutPrompt)
|
|
519
520
|
this.approvedExecutables.add(approvalKey);
|
|
520
521
|
const processId = randomUUID();
|
|
522
|
+
const modelTraceOwner = entry && this.options.modelTraces ? this.options.modelTraceOwner?.(owner) : undefined;
|
|
523
|
+
// Never inherit a caller's descriptor claim for an ordinary executable.
|
|
524
|
+
delete env.CODESHELL_MODEL_TRACE_FD;
|
|
525
|
+
if (modelTraceOwner)
|
|
526
|
+
env.CODESHELL_MODEL_TRACE_FD = "3";
|
|
521
527
|
const child = spawn(executable.path, args, {
|
|
522
528
|
cwd,
|
|
523
529
|
detached: process.platform !== "win32",
|
|
524
530
|
env,
|
|
525
531
|
shell: false,
|
|
526
|
-
stdio: [
|
|
532
|
+
stdio: [
|
|
533
|
+
input.stdin === "pipe" ? "pipe" : "ignore",
|
|
534
|
+
"pipe",
|
|
535
|
+
"pipe",
|
|
536
|
+
...(modelTraceOwner ? ["pipe"] : []),
|
|
537
|
+
],
|
|
527
538
|
windowsHide: true,
|
|
528
539
|
});
|
|
529
540
|
const record = {
|
|
@@ -607,6 +618,28 @@ export class PanelAppProcessService {
|
|
|
607
618
|
}),
|
|
608
619
|
};
|
|
609
620
|
this.processes.set(processId, running);
|
|
621
|
+
if (modelTraceOwner && this.options.modelTraces) {
|
|
622
|
+
const channel = new PanelModelTraceChannel({
|
|
623
|
+
service: this.options.modelTraces,
|
|
624
|
+
owner: {
|
|
625
|
+
...modelTraceOwner,
|
|
626
|
+
ownerKey: `process:${processId}`,
|
|
627
|
+
traceId: modelTraceOwner.traceId ?? processId,
|
|
628
|
+
},
|
|
629
|
+
isAuthorized: async () => {
|
|
630
|
+
try {
|
|
631
|
+
await this.authorize(owner, epoch);
|
|
632
|
+
return this.processes.get(processId) === running && !running.termination;
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
running.modelTraceChannel = channel;
|
|
640
|
+
child.stdio[3]?.on("data", (chunk) => channel.write(chunk));
|
|
641
|
+
child.stdio[3]?.on("error", () => channel.abort("failed"));
|
|
642
|
+
}
|
|
610
643
|
let checkingInputs = false;
|
|
611
644
|
const inputChecks = fileArgumentHandles.some((handle) => this.fileArguments.get(handle)?.validate)
|
|
612
645
|
? setInterval(() => {
|
|
@@ -651,6 +684,7 @@ export class PanelAppProcessService {
|
|
|
651
684
|
});
|
|
652
685
|
}
|
|
653
686
|
}
|
|
687
|
+
await running.modelTraceChannel?.finish(record.cancelRequested ? "cancelled" : "failed");
|
|
654
688
|
finishExit({ code: finalCode, signal });
|
|
655
689
|
this.retiring.delete(processId);
|
|
656
690
|
if (this.processes.get(processId)?.child !== child)
|
|
@@ -879,10 +913,18 @@ export class PanelAppProcessService {
|
|
|
879
913
|
}
|
|
880
914
|
}
|
|
881
915
|
}
|
|
882
|
-
/**
|
|
916
|
+
/**
|
|
917
|
+
* Permanently shut down all guests, including operations awaiting approval.
|
|
918
|
+
* Resolves once every native process group has exited or been force-killed:
|
|
919
|
+
* escalation runs on a Host timer, so a Host that exits earlier would leave
|
|
920
|
+
* a process ignoring SIGTERM running.
|
|
921
|
+
*/
|
|
883
922
|
close() {
|
|
884
|
-
if (this.closed)
|
|
885
|
-
|
|
923
|
+
if (!this.closed)
|
|
924
|
+
this.shutDown();
|
|
925
|
+
return Promise.all([...this.retiring.values()].map((running) => running.termination ?? Promise.resolve())).then(() => undefined);
|
|
926
|
+
}
|
|
927
|
+
shutDown() {
|
|
886
928
|
this.closed = true;
|
|
887
929
|
const guests = new Set([
|
|
888
930
|
...this.guestEpochs.keys(),
|
|
@@ -901,6 +943,7 @@ export class PanelAppProcessService {
|
|
|
901
943
|
terminate(running) {
|
|
902
944
|
if (running.termination)
|
|
903
945
|
return;
|
|
946
|
+
running.modelTraceChannel?.abort();
|
|
904
947
|
running.record.cancelRequested = true;
|
|
905
948
|
running.record.status = "stopping";
|
|
906
949
|
const pid = running.child.pid;
|