@cjhyy/code-shell-server 0.9.22 → 0.9.25
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/README.md +45 -1
- package/dist/bin/code-shell-settings-recovery.d.ts +2 -0
- package/dist/bin/code-shell-settings-recovery.js +4 -0
- package/dist/desktop-web/http-api.js +14 -0
- package/dist/environment-identity.d.ts +4 -0
- package/dist/environment-identity.js +67 -0
- package/dist/hub/auth-store.d.ts +2 -0
- package/dist/hub/auth-store.js +7 -0
- package/dist/index.auth.d.ts +3 -0
- package/dist/index.auth.js +2 -0
- package/dist/index.links.d.ts +1 -0
- package/dist/index.links.js +1 -0
- package/dist/index.panels.d.ts +7 -0
- package/dist/index.panels.js +7 -0
- package/dist/index.remote-relay.d.ts +5 -0
- package/dist/index.remote-relay.js +6 -0
- package/dist/index.serve.d.ts +2 -0
- package/dist/index.serve.js +2 -0
- package/dist/links/http.d.ts +3 -0
- package/dist/links/http.js +11 -2
- package/dist/links/remote-configuration.d.ts +4 -0
- package/dist/links/remote-configuration.js +32 -0
- package/dist/links/service.d.ts +6 -1
- package/dist/links/service.js +241 -19
- package/dist/mobile-remote/mobile-upload-service.d.ts +4 -0
- package/dist/mobile-remote/mobile-upload-service.js +69 -11
- package/dist/mobile-remote/remote-host-manager.d.ts +10 -5
- package/dist/mobile-remote/remote-host-manager.js +53 -5
- package/dist/panels/agent-task-service.js +9 -2
- package/dist/panels/automations.d.ts +49 -0
- package/dist/panels/automations.js +134 -0
- package/dist/panels/bridge-contract.d.ts +5 -3
- package/dist/panels/bridge-contract.js +11 -1
- package/dist/panels/directory-bookmarks.d.ts +9 -1
- package/dist/panels/directory-bookmarks.js +64 -5
- package/dist/panels/execution-gate.d.ts +26 -0
- package/dist/panels/execution-gate.js +70 -0
- package/dist/panels/http.d.ts +26 -2
- package/dist/panels/http.js +7 -1
- package/dist/panels/hub-automations.d.ts +31 -0
- package/dist/panels/hub-automations.js +352 -0
- package/dist/panels/management-http.d.ts +20 -2
- package/dist/panels/management-http.js +15 -2
- package/dist/panels/management.d.ts +26 -4
- package/dist/panels/management.js +382 -39
- package/dist/panels/package-mutation.d.ts +8 -0
- package/dist/panels/package-mutation.js +23 -0
- package/dist/panels/process-service.d.ts +3 -0
- package/dist/panels/process-service.js +77 -4
- package/dist/panels/resources/clone.d.ts +13 -0
- package/dist/panels/resources/clone.js +99 -0
- package/dist/panels/resources/http.d.ts +11 -0
- package/dist/panels/resources/http.js +74 -0
- package/dist/panels/resources/references.d.ts +5 -0
- package/dist/panels/resources/references.js +26 -0
- package/dist/panels/resources/service.js +63 -27
- package/dist/panels/runtime-services.js +16 -3
- package/dist/panels/runtime.d.ts +16 -4
- package/dist/panels/runtime.js +522 -64
- package/dist/panels/shared-tool-jobs.d.ts +40 -0
- package/dist/panels/shared-tool-jobs.js +37 -0
- package/dist/panels/storage-store.d.ts +21 -0
- package/dist/panels/storage-store.js +43 -0
- package/dist/panels/task-cookie-host.d.ts +26 -0
- package/dist/panels/task-cookie-host.js +145 -0
- package/dist/panels/task-cookies.d.ts +51 -0
- package/dist/panels/task-cookies.js +183 -0
- package/dist/panels/tool-executor.d.ts +21 -5
- package/dist/panels/tool-executor.js +171 -30
- package/dist/panels/tool-jobs-storage.d.ts +2 -0
- package/dist/panels/tool-jobs-storage.js +34 -0
- package/dist/panels/tool-jobs.d.ts +49 -3
- package/dist/panels/tool-jobs.js +270 -15
- package/dist/panels/types.d.ts +43 -0
- package/dist/project-runtime/backup.d.ts +26 -0
- package/dist/project-runtime/backup.js +500 -0
- package/dist/project-runtime/control-server.d.ts +4 -0
- package/dist/project-runtime/control-server.js +19 -1
- package/dist/project-runtime/docker-provider.d.ts +4 -0
- package/dist/project-runtime/docker-provider.js +16 -1
- package/dist/project-runtime/managed-entry.js +4 -0
- package/dist/project-runtime/registry.d.ts +2 -0
- package/dist/project-runtime/registry.js +15 -0
- package/dist/project-runtime/seccomp.d.ts +8 -0
- package/dist/project-runtime/seccomp.js +72 -0
- package/dist/project-runtime/types.d.ts +2 -0
- package/dist/remote-relay/byte-stream.d.ts +9 -0
- package/dist/remote-relay/byte-stream.js +71 -0
- package/dist/remote-relay/connector.d.ts +27 -0
- package/dist/remote-relay/connector.js +197 -0
- package/dist/remote-relay/local-stream.d.ts +10 -0
- package/dist/remote-relay/local-stream.js +120 -0
- package/dist/remote-relay/protocol.d.ts +39 -0
- package/dist/remote-relay/protocol.js +64 -0
- package/dist/serve/automation-worker.d.ts +16 -0
- package/dist/serve/automation-worker.js +126 -0
- package/dist/serve/cli.d.ts +3 -0
- package/dist/serve/cli.js +17 -0
- package/dist/serve/headless-server.d.ts +3 -0
- package/dist/serve/headless-server.js +140 -10
- package/dist/serve/settings-recovery-cli.d.ts +29 -0
- package/dist/serve/settings-recovery-cli.js +128 -0
- package/dist/session-http-responses.d.ts +7 -0
- package/dist/session-http-responses.js +40 -0
- package/dist/sessions-service.d.ts +6 -2
- package/dist/sessions-service.js +23 -4
- package/dist/worker-bridge-core.d.ts +5 -0
- package/dist/worker-bridge-core.js +23 -0
- package/package.json +15 -6
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { panelExecutionGate } from "./execution-gate.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { opendir } from "node:fs/promises";
|
|
3
4
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
5
|
import { ToolJobStorage, toolJobJson } from "./tool-jobs-storage.js";
|
|
5
6
|
export const toolJobLimits = Object.freeze({
|
|
6
7
|
maxJobs: 2000,
|
|
7
|
-
maxQueuedPerScope:
|
|
8
|
+
maxQueuedPerScope: 128,
|
|
9
|
+
maxQueues: 512,
|
|
10
|
+
maxQueueCatalogBytes: 4 * 1024 * 1024,
|
|
8
11
|
maxConcurrent: 2,
|
|
9
12
|
maxInputBytes: 2 * 1024 * 1024 + 8 * 1024,
|
|
10
13
|
maxResultBytes: 2 * 1024 * 1024,
|
|
@@ -12,6 +15,43 @@ export const toolJobLimits = Object.freeze({
|
|
|
12
15
|
maxProgressBytes: 4 * 1024,
|
|
13
16
|
maxExaminedEntries: 4096,
|
|
14
17
|
});
|
|
18
|
+
const queueKey = (scope) => JSON.stringify(scopeValue(scope));
|
|
19
|
+
function queueState(value) {
|
|
20
|
+
if (!value ||
|
|
21
|
+
typeof value.paused !== "boolean" ||
|
|
22
|
+
!Number.isSafeInteger(value.revision) ||
|
|
23
|
+
value.revision < 0 ||
|
|
24
|
+
!Number.isSafeInteger(value.maxConcurrent) ||
|
|
25
|
+
value.maxConcurrent < 1 ||
|
|
26
|
+
value.maxConcurrent > toolJobLimits.maxConcurrent)
|
|
27
|
+
throw new Error("Invalid tool queue state");
|
|
28
|
+
return { revision: value.revision, paused: value.paused, maxConcurrent: value.maxConcurrent };
|
|
29
|
+
}
|
|
30
|
+
async function waitPreparation(result, signal) {
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
let abort;
|
|
33
|
+
try {
|
|
34
|
+
const outcome = signal
|
|
35
|
+
? await Promise.race([
|
|
36
|
+
result,
|
|
37
|
+
new Promise((_resolve, reject) => {
|
|
38
|
+
abort = () => reject(signal.reason ?? new Error("Tool request cancelled"));
|
|
39
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
abort();
|
|
42
|
+
}),
|
|
43
|
+
])
|
|
44
|
+
: await result;
|
|
45
|
+
signal?.throwIfAborted();
|
|
46
|
+
if ("error" in outcome)
|
|
47
|
+
throw outcome.error;
|
|
48
|
+
return outcome.job;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (abort)
|
|
52
|
+
signal?.removeEventListener("abort", abort);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
15
55
|
const TERMINAL = new Set(["succeeded", "failed", "cancelled", "interrupted"]);
|
|
16
56
|
const STATUSES = new Set([...TERMINAL, "queued", "running", "cancelling"]);
|
|
17
57
|
function scopeValue(scope) {
|
|
@@ -63,6 +103,20 @@ function publicJob(job) {
|
|
|
63
103
|
const { version: _version, requestKey: _requestKey, requestDigest: _requestDigest, ...value } = job;
|
|
64
104
|
return toolJobJson(value, toolJobLimits.maxRecordBytes);
|
|
65
105
|
}
|
|
106
|
+
function packageValue(value) {
|
|
107
|
+
if (!value ||
|
|
108
|
+
typeof value.version !== "string" ||
|
|
109
|
+
!value.version ||
|
|
110
|
+
value.version.length > 128 ||
|
|
111
|
+
/[\u0000-\u001f\u007f]/.test(value.version) ||
|
|
112
|
+
typeof value.packageDigest !== "string" ||
|
|
113
|
+
!/^[a-f0-9]{64}$/.test(value.packageDigest))
|
|
114
|
+
throw new Error("Invalid tool job package identity");
|
|
115
|
+
return { version: value.version, packageDigest: value.packageDigest };
|
|
116
|
+
}
|
|
117
|
+
function packageError(code, message, retryable = true) {
|
|
118
|
+
return Object.assign(new Error(message), { code, retryable });
|
|
119
|
+
}
|
|
66
120
|
function storedValue(raw) {
|
|
67
121
|
const job = toolJobJson(raw, toolJobLimits.maxRecordBytes);
|
|
68
122
|
if (!job ||
|
|
@@ -80,6 +134,8 @@ function storedValue(raw) {
|
|
|
80
134
|
throw new Error("invalid tool job record");
|
|
81
135
|
job.scope = scopeValue(job.scope);
|
|
82
136
|
job.entry = entryValue(job.entry);
|
|
137
|
+
if (job.package !== undefined)
|
|
138
|
+
job.package = packageValue(job.package);
|
|
83
139
|
job.input = toolJobJson(job.input, toolJobLimits.maxInputBytes);
|
|
84
140
|
if (job.result !== undefined)
|
|
85
141
|
job.result = toolJobJson(job.result, toolJobLimits.maxResultBytes);
|
|
@@ -99,6 +155,7 @@ export class PanelToolJobService {
|
|
|
99
155
|
options;
|
|
100
156
|
storage;
|
|
101
157
|
jobs = new Map();
|
|
158
|
+
queues = new Map();
|
|
102
159
|
active = new Map();
|
|
103
160
|
preparing = new Map();
|
|
104
161
|
ready;
|
|
@@ -106,13 +163,31 @@ export class PanelToolJobService {
|
|
|
106
163
|
stopping = false;
|
|
107
164
|
pumping = false;
|
|
108
165
|
shutdownPromise;
|
|
166
|
+
unregisterExecution;
|
|
109
167
|
constructor(options) {
|
|
110
168
|
this.options = options;
|
|
111
169
|
this.storage = new ToolJobStorage(options.rootDir);
|
|
170
|
+
this.unregisterExecution = panelExecutionGate.register(() => this.executionScopes());
|
|
112
171
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
172
|
+
listeners = new Set();
|
|
173
|
+
/** Trusted Host subscription. Transports must recheck their viewer's authorization. */
|
|
174
|
+
subscribe(rawScope, send) {
|
|
175
|
+
const listener = { scope: scopeValue(rawScope), send };
|
|
176
|
+
this.listeners.add(listener);
|
|
177
|
+
return () => {
|
|
178
|
+
this.listeners.delete(listener);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
executionScopes() {
|
|
182
|
+
return [
|
|
183
|
+
...[...this.preparing.values()].map((item) => item.scope),
|
|
184
|
+
...[...this.jobs.values()]
|
|
185
|
+
.filter((job) => !TERMINAL.has(job.status) || this.active.has(job.id))
|
|
186
|
+
.map((job) => job.scope),
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
activeCount(projectPath) {
|
|
190
|
+
return this.executionScopes().filter((scope) => !projectPath || scope.projectPath === resolve(projectPath)).length;
|
|
116
191
|
}
|
|
117
192
|
now() {
|
|
118
193
|
return this.options.now?.() ?? Date.now();
|
|
@@ -126,12 +201,46 @@ export class PanelToolJobService {
|
|
|
126
201
|
if (this.options.isAuthorized && !(await this.options.isAuthorized(scope)))
|
|
127
202
|
throw new Error("tool job owner is no longer authorized");
|
|
128
203
|
}
|
|
204
|
+
async describePackage(scope) {
|
|
205
|
+
if (!this.options.describePackage)
|
|
206
|
+
return undefined;
|
|
207
|
+
try {
|
|
208
|
+
return packageValue(await this.options.describePackage(scope));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
throw packageError("PACKAGE_UNAVAILABLE", "任务的项目安装包无法校验,请恢复原包后重试。");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async verifyPackage(job) {
|
|
215
|
+
if (!job.package && !this.options.describePackage)
|
|
216
|
+
return; // Legacy standalone embedders.
|
|
217
|
+
if (!job.package)
|
|
218
|
+
throw packageError("PACKAGE_UNKNOWN", "旧任务未记录安装包版本,请检查输入并重新提交任务。", false);
|
|
219
|
+
const current = await this.describePackage(job.scope);
|
|
220
|
+
if (!current)
|
|
221
|
+
throw packageError("PACKAGE_UNAVAILABLE", "当前 Host 无法校验此任务的原安装包。");
|
|
222
|
+
if (current.version !== job.package.version ||
|
|
223
|
+
current.packageDigest !== job.package.packageDigest)
|
|
224
|
+
throw packageError("PACKAGE_CHANGED", "项目安装包与任务启动时不一致,请恢复原版本,或检查输入并创建新任务。");
|
|
225
|
+
}
|
|
226
|
+
readOnly(job, scope) {
|
|
227
|
+
return (job.scope.revision !== scope.revision || (!!this.options.describePackage && !job.package));
|
|
228
|
+
}
|
|
129
229
|
initialize() {
|
|
130
230
|
return (this.ready ??= this.load());
|
|
131
231
|
}
|
|
132
232
|
async load() {
|
|
133
233
|
await this.storage.initialize();
|
|
134
234
|
try {
|
|
235
|
+
const queues = await this.storage.readQueueCatalog(toolJobLimits.maxQueueCatalogBytes);
|
|
236
|
+
if (!Array.isArray(queues) || queues.length > toolJobLimits.maxQueues)
|
|
237
|
+
throw new Error("Invalid tool queue catalog");
|
|
238
|
+
for (const raw of queues) {
|
|
239
|
+
const scope = scopeValue(raw?.scope), key = queueKey(scope);
|
|
240
|
+
if (this.queues.has(key))
|
|
241
|
+
throw new Error("Duplicate tool queue scope");
|
|
242
|
+
this.queues.set(key, { scope, ...queueState(raw) });
|
|
243
|
+
}
|
|
135
244
|
const listing = await opendir(await this.storage.directory());
|
|
136
245
|
let examined = 0;
|
|
137
246
|
for await (const item of listing) {
|
|
@@ -181,6 +290,17 @@ export class PanelToolJobService {
|
|
|
181
290
|
catch {
|
|
182
291
|
/* A caller may reconnect via list/get. */
|
|
183
292
|
}
|
|
293
|
+
for (const listener of this.listeners) {
|
|
294
|
+
if (!sameScope(listener.scope, job.scope))
|
|
295
|
+
continue;
|
|
296
|
+
try {
|
|
297
|
+
const { input: _input, result: _result, ...event } = publicJob(job);
|
|
298
|
+
listener.send(event);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
/* One viewer must not interrupt persistence. */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
184
304
|
}
|
|
185
305
|
lookup(scope, id, mutate = false) {
|
|
186
306
|
if (typeof id !== "string")
|
|
@@ -192,6 +312,16 @@ export class PanelToolJobService {
|
|
|
192
312
|
throw new Error("old-revision tool jobs are read-only; create a new compatible task");
|
|
193
313
|
return job;
|
|
194
314
|
}
|
|
315
|
+
/** Scoped existence check for transports merging read-only legacy history. */
|
|
316
|
+
async has(rawScope, id) {
|
|
317
|
+
const scope = scopeValue(rawScope);
|
|
318
|
+
await this.initialize();
|
|
319
|
+
await this.authorize(scope);
|
|
320
|
+
if (typeof id !== "string")
|
|
321
|
+
throw new Error("tool job ID is required");
|
|
322
|
+
const job = this.jobs.get(id);
|
|
323
|
+
return !!job && sameScope(job.scope, scope, false);
|
|
324
|
+
}
|
|
195
325
|
async list(rawScope) {
|
|
196
326
|
const scope = scopeValue(rawScope);
|
|
197
327
|
await this.initialize();
|
|
@@ -199,18 +329,91 @@ export class PanelToolJobService {
|
|
|
199
329
|
return [...this.jobs.values()]
|
|
200
330
|
.filter((job) => sameScope(job.scope, scope, false))
|
|
201
331
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
202
|
-
.map((job) => ({ ...publicJob(job), readOnly: job
|
|
332
|
+
.map((job) => ({ ...publicJob(job), readOnly: this.readOnly(job, scope) }));
|
|
203
333
|
}
|
|
204
334
|
async get(rawScope, id) {
|
|
205
335
|
const scope = scopeValue(rawScope);
|
|
206
336
|
await this.initialize();
|
|
207
337
|
await this.authorize(scope);
|
|
208
338
|
const job = this.lookup(scope, id);
|
|
209
|
-
return { ...publicJob(job), readOnly: job
|
|
339
|
+
return { ...publicJob(job), readOnly: this.readOnly(job, scope) };
|
|
340
|
+
}
|
|
341
|
+
async find(rawScope, requestKey) {
|
|
342
|
+
const scope = scopeValue(rawScope);
|
|
343
|
+
if (typeof requestKey !== "string" || !requestKey || requestKey.length > 160)
|
|
344
|
+
throw new Error("invalid tool job request key");
|
|
345
|
+
await this.initialize();
|
|
346
|
+
await this.authorize(scope);
|
|
347
|
+
const job = [...this.jobs.values()].find((item) => sameScope(item.scope, scope) && item.requestKey === requestKey);
|
|
348
|
+
return job ? publicJob(job) : null;
|
|
349
|
+
}
|
|
350
|
+
queueFor(scope) {
|
|
351
|
+
return queueState(this.queues.get(queueKey(scope)) ?? {
|
|
352
|
+
revision: 0,
|
|
353
|
+
paused: false,
|
|
354
|
+
maxConcurrent: toolJobLimits.maxConcurrent,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async getQueue(rawScope) {
|
|
358
|
+
const scope = scopeValue(rawScope);
|
|
359
|
+
await this.initialize();
|
|
360
|
+
await this.authorize(scope);
|
|
361
|
+
return this.queueFor(scope);
|
|
362
|
+
}
|
|
363
|
+
async setQueue(rawScope, update) {
|
|
364
|
+
const scope = scopeValue(rawScope);
|
|
365
|
+
const desired = queueState({
|
|
366
|
+
revision: update?.expectedRevision,
|
|
367
|
+
paused: update?.paused,
|
|
368
|
+
maxConcurrent: update?.maxConcurrent,
|
|
369
|
+
});
|
|
370
|
+
if (desired.revision >= Number.MAX_SAFE_INTEGER)
|
|
371
|
+
throw new Error("Tool queue revision exhausted");
|
|
372
|
+
await this.initialize();
|
|
373
|
+
await this.authorize(scope);
|
|
374
|
+
const result = await this.exclusive(async () => {
|
|
375
|
+
if (this.stopping)
|
|
376
|
+
throw new Error("tool job service is shutting down");
|
|
377
|
+
await this.authorize(scope);
|
|
378
|
+
const current = this.queueFor(scope);
|
|
379
|
+
if (current.revision !== desired.revision)
|
|
380
|
+
return { saved: false, queue: current };
|
|
381
|
+
const key = queueKey(scope);
|
|
382
|
+
if (!this.queues.has(key) && this.queues.size >= toolJobLimits.maxQueues)
|
|
383
|
+
throw new Error("Tool queue catalog is full");
|
|
384
|
+
const next = { scope, ...desired, revision: current.revision + 1 };
|
|
385
|
+
const catalog = new Map(this.queues);
|
|
386
|
+
catalog.set(key, next);
|
|
387
|
+
await this.storage.writeQueueCatalog([...catalog.values()], toolJobLimits.maxQueueCatalogBytes);
|
|
388
|
+
this.queues.set(key, next);
|
|
389
|
+
return { saved: true, queue: queueState(next) };
|
|
390
|
+
});
|
|
391
|
+
this.schedule();
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
runnable(job) {
|
|
395
|
+
if (job.status !== "queued")
|
|
396
|
+
return false;
|
|
397
|
+
const queue = this.queueFor(job.scope);
|
|
398
|
+
if (queue.paused)
|
|
399
|
+
return false;
|
|
400
|
+
let active = 0;
|
|
401
|
+
for (const id of this.active.keys()) {
|
|
402
|
+
const running = this.jobs.get(id);
|
|
403
|
+
if (running && sameScope(running.scope, job.scope))
|
|
404
|
+
active++;
|
|
405
|
+
}
|
|
406
|
+
return active < queue.maxConcurrent;
|
|
407
|
+
}
|
|
408
|
+
pendingCount(scope) {
|
|
409
|
+
return ([...this.jobs.values()].filter((job) => sameScope(job.scope, scope) && !TERMINAL.has(job.status)).length + [...this.preparing.values()].filter((job) => sameScope(job.scope, scope)).length);
|
|
210
410
|
}
|
|
211
411
|
async start(rawScope, request, signal) {
|
|
212
|
-
signal?.throwIfAborted();
|
|
213
412
|
const scope = scopeValue(rawScope);
|
|
413
|
+
return panelExecutionGate.run(scope, () => this.startAdmitted(scope, request, signal));
|
|
414
|
+
}
|
|
415
|
+
async startAdmitted(scope, request, signal) {
|
|
416
|
+
signal?.throwIfAborted();
|
|
214
417
|
const entry = entryValue(request?.entry);
|
|
215
418
|
if (!["manual", "retry"].includes(request.recovery))
|
|
216
419
|
throw new Error("tool job recovery must be manual or retry");
|
|
@@ -232,6 +435,8 @@ export class PanelToolJobService {
|
|
|
232
435
|
await this.initialize();
|
|
233
436
|
await this.authorize(scope);
|
|
234
437
|
let done;
|
|
438
|
+
let settle;
|
|
439
|
+
let outcome;
|
|
235
440
|
const reservation = await this.exclusive(async () => {
|
|
236
441
|
signal?.throwIfAborted();
|
|
237
442
|
if (this.stopping)
|
|
@@ -243,6 +448,12 @@ export class PanelToolJobService {
|
|
|
243
448
|
throw new Error("tool job request key was already used for different input");
|
|
244
449
|
return { existing };
|
|
245
450
|
}
|
|
451
|
+
const pending = [...this.preparing.values()].find((item) => sameScope(item.scope, scope) && item.requestKey === request.requestKey);
|
|
452
|
+
if (pending) {
|
|
453
|
+
if (pending.requestDigest !== requestDigest)
|
|
454
|
+
throw new Error("tool job request key was already used for different input");
|
|
455
|
+
return { following: pending.result };
|
|
456
|
+
}
|
|
246
457
|
}
|
|
247
458
|
while (this.jobs.size + this.preparing.size >= toolJobLimits.maxJobs) {
|
|
248
459
|
const oldest = [...this.jobs.values()]
|
|
@@ -253,14 +464,18 @@ export class PanelToolJobService {
|
|
|
253
464
|
await this.storage.remove(oldest.id);
|
|
254
465
|
this.jobs.delete(oldest.id);
|
|
255
466
|
}
|
|
256
|
-
|
|
257
|
-
if (pending >= toolJobLimits.maxQueuedPerScope)
|
|
467
|
+
if (this.pendingCount(scope) >= toolJobLimits.maxQueuedPerScope)
|
|
258
468
|
throw new Error("too many queued tool jobs for this workspace");
|
|
259
469
|
const id = randomUUID();
|
|
260
470
|
const controller = new AbortController();
|
|
261
471
|
this.preparing.set(id, {
|
|
262
472
|
scope,
|
|
263
473
|
controller,
|
|
474
|
+
requestKey: request.requestKey,
|
|
475
|
+
requestDigest,
|
|
476
|
+
result: new Promise((resolveResult) => {
|
|
477
|
+
settle = resolveResult;
|
|
478
|
+
}),
|
|
264
479
|
done: new Promise((resolve) => {
|
|
265
480
|
done = resolve;
|
|
266
481
|
}),
|
|
@@ -269,6 +484,12 @@ export class PanelToolJobService {
|
|
|
269
484
|
});
|
|
270
485
|
if (reservation.existing)
|
|
271
486
|
return publicJob(reservation.existing);
|
|
487
|
+
if (reservation.following) {
|
|
488
|
+
const job = await waitPreparation(reservation.following, signal);
|
|
489
|
+
await this.authorize(scope);
|
|
490
|
+
signal?.throwIfAborted();
|
|
491
|
+
return toolJobJson(job, toolJobLimits.maxRecordBytes);
|
|
492
|
+
}
|
|
272
493
|
const { id, controller } = reservation;
|
|
273
494
|
const abort = () => controller.abort(signal?.reason);
|
|
274
495
|
signal?.addEventListener("abort", abort, { once: true });
|
|
@@ -276,6 +497,7 @@ export class PanelToolJobService {
|
|
|
276
497
|
abort();
|
|
277
498
|
try {
|
|
278
499
|
controller.signal.throwIfAborted();
|
|
500
|
+
const selectedPackage = await this.describePackage(scope);
|
|
279
501
|
const workDir = await this.storage.directory(id, true, true);
|
|
280
502
|
controller.signal.throwIfAborted();
|
|
281
503
|
const prepared = this.options.prepareInput
|
|
@@ -286,6 +508,7 @@ export class PanelToolJobService {
|
|
|
286
508
|
id: id,
|
|
287
509
|
scope,
|
|
288
510
|
entry,
|
|
511
|
+
...(selectedPackage ? { package: selectedPackage } : {}),
|
|
289
512
|
input: toolJobJson(prepared, toolJobLimits.maxInputBytes),
|
|
290
513
|
recovery: request.recovery,
|
|
291
514
|
status: "queued",
|
|
@@ -297,6 +520,7 @@ export class PanelToolJobService {
|
|
|
297
520
|
requestDigest,
|
|
298
521
|
};
|
|
299
522
|
await this.authorize(scope);
|
|
523
|
+
await this.verifyPackage(job);
|
|
300
524
|
await this.exclusive(async () => {
|
|
301
525
|
if (this.stopping || controller.signal.aborted)
|
|
302
526
|
throw new Error("tool job input preparation was interrupted");
|
|
@@ -319,9 +543,12 @@ export class PanelToolJobService {
|
|
|
319
543
|
}
|
|
320
544
|
});
|
|
321
545
|
this.schedule();
|
|
322
|
-
|
|
546
|
+
const result = publicJob(job);
|
|
547
|
+
outcome = { job: publicJob(job) };
|
|
548
|
+
return result;
|
|
323
549
|
}
|
|
324
550
|
catch (error) {
|
|
551
|
+
outcome = { error };
|
|
325
552
|
const path = await this.storage.directory(id).catch(() => undefined);
|
|
326
553
|
if (path && !this.jobs.has(id))
|
|
327
554
|
await this.storage.remove(id);
|
|
@@ -330,6 +557,7 @@ export class PanelToolJobService {
|
|
|
330
557
|
finally {
|
|
331
558
|
signal?.removeEventListener("abort", abort);
|
|
332
559
|
this.preparing.delete(id);
|
|
560
|
+
settle(outcome);
|
|
333
561
|
done();
|
|
334
562
|
}
|
|
335
563
|
}
|
|
@@ -342,7 +570,7 @@ export class PanelToolJobService {
|
|
|
342
570
|
this.pumping = false;
|
|
343
571
|
if (!this.stopping &&
|
|
344
572
|
this.active.size < toolJobLimits.maxConcurrent &&
|
|
345
|
-
[...this.jobs.values()].some((job) => job
|
|
573
|
+
[...this.jobs.values()].some((job) => this.runnable(job)))
|
|
346
574
|
this.schedule();
|
|
347
575
|
}, () => {
|
|
348
576
|
this.pumping = false;
|
|
@@ -354,7 +582,7 @@ export class PanelToolJobService {
|
|
|
354
582
|
const selected = await this.exclusive(async () => {
|
|
355
583
|
if (this.stopping)
|
|
356
584
|
return;
|
|
357
|
-
const current = [...this.jobs.values()].find((job) => job
|
|
585
|
+
const current = [...this.jobs.values()].find((job) => this.runnable(job));
|
|
358
586
|
if (!current)
|
|
359
587
|
return;
|
|
360
588
|
const job = structuredClone(current);
|
|
@@ -390,6 +618,7 @@ export class PanelToolJobService {
|
|
|
390
618
|
let failure;
|
|
391
619
|
try {
|
|
392
620
|
await this.authorize(job.scope);
|
|
621
|
+
await this.verifyPackage(job);
|
|
393
622
|
if (this.stopping)
|
|
394
623
|
controller.abort();
|
|
395
624
|
if (controller.signal.aborted)
|
|
@@ -472,6 +701,9 @@ export class PanelToolJobService {
|
|
|
472
701
|
}
|
|
473
702
|
async retry(rawScope, id) {
|
|
474
703
|
const scope = scopeValue(rawScope);
|
|
704
|
+
return panelExecutionGate.run(scope, () => this.retryAdmitted(scope, id));
|
|
705
|
+
}
|
|
706
|
+
async retryAdmitted(scope, id) {
|
|
475
707
|
await this.initialize();
|
|
476
708
|
await this.authorize(scope);
|
|
477
709
|
const job = await this.exclusive(async () => {
|
|
@@ -482,6 +714,9 @@ export class PanelToolJobService {
|
|
|
482
714
|
throw new Error("tool job must finish stopping before retry");
|
|
483
715
|
if (current.recovery !== "retry" || !current.error?.retryable)
|
|
484
716
|
throw new Error("tool job requires a new manually reviewed request");
|
|
717
|
+
if (this.pendingCount(scope) >= toolJobLimits.maxQueuedPerScope)
|
|
718
|
+
throw new Error("too many queued tool jobs for this workspace");
|
|
719
|
+
await this.verifyPackage(current);
|
|
485
720
|
const next = structuredClone(current);
|
|
486
721
|
next.status = "queued";
|
|
487
722
|
delete next.error;
|
|
@@ -494,12 +729,30 @@ export class PanelToolJobService {
|
|
|
494
729
|
this.schedule();
|
|
495
730
|
return job;
|
|
496
731
|
}
|
|
497
|
-
async
|
|
732
|
+
async cancelProject(projectPath, appId) {
|
|
733
|
+
await this.initialize();
|
|
734
|
+
const project = resolve(projectPath);
|
|
735
|
+
const ids = appId
|
|
736
|
+
? [appId]
|
|
737
|
+
: [
|
|
738
|
+
...new Set([
|
|
739
|
+
...[...this.preparing.values()]
|
|
740
|
+
.filter((item) => item.scope.projectPath === project)
|
|
741
|
+
.map((item) => item.scope.appId),
|
|
742
|
+
...[...this.jobs.values()]
|
|
743
|
+
.filter((job) => job.scope.projectPath === project)
|
|
744
|
+
.map((job) => job.scope.appId),
|
|
745
|
+
]),
|
|
746
|
+
];
|
|
747
|
+
await Promise.all(ids.map((id) => this.cancelApp(id, project)));
|
|
748
|
+
}
|
|
749
|
+
async cancelApp(appId, projectPath) {
|
|
498
750
|
await this.initialize();
|
|
751
|
+
const matches = (scope) => scope.appId === appId && (!projectPath || scope.projectPath === resolve(projectPath));
|
|
499
752
|
for (const item of this.preparing.values())
|
|
500
|
-
if (item.scope
|
|
753
|
+
if (matches(item.scope))
|
|
501
754
|
item.controller.abort();
|
|
502
|
-
const matching = [...this.jobs.values()].filter((job) => job.scope
|
|
755
|
+
const matching = [...this.jobs.values()].filter((job) => matches(job.scope) && !TERMINAL.has(job.status));
|
|
503
756
|
// Revocation bypasses the now-revoked caller grant but still waits for real exits.
|
|
504
757
|
await this.exclusive(async () => {
|
|
505
758
|
for (const current of matching) {
|
|
@@ -551,6 +804,8 @@ export class PanelToolJobService {
|
|
|
551
804
|
await this.commit(job);
|
|
552
805
|
}
|
|
553
806
|
});
|
|
807
|
+
this.listeners.clear();
|
|
554
808
|
await this.storage.close();
|
|
809
|
+
this.unregisterExecution();
|
|
555
810
|
}
|
|
556
811
|
}
|
package/dist/panels/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface PanelGitSource {
|
|
|
12
12
|
commit: string;
|
|
13
13
|
}
|
|
14
14
|
export interface ManagedPanel extends Omit<PanelAppPreview, "reviewToken" | "alreadyInstalled" | "warnings" | "source"> {
|
|
15
|
+
packageDigest?: string;
|
|
15
16
|
revision: string;
|
|
16
17
|
bound: boolean;
|
|
17
18
|
enabled: boolean;
|
|
@@ -27,10 +28,21 @@ export interface ManagedPanel extends Omit<PanelAppPreview, "reviewToken" | "alr
|
|
|
27
28
|
};
|
|
28
29
|
compatibility: PanelCompatibility;
|
|
29
30
|
}
|
|
31
|
+
export interface PanelPackageIssue {
|
|
32
|
+
id: string;
|
|
33
|
+
revision: string;
|
|
34
|
+
code: "package_unavailable";
|
|
35
|
+
version?: string;
|
|
36
|
+
packageDigest?: string;
|
|
37
|
+
bound: boolean;
|
|
38
|
+
globalDisabled: boolean;
|
|
39
|
+
}
|
|
30
40
|
export interface PanelSnapshot {
|
|
31
41
|
panels: ManagedPanel[];
|
|
42
|
+
issues?: PanelPackageIssue[];
|
|
32
43
|
workspace: string;
|
|
33
44
|
hasProject: boolean;
|
|
45
|
+
canRestorePackages?: boolean;
|
|
34
46
|
}
|
|
35
47
|
export interface PanelDiscovery {
|
|
36
48
|
panels: GitPanelAppDiscoveryCandidate[];
|
|
@@ -46,7 +58,38 @@ export interface PanelReview {
|
|
|
46
58
|
expectedRevision: string | null;
|
|
47
59
|
compatibility: PanelCompatibility;
|
|
48
60
|
}
|
|
61
|
+
/** Trusted native hosts may review local sources without exposing paths to Web routes. */
|
|
62
|
+
export interface PanelProjectReview extends Omit<PanelReview, "source"> {
|
|
63
|
+
installedVersion?: string;
|
|
64
|
+
}
|
|
49
65
|
export interface PanelOperationContext {
|
|
50
66
|
ownerId: string;
|
|
51
67
|
authorize: () => boolean | Promise<boolean>;
|
|
52
68
|
}
|
|
69
|
+
export interface PanelPackageVersion {
|
|
70
|
+
version: string;
|
|
71
|
+
packageDigest: string;
|
|
72
|
+
permissions: PanelAppPreview["permissions"];
|
|
73
|
+
compatibility: PanelCompatibility;
|
|
74
|
+
}
|
|
75
|
+
export interface PanelPackageHistory {
|
|
76
|
+
appId: string;
|
|
77
|
+
title: PanelAppPreview["title"];
|
|
78
|
+
expectedRevision: string;
|
|
79
|
+
current: {
|
|
80
|
+
version: string;
|
|
81
|
+
packageDigest?: string;
|
|
82
|
+
unavailable?: boolean;
|
|
83
|
+
};
|
|
84
|
+
versions: PanelPackageVersion[];
|
|
85
|
+
unavailablePackages: number;
|
|
86
|
+
}
|
|
87
|
+
export interface PanelPackageRestoreReview extends PanelPackageVersion {
|
|
88
|
+
appId: string;
|
|
89
|
+
title: PanelAppPreview["title"];
|
|
90
|
+
current: PanelPackageHistory["current"];
|
|
91
|
+
addedPermissions: PanelAppPreview["permissions"];
|
|
92
|
+
expectedRevision: string;
|
|
93
|
+
reviewToken: string;
|
|
94
|
+
expiresAt: number;
|
|
95
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ProjectBackupOptions {
|
|
2
|
+
dataDir: string;
|
|
3
|
+
destination: string;
|
|
4
|
+
/** Locally available, administrator-reviewed Linux image containing tar. No pull occurs. */
|
|
5
|
+
helperImage: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ProjectRestoreOptions {
|
|
8
|
+
source: string;
|
|
9
|
+
destination: string;
|
|
10
|
+
helperImage: string;
|
|
11
|
+
}
|
|
12
|
+
/** Offline, owner-only backup. It never stops projects or modifies their volumes. */
|
|
13
|
+
export declare function backupProjectInstallation(options: ProjectBackupOptions): Promise<{
|
|
14
|
+
destination: string;
|
|
15
|
+
projects: number;
|
|
16
|
+
volumes: number;
|
|
17
|
+
installationId: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Restore into a new directory and new Docker volumes. Original installation is untouched. */
|
|
20
|
+
export declare function restoreProjectInstallation(options: ProjectRestoreOptions): Promise<{
|
|
21
|
+
destination: string;
|
|
22
|
+
installationId: string;
|
|
23
|
+
previousInstallationId: string;
|
|
24
|
+
projects: number;
|
|
25
|
+
volumes: number;
|
|
26
|
+
}>;
|