@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,14 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ModelTraceListQuery } from "@cjhyy/code-shell-core/internal";
|
|
3
|
+
import { type ModelTraceReader } from "./model-trace-history.js";
|
|
4
|
+
/** Authenticated read-only facade over one Host-owned store and one fixed workspace. */
|
|
5
|
+
export declare function createModelTraceHttp(options: {
|
|
6
|
+
storageDir: string;
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
isAuthorized: (request: IncomingMessage) => boolean | Promise<boolean>;
|
|
9
|
+
/** Recomputed for each authenticated request and immediately before responding. */
|
|
10
|
+
resolveWorkspaceScopes?: (request: IncomingMessage) => ModelTraceListQuery["workspaceScopes"] | Promise<ModelTraceListQuery["workspaceScopes"]>;
|
|
11
|
+
store?: ModelTraceReader;
|
|
12
|
+
}): {
|
|
13
|
+
handle(request: IncomingMessage, response: ServerResponse): Promise<boolean>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createModelTraceHistory, ModelTraceRequestError, } from "./model-trace-history.js";
|
|
3
|
+
const ROOT = "/api/v1/model-traces";
|
|
4
|
+
function json(response, status, body) {
|
|
5
|
+
if (response.destroyed || response.writableEnded)
|
|
6
|
+
return;
|
|
7
|
+
response.writeHead(status, {
|
|
8
|
+
"content-type": "application/json; charset=utf-8",
|
|
9
|
+
"cache-control": "no-store",
|
|
10
|
+
"x-content-type-options": "nosniff",
|
|
11
|
+
});
|
|
12
|
+
response.end(JSON.stringify(body));
|
|
13
|
+
}
|
|
14
|
+
function queryFromUrl(url) {
|
|
15
|
+
const query = {};
|
|
16
|
+
for (const key of new Set(url.searchParams.keys())) {
|
|
17
|
+
if (key === "workspace")
|
|
18
|
+
continue; // Resolved and authorized by the outer Host route.
|
|
19
|
+
const values = url.searchParams.getAll(key);
|
|
20
|
+
if (values.length !== 1)
|
|
21
|
+
throw new ModelTraceRequestError(400, "Duplicate model trace query field");
|
|
22
|
+
const value = values[0];
|
|
23
|
+
if (key === "withoutSession") {
|
|
24
|
+
if (value !== "true" && value !== "false")
|
|
25
|
+
throw new ModelTraceRequestError(400, "Invalid model trace withoutSession");
|
|
26
|
+
query[key] = value === "true";
|
|
27
|
+
}
|
|
28
|
+
else if (["limit", "since", "until"].includes(key)) {
|
|
29
|
+
if (!value.trim())
|
|
30
|
+
throw new ModelTraceRequestError(400, "Invalid numeric model trace query");
|
|
31
|
+
query[key] = Number(value);
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
query[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return query;
|
|
37
|
+
}
|
|
38
|
+
/** Authenticated read-only facade over one Host-owned store and one fixed workspace. */
|
|
39
|
+
export function createModelTraceHttp(options) {
|
|
40
|
+
const resolveScopes = async (request) => {
|
|
41
|
+
const scopes = await options.resolveWorkspaceScopes?.(request);
|
|
42
|
+
return scopes?.map((scope) => ({ ...scope, workspacePath: resolve(scope.workspacePath) }));
|
|
43
|
+
};
|
|
44
|
+
const fingerprint = (scopes) => JSON.stringify(scopes?.map((scope) => [scope.workspacePath, scope.sourcePrefix ?? null]).sort());
|
|
45
|
+
return {
|
|
46
|
+
async handle(request, response) {
|
|
47
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
48
|
+
if (url.pathname !== ROOT && !url.pathname.startsWith(`${ROOT}/`))
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
if (!(await options.isAuthorized(request)))
|
|
52
|
+
throw new ModelTraceRequestError(401, "Model history authorization expired");
|
|
53
|
+
if (request.method !== "GET") {
|
|
54
|
+
response.setHeader("allow", "GET");
|
|
55
|
+
throw new ModelTraceRequestError(405, "Model history is read-only on remote clients");
|
|
56
|
+
}
|
|
57
|
+
const workspaceScopes = await resolveScopes(request);
|
|
58
|
+
const history = createModelTraceHistory({ ...options, workspaceScopes });
|
|
59
|
+
let result;
|
|
60
|
+
if (url.pathname === ROOT)
|
|
61
|
+
result = history.list(queryFromUrl(url));
|
|
62
|
+
else if (url.pathname === `${ROOT}/sessions`)
|
|
63
|
+
result = history.sessions(queryFromUrl(url));
|
|
64
|
+
else if (url.pathname === `${ROOT}/config`) {
|
|
65
|
+
if (Object.keys(queryFromUrl(url)).length)
|
|
66
|
+
throw new ModelTraceRequestError(400, "Configuration queries are not supported");
|
|
67
|
+
result = history.getConfig();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const match = new RegExp(`^${ROOT}/([^/]+)$`).exec(url.pathname);
|
|
71
|
+
if (!match)
|
|
72
|
+
throw new ModelTraceRequestError(404, "Model record not found");
|
|
73
|
+
if (Object.keys(queryFromUrl(url)).length)
|
|
74
|
+
throw new ModelTraceRequestError(400, "Record queries are not supported");
|
|
75
|
+
let id;
|
|
76
|
+
try {
|
|
77
|
+
id = decodeURIComponent(match[1]);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new ModelTraceRequestError(400, "Invalid model trace id");
|
|
81
|
+
}
|
|
82
|
+
result = history.get(id);
|
|
83
|
+
if (!result)
|
|
84
|
+
throw new ModelTraceRequestError(404, "Model record not found");
|
|
85
|
+
}
|
|
86
|
+
// Authentication may have been revoked during asynchronous Host validation.
|
|
87
|
+
if (!(await options.isAuthorized(request)))
|
|
88
|
+
throw new ModelTraceRequestError(401, "Model history authorization expired");
|
|
89
|
+
if (fingerprint(await resolveScopes(request)) !== fingerprint(workspaceScopes))
|
|
90
|
+
throw new ModelTraceRequestError(401, "Model history workspace authorization changed");
|
|
91
|
+
if (!(await options.isAuthorized(request)))
|
|
92
|
+
throw new ModelTraceRequestError(401, "Model history authorization expired");
|
|
93
|
+
json(response, 200, result);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
json(response, error instanceof ModelTraceRequestError ? error.status : 500, {
|
|
97
|
+
error: error instanceof ModelTraceRequestError
|
|
98
|
+
? error.message
|
|
99
|
+
: "Model history is unavailable",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -341,6 +341,7 @@ export function createPanelAgentTaskHost(options = {}) {
|
|
|
341
341
|
cwd: scope.cwd,
|
|
342
342
|
bucket: owner.bucket,
|
|
343
343
|
task: input.prompt,
|
|
344
|
+
modelTraceSource: `panel:${scope.appId}`,
|
|
344
345
|
displayText: input.label,
|
|
345
346
|
clientMessageId: `panel-task:${scope.appId}:${input.sessionId}`,
|
|
346
347
|
behaviorMode: "isolatedTask",
|
|
@@ -12,14 +12,14 @@ export function createPanelDirectoryProjectScope(workspacePath, expectedProjectP
|
|
|
12
12
|
const binding = createHubPanelBinding(workspacePath);
|
|
13
13
|
binding.assertBinding();
|
|
14
14
|
const projectPath = realpathSync(expectedProjectPath);
|
|
15
|
-
const identity = lstatSync(projectPath);
|
|
15
|
+
const identity = lstatSync(projectPath, { bigint: true });
|
|
16
16
|
if (!identity.isDirectory() ||
|
|
17
17
|
identity.isSymbolicLink() ||
|
|
18
18
|
realpathSync(binding.bindingCwd) !== projectPath)
|
|
19
19
|
throw new Error("Panel directory project scope is unavailable");
|
|
20
20
|
const assertCurrent = () => {
|
|
21
21
|
binding.assertBinding();
|
|
22
|
-
const current = lstatSync(projectPath);
|
|
22
|
+
const current = lstatSync(projectPath, { bigint: true });
|
|
23
23
|
if (!current.isDirectory() ||
|
|
24
24
|
current.isSymbolicLink() ||
|
|
25
25
|
current.dev !== identity.dev ||
|
|
@@ -65,11 +65,14 @@ export function hubPanelDirectoryBookmarks(dataDir) {
|
|
|
65
65
|
const MAX_RECORDS = 256;
|
|
66
66
|
const MAX_BYTES = 256 * 1024;
|
|
67
67
|
const ID = /^[a-f0-9-]{36}$/i;
|
|
68
|
-
function
|
|
68
|
+
function parseBookmark(record, version) {
|
|
69
|
+
const invalid = () => {
|
|
70
|
+
throw new Error("Invalid Panel directory bookmarks");
|
|
71
|
+
};
|
|
69
72
|
if (!record || typeof record !== "object" || Array.isArray(record))
|
|
70
|
-
return
|
|
73
|
+
return invalid();
|
|
71
74
|
const item = record;
|
|
72
|
-
|
|
75
|
+
if (!(typeof item.id === "string" &&
|
|
73
76
|
ID.test(item.id) &&
|
|
74
77
|
typeof item.appId === "string" &&
|
|
75
78
|
/^[a-z][a-z0-9-]{0,63}$/.test(item.appId) &&
|
|
@@ -79,9 +82,36 @@ function valid(record) {
|
|
|
79
82
|
isAbsolute(item.path) &&
|
|
80
83
|
item.path.length <= 32_768 &&
|
|
81
84
|
item.projectPath.length <= 32_768 &&
|
|
82
|
-
Number.isSafeInteger(item.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
Number.isSafeInteger(item.savedAt)))
|
|
86
|
+
return invalid();
|
|
87
|
+
let dev, ino;
|
|
88
|
+
if (version === 1) {
|
|
89
|
+
const legacy = record;
|
|
90
|
+
if (typeof legacy.dev !== "number" ||
|
|
91
|
+
!Number.isInteger(legacy.dev) ||
|
|
92
|
+
legacy.dev < 0 ||
|
|
93
|
+
typeof legacy.ino !== "number" ||
|
|
94
|
+
!Number.isInteger(legacy.ino) ||
|
|
95
|
+
legacy.ino < 0)
|
|
96
|
+
return invalid();
|
|
97
|
+
// Never recover an authorization by rounding a file ID. Retain a tombstone
|
|
98
|
+
// so this UUID cannot fall back to a different grant in the legacy file.
|
|
99
|
+
if (!Number.isSafeInteger(legacy.dev) || !Number.isSafeInteger(legacy.ino)) {
|
|
100
|
+
dev = ino = null;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
dev = String(legacy.dev);
|
|
104
|
+
ino = String(legacy.ino);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const exact = (value) => typeof value === "string" && /^(0|[1-9][0-9]{0,19})$/.test(value);
|
|
109
|
+
if (!(item.dev === null && item.ino === null) && !(exact(item.dev) && exact(item.ino)))
|
|
110
|
+
return invalid();
|
|
111
|
+
dev = item.dev;
|
|
112
|
+
ino = item.ino;
|
|
113
|
+
}
|
|
114
|
+
return { ...item, dev, ino };
|
|
85
115
|
}
|
|
86
116
|
function read(file) {
|
|
87
117
|
let fd;
|
|
@@ -97,12 +127,11 @@ function read(file) {
|
|
|
97
127
|
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
98
128
|
throw new Error("Invalid Panel directory bookmarks");
|
|
99
129
|
const value = document;
|
|
100
|
-
if (value.version !== 1 ||
|
|
130
|
+
if ((value.version !== 1 && value.version !== 2) ||
|
|
101
131
|
!Array.isArray(value.bookmarks) ||
|
|
102
|
-
value.bookmarks.length > MAX_RECORDS
|
|
103
|
-
!value.bookmarks.every(valid))
|
|
132
|
+
value.bookmarks.length > MAX_RECORDS)
|
|
104
133
|
throw new Error("Invalid Panel directory bookmarks");
|
|
105
|
-
return value.bookmarks;
|
|
134
|
+
return value.bookmarks.map((record) => parseBookmark(record, value.version));
|
|
106
135
|
}
|
|
107
136
|
catch (error) {
|
|
108
137
|
if (error.code === "ENOENT")
|
|
@@ -129,7 +158,7 @@ function write(file, bookmarks) {
|
|
|
129
158
|
if (error.code !== "ENOENT")
|
|
130
159
|
throw error;
|
|
131
160
|
}
|
|
132
|
-
const serialized = JSON.stringify({ version:
|
|
161
|
+
const serialized = JSON.stringify({ version: 2, bookmarks });
|
|
133
162
|
if (Buffer.byteLength(serialized) > MAX_BYTES)
|
|
134
163
|
throw new Error("Panel directory bookmarks are full");
|
|
135
164
|
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -154,7 +183,7 @@ export class PanelAppDirectoryBookmarks {
|
|
|
154
183
|
remember(appId, projectPath, path) {
|
|
155
184
|
if (!/^[a-z][a-z0-9-]{0,63}$/.test(appId) || !isAbsolute(projectPath) || !isAbsolute(path))
|
|
156
185
|
throw new Error("Invalid Panel directory bookmark scope");
|
|
157
|
-
const info = lstatSync(path);
|
|
186
|
+
const info = lstatSync(path, { bigint: true });
|
|
158
187
|
if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(path) !== path)
|
|
159
188
|
throw new Error("Selected directory changed; choose it again");
|
|
160
189
|
const bookmark = {
|
|
@@ -162,8 +191,8 @@ export class PanelAppDirectoryBookmarks {
|
|
|
162
191
|
appId,
|
|
163
192
|
projectPath,
|
|
164
193
|
path,
|
|
165
|
-
dev: info.dev,
|
|
166
|
-
ino: info.ino,
|
|
194
|
+
dev: info.dev.toString(),
|
|
195
|
+
ino: info.ino.toString(),
|
|
167
196
|
savedAt: Date.now(),
|
|
168
197
|
};
|
|
169
198
|
mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
@@ -173,18 +202,22 @@ export class PanelAppDirectoryBookmarks {
|
|
|
173
202
|
const existing = previous.find((item) => item.appId === appId &&
|
|
174
203
|
item.projectPath === projectPath &&
|
|
175
204
|
item.path === path &&
|
|
176
|
-
item.dev ===
|
|
177
|
-
item.ino ===
|
|
205
|
+
item.dev === bookmark.dev &&
|
|
206
|
+
item.ino === bookmark.ino);
|
|
178
207
|
if (existing)
|
|
179
208
|
bookmark.id = existing.id;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
const retained = previous.filter((item) => item.id !== bookmark.id &&
|
|
210
|
+
!(item.dev !== null &&
|
|
211
|
+
item.appId === appId &&
|
|
212
|
+
item.projectPath === projectPath &&
|
|
213
|
+
item.path === path &&
|
|
214
|
+
(item.dev !== bookmark.dev || item.ino !== bookmark.ino)));
|
|
215
|
+
// Keep revocations even when the bounded store evicts older live grants:
|
|
216
|
+
// forgetting one could resurrect that UUID through a legacy-file fallback.
|
|
217
|
+
const revoked = retained.filter((item) => item.dev === null);
|
|
218
|
+
if (revoked.length >= MAX_RECORDS)
|
|
219
|
+
throw new Error("Panel directory bookmarks are full");
|
|
220
|
+
write(this.file, [bookmark, ...revoked, ...retained.filter((item) => item.dev !== null)].slice(0, MAX_RECORDS));
|
|
188
221
|
}
|
|
189
222
|
finally {
|
|
190
223
|
release();
|
|
@@ -222,11 +255,11 @@ export class PanelAppDirectoryBookmarks {
|
|
|
222
255
|
};
|
|
223
256
|
assertProjectScope();
|
|
224
257
|
try {
|
|
225
|
-
const info = lstatSync(bookmark.path);
|
|
258
|
+
const info = lstatSync(bookmark.path, { bigint: true });
|
|
226
259
|
if (!info.isDirectory() ||
|
|
227
260
|
info.isSymbolicLink() ||
|
|
228
|
-
info.dev !== bookmark.dev ||
|
|
229
|
-
info.ino !== bookmark.ino ||
|
|
261
|
+
info.dev.toString() !== bookmark.dev ||
|
|
262
|
+
info.ino.toString() !== bookmark.ino ||
|
|
230
263
|
realpathSync(bookmark.path) !== bookmark.path)
|
|
231
264
|
throw new Error("Saved directory changed");
|
|
232
265
|
}
|
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
|
+
}
|