@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Retire an unusable room transport without leaving an unresponsive CLI alive. */
|
|
2
|
+
export function stopRoomProcess(child) {
|
|
3
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
4
|
+
return;
|
|
5
|
+
const signal = (value) => {
|
|
6
|
+
if (child.pid && child.pid > 1 && process.platform !== "win32") {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(-child.pid, value);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
/* A failed spawn or an exited group falls back to the exact child. */
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
child.kill(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* already gone */
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
signal("SIGTERM");
|
|
23
|
+
if (!child.pid)
|
|
24
|
+
return;
|
|
25
|
+
const timer = setTimeout(() => signal("SIGKILL"), 1_000);
|
|
26
|
+
timer.unref();
|
|
27
|
+
child.once("exit", () => clearTimeout(timer));
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ModelTraceStore, type ModelTraceConfig, type ModelTraceConfigPatch, type ModelTraceEvent, type ModelTraceListQuery, type ModelTracePage, type ModelTraceSessionPage, type ModelTraceRecord } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
export type ModelTraceDetail = {
|
|
3
|
+
record: ModelTraceRecord;
|
|
4
|
+
events: ModelTraceEvent[];
|
|
5
|
+
};
|
|
6
|
+
export type ModelTraceReader = Pick<ModelTraceStore, "list" | "sessions" | "get" | "events" | "getConfig" | "setConfig">;
|
|
7
|
+
export declare class ModelTraceRequestError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
constructor(status: number, message: string);
|
|
10
|
+
}
|
|
11
|
+
/** Never forward renderer/browser-owned filesystem paths or unknown scan options. */
|
|
12
|
+
export declare function validateModelTraceQuery(input?: unknown): ModelTraceListQuery;
|
|
13
|
+
export declare function validateModelTraceId(input: unknown): string;
|
|
14
|
+
export declare function validateModelTraceConfigPatch(input: unknown): ModelTraceConfigPatch;
|
|
15
|
+
/** Host fixes storageDir once; no query or request can select a different store. */
|
|
16
|
+
export declare function createModelTraceHistory(options: {
|
|
17
|
+
storageDir: string;
|
|
18
|
+
/** Set only by the authenticated Host, never by list/get arguments. */
|
|
19
|
+
workspacePath?: string;
|
|
20
|
+
/** Additional scopes fixed and authorized by the Host, never by list/get input. */
|
|
21
|
+
workspaceScopes?: ModelTraceListQuery["workspaceScopes"];
|
|
22
|
+
store?: ModelTraceReader;
|
|
23
|
+
}): {
|
|
24
|
+
list(query?: unknown): ModelTracePage;
|
|
25
|
+
sessions(query?: unknown): ModelTraceSessionPage;
|
|
26
|
+
get(id: unknown): ModelTraceDetail | null;
|
|
27
|
+
getConfig(): ModelTraceConfig;
|
|
28
|
+
setConfig(patch: unknown): ModelTraceConfig;
|
|
29
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ModelTraceStore, isModelTraceCallType, } from "@cjhyy/code-shell-core/internal";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const QUERY_FIELDS = new Set([
|
|
4
|
+
"limit",
|
|
5
|
+
"cursor",
|
|
6
|
+
"search",
|
|
7
|
+
"traceId",
|
|
8
|
+
"sessionId",
|
|
9
|
+
"withoutSession",
|
|
10
|
+
"model",
|
|
11
|
+
"provider",
|
|
12
|
+
"purpose",
|
|
13
|
+
"callType",
|
|
14
|
+
"source",
|
|
15
|
+
"status",
|
|
16
|
+
"since",
|
|
17
|
+
"until",
|
|
18
|
+
]);
|
|
19
|
+
const CONFIG_FIELDS = new Set(["enabled", "captureContent", "includeEphemeral", "retentionDays"]);
|
|
20
|
+
export class ModelTraceRequestError extends Error {
|
|
21
|
+
status;
|
|
22
|
+
constructor(status, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function record(value, label) {
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
29
|
+
throw new ModelTraceRequestError(400, `Invalid model trace ${label}`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** Never forward renderer/browser-owned filesystem paths or unknown scan options. */
|
|
33
|
+
export function validateModelTraceQuery(input) {
|
|
34
|
+
if (input === undefined)
|
|
35
|
+
return {};
|
|
36
|
+
const query = record(input, "query");
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [key, value] of Object.entries(query)) {
|
|
39
|
+
if (!QUERY_FIELDS.has(key))
|
|
40
|
+
throw new ModelTraceRequestError(400, `Unknown model trace query field: ${key}`);
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
if (key === "callType") {
|
|
44
|
+
if (!isModelTraceCallType(value))
|
|
45
|
+
throw new ModelTraceRequestError(400, "Invalid model trace callType");
|
|
46
|
+
}
|
|
47
|
+
else if (key === "withoutSession") {
|
|
48
|
+
if (typeof value !== "boolean")
|
|
49
|
+
throw new ModelTraceRequestError(400, "Invalid model trace withoutSession");
|
|
50
|
+
}
|
|
51
|
+
else if (key === "limit") {
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 200)
|
|
53
|
+
throw new ModelTraceRequestError(400, "Model trace limit must be between 1 and 200");
|
|
54
|
+
}
|
|
55
|
+
else if (key === "since" || key === "until") {
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
57
|
+
throw new ModelTraceRequestError(400, `Invalid model trace ${key}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const max = key === "search" || key === "cursor" ? 2048 : 512;
|
|
61
|
+
if (typeof value !== "string" || value.length > max || value.includes("\0"))
|
|
62
|
+
throw new ModelTraceRequestError(400, `Invalid model trace ${key}`);
|
|
63
|
+
if (key === "status" && !["running", "completed", "failed", "cancelled"].includes(value))
|
|
64
|
+
throw new ModelTraceRequestError(400, "Invalid model trace status");
|
|
65
|
+
}
|
|
66
|
+
result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (typeof result.since === "number" &&
|
|
69
|
+
typeof result.until === "number" &&
|
|
70
|
+
result.since > result.until)
|
|
71
|
+
throw new ModelTraceRequestError(400, "Model trace time range is reversed");
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
export function validateModelTraceId(input) {
|
|
75
|
+
if (typeof input !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(input))
|
|
76
|
+
throw new ModelTraceRequestError(400, "Invalid model trace id");
|
|
77
|
+
return input;
|
|
78
|
+
}
|
|
79
|
+
export function validateModelTraceConfigPatch(input) {
|
|
80
|
+
const patch = record(input, "configuration");
|
|
81
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
82
|
+
if (!CONFIG_FIELDS.has(key))
|
|
83
|
+
throw new ModelTraceRequestError(400, `Unknown model trace configuration field: ${key}`);
|
|
84
|
+
if ((key === "enabled" || key === "captureContent" || key === "includeEphemeral") &&
|
|
85
|
+
typeof value !== "boolean")
|
|
86
|
+
throw new ModelTraceRequestError(400, `Invalid model trace ${key}`);
|
|
87
|
+
if (key === "retentionDays" &&
|
|
88
|
+
(!Number.isSafeInteger(value) || value < 1 || value > 365))
|
|
89
|
+
throw new ModelTraceRequestError(400, "Model trace retention must be between 1 and 365 days");
|
|
90
|
+
}
|
|
91
|
+
return { ...patch };
|
|
92
|
+
}
|
|
93
|
+
/** Host fixes storageDir once; no query or request can select a different store. */
|
|
94
|
+
export function createModelTraceHistory(options) {
|
|
95
|
+
const store = options.store ?? new ModelTraceStore({ storageDir: options.storageDir });
|
|
96
|
+
const workspacePath = options.workspacePath === undefined ? undefined : resolve(options.workspacePath);
|
|
97
|
+
if (options.workspaceScopes !== undefined &&
|
|
98
|
+
(!Array.isArray(options.workspaceScopes) ||
|
|
99
|
+
options.workspaceScopes.length + (workspacePath === undefined ? 0 : 1) > 8 ||
|
|
100
|
+
options.workspaceScopes.some((scope) => !scope ||
|
|
101
|
+
typeof scope !== "object" ||
|
|
102
|
+
Array.isArray(scope) ||
|
|
103
|
+
Object.keys(scope).some((field) => !["workspacePath", "sourcePrefix"].includes(field)) ||
|
|
104
|
+
typeof scope.workspacePath !== "string" ||
|
|
105
|
+
!scope.workspacePath ||
|
|
106
|
+
scope.workspacePath.length > 4096 ||
|
|
107
|
+
scope.workspacePath.includes("\0") ||
|
|
108
|
+
(scope.sourcePrefix !== undefined &&
|
|
109
|
+
(typeof scope.sourcePrefix !== "string" ||
|
|
110
|
+
!scope.sourcePrefix ||
|
|
111
|
+
scope.sourcePrefix.length > 256 ||
|
|
112
|
+
scope.sourcePrefix.includes("\0"))))))
|
|
113
|
+
throw new ModelTraceRequestError(500, "Invalid Host model trace scopes");
|
|
114
|
+
const scopes = options.workspaceScopes
|
|
115
|
+
? [
|
|
116
|
+
...(workspacePath === undefined ? [] : [{ workspacePath }]),
|
|
117
|
+
...options.workspaceScopes.map((scope) => ({
|
|
118
|
+
...scope,
|
|
119
|
+
workspacePath: resolve(scope.workspacePath),
|
|
120
|
+
})),
|
|
121
|
+
]
|
|
122
|
+
: undefined;
|
|
123
|
+
const matchesPath = (entry, path) => entry.workspacePath !== undefined && resolve(entry.workspacePath) === path;
|
|
124
|
+
const unrestricted = (entry) => scopes
|
|
125
|
+
? scopes.some((scope) => scope.sourcePrefix === undefined && matchesPath(entry, scope.workspacePath))
|
|
126
|
+
: workspacePath === undefined || matchesPath(entry, workspacePath);
|
|
127
|
+
const visible = (entry) => unrestricted(entry) ||
|
|
128
|
+
!!scopes?.some((scope) => matchesPath(entry, scope.workspacePath) &&
|
|
129
|
+
typeof entry.source === "string" &&
|
|
130
|
+
scope.sourcePrefix !== undefined &&
|
|
131
|
+
entry.source.startsWith(scope.sourcePrefix));
|
|
132
|
+
const visibleEvents = (entry) => {
|
|
133
|
+
const events = store.events(entry.traceId);
|
|
134
|
+
const accepted = new Set(events.filter(unrestricted).map((event) => event.id));
|
|
135
|
+
// Events do not carry a source. A prefix-limited related scope may only
|
|
136
|
+
// expose the exact selected call's descendants, never another root chat's
|
|
137
|
+
// events merely because they share a trace identifier.
|
|
138
|
+
const children = new Map();
|
|
139
|
+
if (entry.workspacePath !== undefined)
|
|
140
|
+
for (const event of events)
|
|
141
|
+
if (event.parentId && matchesPath(event, resolve(entry.workspacePath))) {
|
|
142
|
+
const siblings = children.get(event.parentId) ?? [];
|
|
143
|
+
siblings.push(event);
|
|
144
|
+
children.set(event.parentId, siblings);
|
|
145
|
+
}
|
|
146
|
+
const queue = [entry.id];
|
|
147
|
+
const visited = new Set(queue);
|
|
148
|
+
for (let index = 0; index < queue.length; index++)
|
|
149
|
+
for (const event of children.get(queue[index]) ?? [])
|
|
150
|
+
if (!visited.has(event.id)) {
|
|
151
|
+
visited.add(event.id);
|
|
152
|
+
accepted.add(event.id);
|
|
153
|
+
queue.push(event.id);
|
|
154
|
+
}
|
|
155
|
+
return events.filter((event) => accepted.has(event.id));
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
list(query) {
|
|
159
|
+
const validated = validateModelTraceQuery(query);
|
|
160
|
+
return store.list({
|
|
161
|
+
...validated,
|
|
162
|
+
...(scopes
|
|
163
|
+
? { workspaceScopes: scopes }
|
|
164
|
+
: workspacePath === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { workspacePath }),
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
sessions(query) {
|
|
170
|
+
const validated = validateModelTraceQuery(query);
|
|
171
|
+
try {
|
|
172
|
+
return store.sessions({
|
|
173
|
+
...validated,
|
|
174
|
+
...(scopes
|
|
175
|
+
? { workspaceScopes: scopes }
|
|
176
|
+
: workspacePath === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { workspacePath }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof Error && error.message === "Invalid model trace session cursor")
|
|
183
|
+
throw new ModelTraceRequestError(400, error.message);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
get(id) {
|
|
188
|
+
const entry = store.get(validateModelTraceId(id));
|
|
189
|
+
return entry && visible(entry) ? { record: entry, events: visibleEvents(entry) } : null;
|
|
190
|
+
},
|
|
191
|
+
getConfig() {
|
|
192
|
+
return store.getConfig();
|
|
193
|
+
},
|
|
194
|
+
setConfig(patch) {
|
|
195
|
+
return store.setConfig(validateModelTraceConfigPatch(patch));
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -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",
|
|
@@ -96,7 +96,8 @@ export declare class PanelAppAgentTaskService {
|
|
|
96
96
|
get(owner: PanelAgentTaskOwner, id: unknown): PanelAgentTaskView;
|
|
97
97
|
cancel(owner: PanelAgentTaskOwner, id: unknown): Promise<PanelAgentTaskView>;
|
|
98
98
|
rebind(owner: PanelAgentTaskOwner): void;
|
|
99
|
-
|
|
99
|
+
/** `projects` limits cancellation to tasks bound to those (canonical) projects. */
|
|
100
|
+
cancelApp(appId: string, projects?: ReadonlySet<string>): void;
|
|
100
101
|
private execute;
|
|
101
102
|
private scopedTask;
|
|
102
103
|
private emit;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { panelExecutionGate } from "./execution-gate.js";
|
|
1
|
+
import { panelExecutionGate, panelExecutionProject } from "./execution-gate.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
const MAX_TASKS = 200;
|
|
4
4
|
const MAX_PROMPT_CHARS = 20_000;
|
|
@@ -131,7 +131,11 @@ export class PanelAppAgentTaskService {
|
|
|
131
131
|
cancelRequested: false,
|
|
132
132
|
activity: [],
|
|
133
133
|
};
|
|
134
|
-
const release = panelExecutionGate.enter(
|
|
134
|
+
const release = panelExecutionGate.enter({
|
|
135
|
+
appId: owner.appId,
|
|
136
|
+
projectPath: owner.projectPath,
|
|
137
|
+
holder: { kind: "agent-task", guestId: owner.guestId },
|
|
138
|
+
});
|
|
135
139
|
this.tasks.set(task.id, task);
|
|
136
140
|
this.trimHistory();
|
|
137
141
|
this.emit(task);
|
|
@@ -174,10 +178,13 @@ export class PanelAppAgentTaskService {
|
|
|
174
178
|
this.runtime.rebind(task.sessionId, task.owner);
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
|
-
|
|
181
|
+
/** `projects` limits cancellation to tasks bound to those (canonical) projects. */
|
|
182
|
+
cancelApp(appId, projects) {
|
|
178
183
|
for (const task of this.tasks.values()) {
|
|
179
184
|
if (task.owner.appId !== appId || TERMINAL_STATUSES.has(task.status))
|
|
180
185
|
continue;
|
|
186
|
+
if (projects && !projects.has(panelExecutionProject(task.owner.projectPath)))
|
|
187
|
+
continue;
|
|
181
188
|
task.cancelRequested = true;
|
|
182
189
|
task.status = "cancelling";
|
|
183
190
|
task.updatedAt = this.now();
|
|
@@ -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
|
}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
/** What holds an execution, so a refused change can name it. Host-assigned only. */
|
|
2
|
+
export interface PanelExecutionHolder {
|
|
3
|
+
kind: "panel-call" | "agent-tool" | "process" | "agent-task" | "tool-job" | "automation";
|
|
4
|
+
/** Desktop WebContents id of the Panel instance that owns this execution. */
|
|
5
|
+
guestId?: number;
|
|
6
|
+
/** Trusted Host description, such as a native tool entry name. */
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
1
9
|
export interface PanelExecutionScope {
|
|
2
10
|
appId: string;
|
|
3
11
|
/** Host-verified binding project, never a directory supplied by the guest. */
|
|
4
12
|
projectPath: string;
|
|
13
|
+
holder?: PanelExecutionHolder;
|
|
14
|
+
}
|
|
15
|
+
export interface PanelExecutionBlocker extends PanelExecutionScope {
|
|
16
|
+
count: number;
|
|
5
17
|
}
|
|
6
18
|
/** Existing path aliases must not create separate admission domains. */
|
|
7
19
|
export declare function panelExecutionProject(path: string): string;
|
|
8
20
|
export declare class PanelExecutionBusyError extends Error {
|
|
21
|
+
/** Present when executions block a change; empty while another change runs. */
|
|
22
|
+
readonly blockers: PanelExecutionBlocker[];
|
|
9
23
|
readonly status = 409;
|
|
10
|
-
constructor(message: string
|
|
24
|
+
constructor(message: string,
|
|
25
|
+
/** Present when executions block a change; empty while another change runs. */
|
|
26
|
+
blockers?: PanelExecutionBlocker[]);
|
|
11
27
|
}
|
|
12
28
|
/** One process owns admission for every Panel runtime sharing its installed catalog.
|
|
13
29
|
* Acquire synchronously before authorization/IO; release after real execution cleanup,
|
|
@@ -21,6 +37,8 @@ export declare class PanelExecutionGate {
|
|
|
21
37
|
run<T>(scope: PanelExecutionScope, work: () => Promise<T>): Promise<T>;
|
|
22
38
|
/** Durable queues remain occupied even while paused and between process attempts. */
|
|
23
39
|
register(source: () => Iterable<PanelExecutionScope>): () => void;
|
|
40
|
+
/** Executions that a change matching `matches` would wait for, grouped by holder. */
|
|
41
|
+
blockers(matches: (scope: PanelExecutionScope) => boolean): PanelExecutionBlocker[];
|
|
24
42
|
mutate<T>(matches: (scope: PanelExecutionScope) => boolean, work: () => Promise<T>): Promise<T>;
|
|
25
43
|
}
|
|
26
44
|
export declare const panelExecutionGate: PanelExecutionGate;
|