@cjhyy/code-shell-server 0.9.22 → 0.9.23
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/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/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/worker-bridge-core.d.ts +5 -0
- package/dist/worker-bridge-core.js +23 -0
- package/package.json +15 -6
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, lstat, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { processLimits } from "./process-state.js";
|
|
5
5
|
import { resourceRelativePath } from "./resources/directories.js";
|
|
6
6
|
import { materializePanelConnections, panelConnectionIds } from "./connections.js";
|
|
7
|
+
import { taskCookieSelection, } from "./task-cookies.js";
|
|
8
|
+
function directReads(input) {
|
|
9
|
+
return (input.resources ?? []).filter((resource) => resource.access === "read");
|
|
10
|
+
}
|
|
11
|
+
function originalUnavailable(state, during = false) {
|
|
12
|
+
return state === "missing"
|
|
13
|
+
? Object.assign(new Error("A read-only original is unavailable; reconnect it and retry"), {
|
|
14
|
+
code: "INPUT_MISSING",
|
|
15
|
+
retryable: true,
|
|
16
|
+
})
|
|
17
|
+
: Object.assign(new Error(during
|
|
18
|
+
? "A read-only original changed while the tool ran; check it before running again"
|
|
19
|
+
: "A read-only original changed; select it again"), { code: "INPUT_CHANGED", retryable: false });
|
|
20
|
+
}
|
|
7
21
|
function toolInput(value) {
|
|
8
22
|
if (!value ||
|
|
9
23
|
typeof value !== "object" ||
|
|
@@ -14,6 +28,8 @@ function toolInput(value) {
|
|
|
14
28
|
"directoryArguments",
|
|
15
29
|
"connectionIds",
|
|
16
30
|
"connectionArgument",
|
|
31
|
+
"originalsArgument",
|
|
32
|
+
"cookieArgument",
|
|
17
33
|
].includes(key)))
|
|
18
34
|
throw new Error("Invalid tool job input");
|
|
19
35
|
const input = value;
|
|
@@ -25,8 +41,11 @@ function toolInput(value) {
|
|
|
25
41
|
const paths = new Set();
|
|
26
42
|
for (const resource of input.resources ?? []) {
|
|
27
43
|
if (!resource ||
|
|
28
|
-
Object.keys(resource).some((key) => !["assetId", "path"].includes(key)) ||
|
|
29
|
-
!/^(?:asset|external)-[a-f0-9]{64}$/.test(resource.assetId)
|
|
44
|
+
Object.keys(resource).some((key) => !["assetId", "path", "access"].includes(key)) ||
|
|
45
|
+
!/^(?:asset|external)-[a-f0-9]{64}$/.test(resource.assetId) ||
|
|
46
|
+
(resource.access !== undefined && !["copy", "read"].includes(resource.access)) ||
|
|
47
|
+
// Library assets are Host custody; only a user-selected original may be read in place.
|
|
48
|
+
(resource.access === "read" && !resource.assetId.startsWith("external-")))
|
|
30
49
|
throw new Error("Invalid tool resource");
|
|
31
50
|
resourceRelativePath(resource.path);
|
|
32
51
|
if (paths.has(resource.path))
|
|
@@ -40,9 +59,13 @@ function toolInput(value) {
|
|
|
40
59
|
for (const item of input.directoryArguments ?? []) {
|
|
41
60
|
if (!item ||
|
|
42
61
|
!/^--[a-z][a-z0-9-]{0,63}$/.test(item.argumentName) ||
|
|
43
|
-
!["job", "app-data"].includes(item.directory) ||
|
|
44
|
-
Object.keys(item).some((key) => !["argumentName", "directory", "path"].includes(key)))
|
|
62
|
+
!["job", "app-data", "bookmark"].includes(item.directory) ||
|
|
63
|
+
Object.keys(item).some((key) => !["argumentName", "directory", "path", "bookmark"].includes(key)))
|
|
45
64
|
throw new Error("Invalid tool directory argument");
|
|
65
|
+
if (item.directory === "bookmark"
|
|
66
|
+
? typeof item.bookmark !== "string" || !/^[a-f0-9-]{36}$/i.test(item.bookmark)
|
|
67
|
+
: item.bookmark !== undefined)
|
|
68
|
+
throw new Error("Invalid tool directory bookmark");
|
|
46
69
|
if (argumentsSeen.has(item.argumentName))
|
|
47
70
|
throw new Error("Duplicate tool argument");
|
|
48
71
|
argumentsSeen.add(item.argumentName);
|
|
@@ -55,9 +78,31 @@ function toolInput(value) {
|
|
|
55
78
|
!/^--[a-z][a-z0-9-]{0,63}$/.test(input.connectionArgument) ||
|
|
56
79
|
argumentsSeen.has(input.connectionArgument))
|
|
57
80
|
throw new Error("Invalid connection argument");
|
|
81
|
+
argumentsSeen.add(input.connectionArgument);
|
|
58
82
|
}
|
|
59
83
|
else if (input.connectionArgument !== undefined)
|
|
60
84
|
throw new Error("Connection selection is required");
|
|
85
|
+
if (directReads(input).length) {
|
|
86
|
+
if (!input.originalsArgument ||
|
|
87
|
+
!/^--[a-z][a-z0-9-]{0,63}$/.test(input.originalsArgument) ||
|
|
88
|
+
argumentsSeen.has(input.originalsArgument))
|
|
89
|
+
throw new Error("Invalid originals argument");
|
|
90
|
+
argumentsSeen.add(input.originalsArgument);
|
|
91
|
+
}
|
|
92
|
+
else if (input.originalsArgument !== undefined)
|
|
93
|
+
throw new Error("Invalid originals argument: no resource uses read access");
|
|
94
|
+
if (input.cookieArgument !== undefined) {
|
|
95
|
+
const value = input.cookieArgument;
|
|
96
|
+
if (!value ||
|
|
97
|
+
typeof value !== "object" ||
|
|
98
|
+
Array.isArray(value) ||
|
|
99
|
+
typeof value.argumentName !== "string" ||
|
|
100
|
+
!/^--[a-z][a-z0-9-]{0,63}$/.test(value.argumentName) ||
|
|
101
|
+
argumentsSeen.has(value.argumentName))
|
|
102
|
+
throw new Error("Invalid Cookie argument");
|
|
103
|
+
const { argumentName: _argumentName, ...selection } = value;
|
|
104
|
+
taskCookieSelection(selection);
|
|
105
|
+
}
|
|
61
106
|
return input;
|
|
62
107
|
}
|
|
63
108
|
async function childDirectory(root, relative) {
|
|
@@ -76,32 +121,77 @@ async function childDirectory(root, relative) {
|
|
|
76
121
|
}
|
|
77
122
|
/** The only background processor: launch a reviewed tool and transport bounded JSON/files. */
|
|
78
123
|
export function createPanelToolExecutor(options) {
|
|
124
|
+
const bookmarkDirectory = async (scope, bookmark) => {
|
|
125
|
+
if (!options.resolveDirectoryBookmark)
|
|
126
|
+
throw new Error("This Host does not support task directory bookmarks");
|
|
127
|
+
return options.resolveDirectoryBookmark(scope, bookmark);
|
|
128
|
+
};
|
|
129
|
+
const authorize = async (scope, input) => {
|
|
130
|
+
await options.authorize(scope);
|
|
131
|
+
await authorizeDirectRead(scope, input);
|
|
132
|
+
for (const directory of input.directoryArguments ?? [])
|
|
133
|
+
if (directory.directory === "bookmark")
|
|
134
|
+
await bookmarkDirectory(scope, directory.bookmark);
|
|
135
|
+
if (input.cookieArgument) {
|
|
136
|
+
if (!options.cookies)
|
|
137
|
+
throw new Error("This Host does not support task Cookie authorization");
|
|
138
|
+
const { argumentName: _argumentName, ...selection } = input.cookieArgument;
|
|
139
|
+
await options.cookies.check(scope, selection);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const authorizeDirectRead = async (scope, input) => {
|
|
143
|
+
if (!directReads(input).length)
|
|
144
|
+
return;
|
|
145
|
+
if (!options.authorizeDirectRead)
|
|
146
|
+
throw new Error("This host cannot hand originals to tools; copy the resource instead");
|
|
147
|
+
await options.authorizeDirectRead(scope);
|
|
148
|
+
};
|
|
149
|
+
const materialize = async (scope, resources, workDir, signal) => {
|
|
150
|
+
const handle = createHash("sha256").update(workDir).digest("hex");
|
|
151
|
+
try {
|
|
152
|
+
for (const { assetId, path } of resources) {
|
|
153
|
+
await options.resources.dispatch(scope, "resources.materialize", { assetId, path, directoryHandle: handle }, {
|
|
154
|
+
signal,
|
|
155
|
+
resolveDirectory: async () => {
|
|
156
|
+
await options.authorize(scope);
|
|
157
|
+
return workDir;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
options.resources.releaseDirectory(scope, handle);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
/** Verify each original now; keys are the Guest's names, paths never leave the Host. */
|
|
167
|
+
const verifiedOriginals = async (scope, input, signal) => {
|
|
168
|
+
const originals = Object.create(null);
|
|
169
|
+
for (const resource of directReads(input)) {
|
|
170
|
+
const reference = await options.resources.references.get(scope, resource.assetId);
|
|
171
|
+
if (reference.state !== "available")
|
|
172
|
+
throw originalUnavailable(reference.state);
|
|
173
|
+
originals[resource.path] = {
|
|
174
|
+
path: await options.resources.references.location(scope, resource.assetId, { signal }),
|
|
175
|
+
bytes: reference.bytes,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return originals;
|
|
179
|
+
};
|
|
79
180
|
return {
|
|
80
181
|
async prepareInput(scope, raw, workDir, signal) {
|
|
81
182
|
const input = toolInput(raw);
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return workDir;
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
if (input.connectionIds)
|
|
95
|
-
await options.authorizeConnections(scope);
|
|
96
|
-
return input;
|
|
97
|
-
}
|
|
98
|
-
finally {
|
|
99
|
-
options.resources.releaseDirectory(scope, handle);
|
|
100
|
-
}
|
|
183
|
+
await authorize(scope, input);
|
|
184
|
+
await materialize(scope, (input.resources ?? []).filter((resource) => resource.access !== "read"), workDir, signal);
|
|
185
|
+
// Originals are verified again and sealed at each launch, so a retry cannot run
|
|
186
|
+
// against a file that changed after the task was admitted.
|
|
187
|
+
await verifiedOriginals(scope, input, signal);
|
|
188
|
+
if (input.connectionIds)
|
|
189
|
+
await options.authorizeConnections(scope);
|
|
190
|
+
return input;
|
|
101
191
|
},
|
|
102
192
|
async execute(job, context) {
|
|
103
193
|
const input = toolInput(job.input);
|
|
104
|
-
await
|
|
194
|
+
await authorize(job.scope, input);
|
|
105
195
|
let stdout = "", outputBytes = 0, result, failed;
|
|
106
196
|
let processId = "", terminal = false, stopped;
|
|
107
197
|
let progress = Promise.resolve();
|
|
@@ -257,8 +347,7 @@ export function createPanelToolExecutor(options) {
|
|
|
257
347
|
if (checking || terminal)
|
|
258
348
|
return;
|
|
259
349
|
checking = true;
|
|
260
|
-
void
|
|
261
|
-
.authorize(job.scope)
|
|
350
|
+
void authorize(job.scope, input)
|
|
262
351
|
.catch(() => {
|
|
263
352
|
failed ??= Object.assign(new Error("Tool task authorization was revoked"), {
|
|
264
353
|
code: "APP_REVOKED",
|
|
@@ -281,11 +370,17 @@ export function createPanelToolExecutor(options) {
|
|
|
281
370
|
});
|
|
282
371
|
if (entry.sha256 !== job.entry.sha256)
|
|
283
372
|
throw new Error("Installed tool changed; start a new job");
|
|
373
|
+
await authorizeDirectRead(job.scope, input);
|
|
374
|
+
const originals = await verifiedOriginals(job.scope, input, context.signal);
|
|
284
375
|
const directory = await options.processes.grantDirectory(owner, context.workDir);
|
|
285
376
|
resourceDirectoryHandles.push(directory.handle);
|
|
286
377
|
const fileArgumentHandles = [];
|
|
287
378
|
for (const item of input.directoryArguments ?? []) {
|
|
288
|
-
const root = item.directory === "job"
|
|
379
|
+
const root = item.directory === "job"
|
|
380
|
+
? context.workDir
|
|
381
|
+
: item.directory === "bookmark"
|
|
382
|
+
? await bookmarkDirectory(job.scope, item.bookmark)
|
|
383
|
+
: await options.appDataDirectory(job.scope);
|
|
289
384
|
const location = await childDirectory(root, item.path);
|
|
290
385
|
const grant = await options.processes.grantDirectory(owner, location);
|
|
291
386
|
const argument = await options.processes.grantDirectoryArgument(owner, {
|
|
@@ -307,6 +402,47 @@ export function createPanelToolExecutor(options) {
|
|
|
307
402
|
});
|
|
308
403
|
fileArgumentHandles.push(argument.handle);
|
|
309
404
|
}
|
|
405
|
+
if (Object.keys(originals).length) {
|
|
406
|
+
// A manifest rather than links: a Guest-chosen name must never redirect a tool's
|
|
407
|
+
// writes onto the user's file. The tool opens an original only on purpose.
|
|
408
|
+
await mkdir(options.sealedRoot, { recursive: true, mode: 0o700 });
|
|
409
|
+
const sealed = await mkdtemp(join(options.sealedRoot, "originals-"));
|
|
410
|
+
const cleanup = () => rm(sealed, { recursive: true, force: true }).catch(() => { });
|
|
411
|
+
cleanups.push(cleanup);
|
|
412
|
+
const path = join(sealed, "originals.json");
|
|
413
|
+
await writeFile(path, JSON.stringify({ originals }), { mode: 0o600, flag: "wx" });
|
|
414
|
+
const argument = await options.processes.grantFileArgument(owner, {
|
|
415
|
+
executableHandle: executable.handle,
|
|
416
|
+
argumentName: input.originalsArgument,
|
|
417
|
+
path,
|
|
418
|
+
cleanup,
|
|
419
|
+
// The approval may wait, and a reference may be forgotten or relinked
|
|
420
|
+
// while the child runs. Reuse the process grant's launch/live checks.
|
|
421
|
+
validate: async () => {
|
|
422
|
+
await authorize(job.scope, input);
|
|
423
|
+
const current = await verifiedOriginals(job.scope, input, context.signal);
|
|
424
|
+
for (const [key, original] of Object.entries(originals)) {
|
|
425
|
+
if (current[key]?.path !== original.path || current[key]?.bytes !== original.bytes)
|
|
426
|
+
throw originalUnavailable("changed", true);
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
fileArgumentHandles.push(argument.handle);
|
|
431
|
+
}
|
|
432
|
+
if (input.cookieArgument) {
|
|
433
|
+
const { argumentName, ...selection } = input.cookieArgument;
|
|
434
|
+
const sealed = await options.cookies.materialize(job.scope, selection);
|
|
435
|
+
cleanups.push(sealed.cleanup);
|
|
436
|
+
const argument = await options.processes.grantFileArgument(owner, {
|
|
437
|
+
executableHandle: executable.handle,
|
|
438
|
+
argumentName,
|
|
439
|
+
path: sealed.path,
|
|
440
|
+
cleanup: () => {
|
|
441
|
+
void sealed.cleanup().catch(() => { });
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
fileArgumentHandles.push(argument.handle);
|
|
445
|
+
}
|
|
310
446
|
context.signal.throwIfAborted();
|
|
311
447
|
const launched = await options.processes.start(owner, {
|
|
312
448
|
executableHandle: executable.handle,
|
|
@@ -365,6 +501,11 @@ export function createPanelToolExecutor(options) {
|
|
|
365
501
|
context.signal.throwIfAborted();
|
|
366
502
|
if (failed)
|
|
367
503
|
throw failed;
|
|
504
|
+
for (const resource of directReads(input)) {
|
|
505
|
+
const reference = await options.resources.references.get(job.scope, resource.assetId);
|
|
506
|
+
if (reference.state !== "available")
|
|
507
|
+
throw originalUnavailable(reference.state, true);
|
|
508
|
+
}
|
|
368
509
|
if (receipt.code !== 0 || result === undefined)
|
|
369
510
|
throw new Error("Tool did not return a completed result");
|
|
370
511
|
const value = result;
|
|
@@ -398,7 +539,7 @@ export function createPanelToolExecutor(options) {
|
|
|
398
539
|
}
|
|
399
540
|
value.artifacts = assets;
|
|
400
541
|
}
|
|
401
|
-
await
|
|
542
|
+
await authorize(job.scope, input);
|
|
402
543
|
return value;
|
|
403
544
|
}
|
|
404
545
|
finally {
|
|
@@ -414,7 +555,7 @@ export function createPanelToolExecutor(options) {
|
|
|
414
555
|
options.resources.releaseDirectory(job.scope, handle);
|
|
415
556
|
options.releaseOwner(owner);
|
|
416
557
|
for (const cleanup of cleanups)
|
|
417
|
-
cleanup();
|
|
558
|
+
await cleanup();
|
|
418
559
|
}
|
|
419
560
|
},
|
|
420
561
|
};
|
|
@@ -11,6 +11,8 @@ export declare class ToolJobStorage {
|
|
|
11
11
|
directory(id?: string, work?: boolean, create?: boolean): Promise<string>;
|
|
12
12
|
read(path: string, limit: number): Promise<unknown>;
|
|
13
13
|
write(id: string, value: unknown, limit: number): Promise<void>;
|
|
14
|
+
readQueueCatalog(limit: number): Promise<unknown>;
|
|
15
|
+
writeQueueCatalog(value: unknown, limit: number): Promise<void>;
|
|
14
16
|
close(): Promise<void>;
|
|
15
17
|
remove(id: string): Promise<void>;
|
|
16
18
|
}
|
|
@@ -149,6 +149,40 @@ export class ToolJobStorage {
|
|
|
149
149
|
await rm(temporary, { force: true }).catch(() => { });
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
async readQueueCatalog(limit) {
|
|
153
|
+
try {
|
|
154
|
+
return await this.read(join(await this.directory(), "queues.json"), limit);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (error.code === "ENOENT")
|
|
158
|
+
return [];
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async writeQueueCatalog(value, limit) {
|
|
163
|
+
const directory = await this.directory();
|
|
164
|
+
const temporary = join(directory, `${randomUUID()}.tmp`);
|
|
165
|
+
try {
|
|
166
|
+
const file = await open(temporary, "wx", 0o600);
|
|
167
|
+
try {
|
|
168
|
+
await file.writeFile(JSON.stringify(toolJobJson(value, limit)));
|
|
169
|
+
await file.sync();
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await file.close();
|
|
173
|
+
}
|
|
174
|
+
await this.directory();
|
|
175
|
+
await rename(temporary, join(directory, "queues.json"));
|
|
176
|
+
const parent = await open(directory, "r").catch(() => null);
|
|
177
|
+
if (parent) {
|
|
178
|
+
await parent.sync().catch(() => { });
|
|
179
|
+
await parent.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
152
186
|
async close() {
|
|
153
187
|
if (!this.nonce)
|
|
154
188
|
return;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const toolJobLimits: Readonly<{
|
|
2
2
|
maxJobs: 2000;
|
|
3
|
-
maxQueuedPerScope:
|
|
3
|
+
maxQueuedPerScope: 128;
|
|
4
|
+
maxQueues: 512;
|
|
5
|
+
maxQueueCatalogBytes: number;
|
|
4
6
|
maxConcurrent: 2;
|
|
5
7
|
maxInputBytes: number;
|
|
6
8
|
maxResultBytes: number;
|
|
@@ -13,10 +15,30 @@ export interface ToolJobScope {
|
|
|
13
15
|
projectPath: string;
|
|
14
16
|
revision: string;
|
|
15
17
|
}
|
|
18
|
+
/** Scope-owned scheduling; pausing stops admission to execution, not active processes. */
|
|
19
|
+
export interface ToolQueueState {
|
|
20
|
+
revision: number;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
maxConcurrent: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ToolQueueWriteResult {
|
|
25
|
+
saved: boolean;
|
|
26
|
+
queue: ToolQueueState;
|
|
27
|
+
}
|
|
28
|
+
export interface ToolQueueUpdate {
|
|
29
|
+
expectedRevision: number;
|
|
30
|
+
paused: boolean;
|
|
31
|
+
maxConcurrent: number;
|
|
32
|
+
}
|
|
16
33
|
export interface ToolJobEntry {
|
|
17
34
|
name: string;
|
|
18
35
|
sha256: string;
|
|
19
36
|
}
|
|
37
|
+
/** Host-selected package identity, never supplied by the task request. */
|
|
38
|
+
export interface ToolJobPackage {
|
|
39
|
+
version: string;
|
|
40
|
+
packageDigest: string;
|
|
41
|
+
}
|
|
20
42
|
export interface ToolJobProgress {
|
|
21
43
|
fraction?: number;
|
|
22
44
|
stage?: string;
|
|
@@ -27,6 +49,7 @@ export interface ToolJob {
|
|
|
27
49
|
id: string;
|
|
28
50
|
scope: ToolJobScope;
|
|
29
51
|
entry: ToolJobEntry;
|
|
52
|
+
package?: ToolJobPackage;
|
|
30
53
|
input: unknown;
|
|
31
54
|
recovery: "manual" | "retry";
|
|
32
55
|
status: ToolJobStatus;
|
|
@@ -44,6 +67,7 @@ export interface ToolJob {
|
|
|
44
67
|
};
|
|
45
68
|
result?: unknown;
|
|
46
69
|
}
|
|
70
|
+
export type ToolJobEvent = Omit<ToolJob, "input" | "result">;
|
|
47
71
|
export interface ToolJobRequest {
|
|
48
72
|
entry: ToolJobEntry;
|
|
49
73
|
input: unknown;
|
|
@@ -62,6 +86,8 @@ export interface PanelToolJobServiceOptions {
|
|
|
62
86
|
/** Freeze authorized inputs into workDir; persist only relative names and safe JSON. */
|
|
63
87
|
prepareInput?(scope: ToolJobScope, input: unknown, workDir: string, signal: AbortSignal): Promise<unknown>;
|
|
64
88
|
isAuthorized?(scope: ToolJobScope): boolean | Promise<boolean>;
|
|
89
|
+
/** Resolve and inspect the authorized project package, including its full content digest. */
|
|
90
|
+
describePackage?(scope: ToolJobScope): Promise<ToolJobPackage>;
|
|
65
91
|
onEvent?(job: ToolJob): void;
|
|
66
92
|
now?: () => number;
|
|
67
93
|
}
|
|
@@ -70,6 +96,7 @@ export declare class PanelToolJobService {
|
|
|
70
96
|
private readonly options;
|
|
71
97
|
private readonly storage;
|
|
72
98
|
private readonly jobs;
|
|
99
|
+
private readonly queues;
|
|
73
100
|
private readonly active;
|
|
74
101
|
private readonly preparing;
|
|
75
102
|
private ready?;
|
|
@@ -77,28 +104,47 @@ export declare class PanelToolJobService {
|
|
|
77
104
|
private stopping;
|
|
78
105
|
private pumping;
|
|
79
106
|
private shutdownPromise?;
|
|
107
|
+
private readonly unregisterExecution;
|
|
80
108
|
constructor(options: PanelToolJobServiceOptions);
|
|
81
|
-
|
|
109
|
+
private readonly listeners;
|
|
110
|
+
/** Trusted Host subscription. Transports must recheck their viewer's authorization. */
|
|
111
|
+
subscribe(rawScope: ToolJobScope, send: (job: ToolJobEvent) => void): () => void;
|
|
112
|
+
private executionScopes;
|
|
113
|
+
activeCount(projectPath?: string): number;
|
|
82
114
|
private now;
|
|
83
115
|
private exclusive;
|
|
84
116
|
private authorize;
|
|
117
|
+
private describePackage;
|
|
118
|
+
private verifyPackage;
|
|
119
|
+
private readOnly;
|
|
85
120
|
initialize(): Promise<void>;
|
|
86
121
|
private load;
|
|
87
122
|
private commit;
|
|
88
123
|
private lookup;
|
|
124
|
+
/** Scoped existence check for transports merging read-only legacy history. */
|
|
125
|
+
has(rawScope: ToolJobScope, id: string): Promise<boolean>;
|
|
89
126
|
list(rawScope: ToolJobScope): Promise<Array<ToolJob & {
|
|
90
127
|
readOnly: boolean;
|
|
91
128
|
}>>;
|
|
92
129
|
get(rawScope: ToolJobScope, id: string): Promise<ToolJob & {
|
|
93
130
|
readOnly: boolean;
|
|
94
131
|
}>;
|
|
132
|
+
find(rawScope: ToolJobScope, requestKey: string): Promise<ToolJob | null>;
|
|
133
|
+
private queueFor;
|
|
134
|
+
getQueue(rawScope: ToolJobScope): Promise<ToolQueueState>;
|
|
135
|
+
setQueue(rawScope: ToolJobScope, update: ToolQueueUpdate): Promise<ToolQueueWriteResult>;
|
|
136
|
+
private runnable;
|
|
137
|
+
private pendingCount;
|
|
95
138
|
start(rawScope: ToolJobScope, request: ToolJobRequest, signal?: AbortSignal): Promise<ToolJob>;
|
|
139
|
+
private startAdmitted;
|
|
96
140
|
private schedule;
|
|
97
141
|
private pump;
|
|
98
142
|
private run;
|
|
99
143
|
cancel(rawScope: ToolJobScope, id: string): Promise<ToolJob>;
|
|
100
144
|
retry(rawScope: ToolJobScope, id: string): Promise<ToolJob>;
|
|
101
|
-
|
|
145
|
+
private retryAdmitted;
|
|
146
|
+
cancelProject(projectPath: string, appId?: string): Promise<void>;
|
|
147
|
+
cancelApp(appId: string, projectPath?: string): Promise<void>;
|
|
102
148
|
shutdown(): Promise<void>;
|
|
103
149
|
private stop;
|
|
104
150
|
}
|