@cjhyy/code-shell-server 0.9.43 → 0.9.46
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/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 +13 -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 +8 -0
- package/dist/mobile-remote/remote-host-manager.js +23 -8
- package/dist/mobile-remote/resident-agent.js +17 -4
- package/dist/mobile-remote/room-manager.d.ts +23 -13
- package/dist/mobile-remote/room-manager.js +102 -20
- package/dist/mobile-remote/room-model-tracing.d.ts +18 -0
- package/dist/mobile-remote/room-model-tracing.js +196 -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/directory-bookmarks.js +62 -29
- package/dist/panels/http.d.ts +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/runtime.d.ts +1 -0
- package/dist/panels/runtime.js +54 -6
- package/dist/panels/tool-jobs.d.ts +2 -0
- package/dist/panels/tool-jobs.js +21 -1
- 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
|
@@ -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;
|
package/dist/panels/runtime.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface PanelRuntimeOptions {
|
|
|
34
34
|
bindingCwd?: string;
|
|
35
35
|
projectPackages?: boolean;
|
|
36
36
|
dataDir: string;
|
|
37
|
+
traceStorageDir?: string;
|
|
37
38
|
host: "hub" | "desktop";
|
|
38
39
|
/** Host authority only, never a field accepted from Panel HTTP requests. */
|
|
39
40
|
projectScope?: () => Promise<PanelProjectScopeBinding | undefined>;
|
package/dist/panels/runtime.js
CHANGED
|
@@ -10,6 +10,7 @@ import { panelRuntimeApiVersion, panelProcessMethods, panelResourceMethods, pane
|
|
|
10
10
|
import { PanelResourceService } from "./resources/service.js";
|
|
11
11
|
import { servePanelResource } from "./resources/http.js";
|
|
12
12
|
import { panelConnections, panelConnectionIds, materializePanelConnections, } from "./connections.js";
|
|
13
|
+
import { PanelModelTraceService, panelModelTraceMethods, panelModelTraceLimits, } from "./model-traces.js";
|
|
13
14
|
import { PanelRuntimeServices } from "./runtime-services.js";
|
|
14
15
|
import { PanelToolJobService, toolJobLimits, } from "./tool-jobs.js";
|
|
15
16
|
import { createPanelToolExecutor } from "./tool-executor.js";
|
|
@@ -26,6 +27,7 @@ const MAX_FILE = 16 * 1024 * 1024;
|
|
|
26
27
|
const GRANT_TTL = 30 * 60_000;
|
|
27
28
|
const METHODS = [
|
|
28
29
|
"context.get",
|
|
30
|
+
...panelModelTraceMethods,
|
|
29
31
|
"storage.get",
|
|
30
32
|
"storage.getSnapshot",
|
|
31
33
|
"storage.compareAndSet",
|
|
@@ -80,6 +82,7 @@ const MIME = {
|
|
|
80
82
|
".html": "text/html; charset=utf-8",
|
|
81
83
|
".js": "text/javascript; charset=utf-8",
|
|
82
84
|
".mjs": "text/javascript; charset=utf-8",
|
|
85
|
+
".wasm": "application/wasm",
|
|
83
86
|
".css": "text/css; charset=utf-8",
|
|
84
87
|
".json": "application/json",
|
|
85
88
|
".md": "text/markdown; charset=utf-8",
|
|
@@ -420,6 +423,10 @@ export function createPanelRuntime(options) {
|
|
|
420
423
|
const assets = new Map();
|
|
421
424
|
const now = options.now ?? Date.now;
|
|
422
425
|
const services = new PanelRuntimeServices({ dataDir: options.dataDir });
|
|
426
|
+
const modelTraces = new PanelModelTraceService({
|
|
427
|
+
storageDir: options.traceStorageDir,
|
|
428
|
+
now: options.now,
|
|
429
|
+
});
|
|
423
430
|
const installed = options.listInstalled ??
|
|
424
431
|
(options.projectPackages
|
|
425
432
|
? async (appId) => (await inspectProjectPanelApps(options.bindingCwd ?? options.cwd, appId)).apps
|
|
@@ -522,6 +529,27 @@ export function createPanelRuntime(options) {
|
|
|
522
529
|
return app;
|
|
523
530
|
}
|
|
524
531
|
const processes = new PanelAppProcessService({
|
|
532
|
+
modelTraces,
|
|
533
|
+
modelTraceOwner: (owner) => {
|
|
534
|
+
const task = toolOwners.get(owner.guestId);
|
|
535
|
+
if (task)
|
|
536
|
+
return {
|
|
537
|
+
appId: owner.appId,
|
|
538
|
+
revision: owner.revision,
|
|
539
|
+
workspacePath: options.cwd,
|
|
540
|
+
traceId: task.taskId,
|
|
541
|
+
taskId: task.taskId,
|
|
542
|
+
};
|
|
543
|
+
const grant = byGuest.get(owner.guestId);
|
|
544
|
+
if (!grant || grant.revision !== owner.revision)
|
|
545
|
+
return undefined;
|
|
546
|
+
return {
|
|
547
|
+
appId: owner.appId,
|
|
548
|
+
revision: owner.revision,
|
|
549
|
+
workspacePath: options.cwd,
|
|
550
|
+
sessionId: typeof grant.context.sessionId === "string" ? grant.context.sessionId : undefined,
|
|
551
|
+
};
|
|
552
|
+
},
|
|
525
553
|
acquireExecution: (owner) => panelExecutionGate.enter({
|
|
526
554
|
appId: owner.appId,
|
|
527
555
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
@@ -751,7 +779,7 @@ export function createPanelRuntime(options) {
|
|
|
751
779
|
resources,
|
|
752
780
|
owner: (job, send) => {
|
|
753
781
|
const guestId = nextToolOwner--;
|
|
754
|
-
toolOwners.set(guestId, job.scope);
|
|
782
|
+
toolOwners.set(guestId, { ...job.scope, taskId: job.id });
|
|
755
783
|
return {
|
|
756
784
|
guestId,
|
|
757
785
|
appId: job.scope.appId,
|
|
@@ -826,6 +854,7 @@ export function createPanelRuntime(options) {
|
|
|
826
854
|
assets.delete(grant.asset);
|
|
827
855
|
byGuest.delete(grant.guestId);
|
|
828
856
|
processes.revokeGuest(grant.guestId);
|
|
857
|
+
modelTraces.cancelOwner(`grant:${grant.id}`);
|
|
829
858
|
agentTasks?.revokeInstance(grant.id);
|
|
830
859
|
for (const item of grant.confirmations.values())
|
|
831
860
|
item.finish(false);
|
|
@@ -1089,6 +1118,8 @@ export function createPanelRuntime(options) {
|
|
|
1089
1118
|
locale: typeof input.locale === "string" ? input.locale.slice(0, 32) : "zh-CN",
|
|
1090
1119
|
apiVersion: panelRuntimeApiVersion,
|
|
1091
1120
|
capabilities: {
|
|
1121
|
+
packageAssets: { wasm: true, fetch: "package" },
|
|
1122
|
+
modelTraces: { writeOnly: true, coverage: "logical-input", ...panelModelTraceLimits },
|
|
1092
1123
|
...panelRuntimeCapabilities({
|
|
1093
1124
|
process: app.permissions.includes("process"),
|
|
1094
1125
|
cookieProcess: app.permissions.includes("process") &&
|
|
@@ -1189,7 +1220,9 @@ export function createPanelRuntime(options) {
|
|
|
1189
1220
|
return false;
|
|
1190
1221
|
if (method.startsWith("tools."))
|
|
1191
1222
|
return !!app.agent?.tools.length;
|
|
1192
|
-
return (method === "context.get" ||
|
|
1223
|
+
return (method === "context.get" ||
|
|
1224
|
+
method.startsWith("modelTraces.") ||
|
|
1225
|
+
app.permissions.includes(methodPermission(method)));
|
|
1193
1226
|
}),
|
|
1194
1227
|
limitations: panel.compatibility.reasons,
|
|
1195
1228
|
};
|
|
@@ -1302,6 +1335,14 @@ export function createPanelRuntime(options) {
|
|
|
1302
1335
|
grant.calls = history;
|
|
1303
1336
|
if (method === "context.get")
|
|
1304
1337
|
return grant.context;
|
|
1338
|
+
if (method.startsWith("modelTraces."))
|
|
1339
|
+
return modelTraces.call({
|
|
1340
|
+
ownerKey: `grant:${grant.id}`,
|
|
1341
|
+
appId: grant.app.id,
|
|
1342
|
+
revision: grant.revision,
|
|
1343
|
+
workspacePath: options.cwd,
|
|
1344
|
+
sessionId: typeof grant.context.sessionId === "string" ? grant.context.sessionId : undefined,
|
|
1345
|
+
}, method, params);
|
|
1305
1346
|
if (method.startsWith("tasks."))
|
|
1306
1347
|
return dispatchToolJobs(grant, method, params);
|
|
1307
1348
|
if (method.startsWith("tools.")) {
|
|
@@ -1600,6 +1641,7 @@ export function createPanelRuntime(options) {
|
|
|
1600
1641
|
return {
|
|
1601
1642
|
effect: method,
|
|
1602
1643
|
prompt: value.prompt,
|
|
1644
|
+
modelTraceSource: `panel:${grant.app.id}`,
|
|
1603
1645
|
sessionId: grant.context.sessionId,
|
|
1604
1646
|
...(value.displayText ? { displayText: value.displayText } : {}),
|
|
1605
1647
|
};
|
|
@@ -1692,6 +1734,7 @@ export function createPanelRuntime(options) {
|
|
|
1692
1734
|
},
|
|
1693
1735
|
async close() {
|
|
1694
1736
|
closed = true;
|
|
1737
|
+
modelTraces.closeAll();
|
|
1695
1738
|
for (const preparation of preparingTools)
|
|
1696
1739
|
preparation.controller.abort();
|
|
1697
1740
|
clearInterval(reaper);
|
|
@@ -1705,8 +1748,11 @@ export function createPanelRuntime(options) {
|
|
|
1705
1748
|
await taskCookies?.shutdown();
|
|
1706
1749
|
}
|
|
1707
1750
|
finally {
|
|
1708
|
-
|
|
1709
|
-
|
|
1751
|
+
await Promise.all([
|
|
1752
|
+
processes.close(),
|
|
1753
|
+
resources.shutdown(),
|
|
1754
|
+
Promise.resolve(agentTasks?.close()),
|
|
1755
|
+
]);
|
|
1710
1756
|
}
|
|
1711
1757
|
},
|
|
1712
1758
|
async handle(request, response) {
|
|
@@ -1936,7 +1982,7 @@ export function createPanelRuntime(options) {
|
|
|
1936
1982
|
// The embedding iframe must enable the same permission.
|
|
1937
1983
|
response.setHeader("Content-Security-Policy", "default-src 'none'; script-src " +
|
|
1938
1984
|
assetSource +
|
|
1939
|
-
"; style-src " +
|
|
1985
|
+
" 'wasm-unsafe-eval'; style-src " +
|
|
1940
1986
|
assetSource +
|
|
1941
1987
|
" 'unsafe-inline'; img-src " +
|
|
1942
1988
|
assetSource +
|
|
@@ -1944,7 +1990,9 @@ export function createPanelRuntime(options) {
|
|
|
1944
1990
|
assetSource +
|
|
1945
1991
|
" blob:; font-src " +
|
|
1946
1992
|
assetSource +
|
|
1947
|
-
" data:; connect-src
|
|
1993
|
+
" data:; connect-src " +
|
|
1994
|
+
assetSource +
|
|
1995
|
+
"; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; sandbox allow-scripts allow-downloads; frame-ancestors " +
|
|
1948
1996
|
resourceOrigin);
|
|
1949
1997
|
response.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), display-capture=()");
|
|
1950
1998
|
}
|
|
@@ -120,6 +120,8 @@ export declare class PanelToolJobService {
|
|
|
120
120
|
initialize(): Promise<void>;
|
|
121
121
|
private load;
|
|
122
122
|
private commit;
|
|
123
|
+
/** Make a job state visible to readers and subscribers. */
|
|
124
|
+
private publish;
|
|
123
125
|
private lookup;
|
|
124
126
|
/** Scoped existence check for transports merging read-only legacy history. */
|
|
125
127
|
has(rawScope: ToolJobScope, id: string): Promise<boolean>;
|
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -283,6 +283,10 @@ export class PanelToolJobService {
|
|
|
283
283
|
job.updatedAt = this.now();
|
|
284
284
|
job.sequence++;
|
|
285
285
|
await this.storage.write(job.id, job, toolJobLimits.maxRecordBytes);
|
|
286
|
+
this.publish(job);
|
|
287
|
+
}
|
|
288
|
+
/** Make a job state visible to readers and subscribers. */
|
|
289
|
+
publish(job) {
|
|
286
290
|
this.jobs.set(job.id, job);
|
|
287
291
|
try {
|
|
288
292
|
this.options.onEvent?.(publicJob(job));
|
|
@@ -670,7 +674,23 @@ export class PanelToolJobService {
|
|
|
670
674
|
next.result = result;
|
|
671
675
|
next.progress = { ...next.progress, fraction: 1 };
|
|
672
676
|
}
|
|
673
|
-
|
|
677
|
+
try {
|
|
678
|
+
await this.commit(next);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
// The executor has exited; a job that stays "running" in memory would
|
|
682
|
+
// count as active forever and hide the outcome. Publish an explicit
|
|
683
|
+
// failure for this Host session instead (keeping any result), while
|
|
684
|
+
// the stale disk record is recovered as interrupted by a later Host.
|
|
685
|
+
const outcome = next.error ? `${next.status} (${next.error.message})` : next.status;
|
|
686
|
+
next.status = "failed";
|
|
687
|
+
next.error = {
|
|
688
|
+
code: "TOOL_JOB_STORAGE_FAILED",
|
|
689
|
+
message: `Task ${outcome} but its final state could not be saved: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2000),
|
|
690
|
+
retryable: next.recovery === "retry",
|
|
691
|
+
};
|
|
692
|
+
this.publish(next);
|
|
693
|
+
}
|
|
674
694
|
});
|
|
675
695
|
}
|
|
676
696
|
async cancel(rawScope, id) {
|
package/dist/serve/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, resolve } from "node:path";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { startHeadlessServer } from "./headless-server.js";
|
|
12
12
|
import { startProjectControlServer } from "../project-runtime/control-server.js";
|
|
13
|
+
import { configureModelTracing } from "@cjhyy/code-shell-core/internal";
|
|
13
14
|
const SERVE_HELP = `Usage: code-shell-serve [options]
|
|
14
15
|
|
|
15
16
|
Run the CodeShell headless Web host for a workspace.
|
|
@@ -127,7 +128,7 @@ export function resolveWorkerEntry() {
|
|
|
127
128
|
* wrapper registered it; arena/pet stay desktop-only).
|
|
128
129
|
*/
|
|
129
130
|
export function resolveWorkerCapabilityModules() {
|
|
130
|
-
return `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule`;
|
|
131
|
+
return `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule,${import.meta.resolve("@cjhyy/code-shell-server/links")}#createLinkProviderModule`;
|
|
131
132
|
}
|
|
132
133
|
/** Locate the built browser app (packages/web `app` build), when present. */
|
|
133
134
|
export function resolveWebAppRoot() {
|
|
@@ -150,6 +151,12 @@ export async function runServeCli(argv = process.argv.slice(2)) {
|
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
const parsed = parseServeArgs(argv);
|
|
154
|
+
if (parsed.runtime === "local") {
|
|
155
|
+
configureModelTracing({
|
|
156
|
+
storageDir: join(parsed.dataDir, "worker", "sessions", ".model-traces"),
|
|
157
|
+
source: "server",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
153
160
|
const staticRootDir = parsed.staticRootDir ?? resolveWebAppRoot();
|
|
154
161
|
const server = parsed.runtime === "docker"
|
|
155
162
|
? await startProjectControlServer({
|