@cjhyy/code-shell-server 0.9.46 → 0.9.48
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 +4 -0
- package/dist/hub/auth-store.d.ts +2 -0
- package/dist/hub/auth-store.js +26 -7
- package/dist/mobile-remote/codex-room-agent.d.ts +0 -6
- package/dist/mobile-remote/codex-room-agent.js +32 -32
- package/dist/mobile-remote/mobile-chat-turn.d.ts +6 -0
- package/dist/mobile-remote/mobile-chat-turn.js +15 -2
- package/dist/mobile-remote/mobile-client-event-validator.js +8 -0
- package/dist/mobile-remote/mobile-run-dispatch.d.ts +1 -0
- package/dist/mobile-remote/mobile-run-dispatch.js +13 -5
- package/dist/mobile-remote/mobile-static.d.ts +5 -6
- package/dist/mobile-remote/mobile-static.js +13 -14
- package/dist/mobile-remote/remote-host-manager.d.ts +2 -0
- package/dist/mobile-remote/remote-host-manager.js +27 -5
- package/dist/mobile-remote/resident-agent.js +27 -22
- package/dist/mobile-remote/room-manager.d.ts +4 -0
- package/dist/mobile-remote/room-manager.js +9 -2
- package/dist/mobile-remote/room-process-stop.d.ts +11 -0
- package/dist/mobile-remote/room-process-stop.js +46 -0
- package/dist/panels/agent-task-service.d.ts +2 -1
- package/dist/panels/agent-task-service.js +10 -3
- package/dist/panels/execution-gate.d.ts +22 -2
- package/dist/panels/execution-gate.js +42 -6
- package/dist/panels/http.d.ts +1 -0
- package/dist/panels/hub-automations.js +1 -0
- package/dist/panels/management-http.d.ts +1 -0
- package/dist/panels/management.d.ts +3 -0
- package/dist/panels/management.js +63 -15
- package/dist/panels/package-mutation.d.ts +1 -1
- package/dist/panels/package-mutation.js +3 -10
- package/dist/panels/resources/uploads.js +15 -6
- package/dist/panels/runtime.js +12 -2
- package/dist/panels/task-cookies.js +8 -16
- package/dist/panels/tool-jobs.js +7 -2
- package/dist/serve/headless-server.js +113 -79
- package/dist/static-files.d.ts +7 -0
- package/dist/static-files.js +114 -0
- package/package.json +6 -6
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `close` waits for every holder of the child's stdout/stderr. A grandchild the
|
|
31
|
+
* CLI backgrounded with inherited stdio (a dev server, say) keeps them open
|
|
32
|
+
* after the CLI exits, so the room would never settle. Once the CLI has exited
|
|
33
|
+
* and its buffered output had a grace period to drain, release the pipes;
|
|
34
|
+
* the grandchild itself is left running.
|
|
35
|
+
*/
|
|
36
|
+
export function releaseStdioAfterExit(child, graceMs = 1_000) {
|
|
37
|
+
child.once("exit", () => {
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.stdout?.destroy();
|
|
40
|
+
child.stderr?.destroy();
|
|
41
|
+
child.stdin?.destroy();
|
|
42
|
+
}, graceMs);
|
|
43
|
+
timer.unref();
|
|
44
|
+
child.once("close", () => clearTimeout(timer));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -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();
|
|
@@ -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,10 @@ 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;
|
|
24
|
-
|
|
40
|
+
/** Executions that a change matching `matches` would wait for, grouped by holder. */
|
|
41
|
+
blockers(matches: (scope: PanelExecutionScope) => boolean): PanelExecutionBlocker[];
|
|
42
|
+
mutate<T>(matches: (scope: PanelExecutionScope) => boolean, work: () => Promise<T>,
|
|
43
|
+
/** Trusted Host preparation that validates a review before draining existing work. */
|
|
44
|
+
drain?: () => Promise<void>): Promise<T>;
|
|
25
45
|
}
|
|
26
46
|
export declare const panelExecutionGate: PanelExecutionGate;
|
|
@@ -10,9 +10,13 @@ export function panelExecutionProject(path) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export class PanelExecutionBusyError extends Error {
|
|
13
|
+
blockers;
|
|
13
14
|
status = 409;
|
|
14
|
-
constructor(message
|
|
15
|
+
constructor(message,
|
|
16
|
+
/** Present when executions block a change; empty while another change runs. */
|
|
17
|
+
blockers = []) {
|
|
15
18
|
super(message);
|
|
19
|
+
this.blockers = blockers;
|
|
16
20
|
this.name = "PanelExecutionBusyError";
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -25,7 +29,11 @@ export class PanelExecutionGate {
|
|
|
25
29
|
sources = new Set();
|
|
26
30
|
mutations = new Set();
|
|
27
31
|
enter(scope) {
|
|
28
|
-
const held = {
|
|
32
|
+
const held = {
|
|
33
|
+
appId: scope.appId,
|
|
34
|
+
projectPath: panelExecutionProject(scope.projectPath),
|
|
35
|
+
...(scope.holder ? { holder: { ...scope.holder } } : {}),
|
|
36
|
+
};
|
|
29
37
|
if ([...this.mutations].some((matches) => matches(held)))
|
|
30
38
|
throw new PanelExecutionBusyError("面板正在更新,请完成后重试。");
|
|
31
39
|
this.executions.add(held);
|
|
@@ -49,15 +57,43 @@ export class PanelExecutionGate {
|
|
|
49
57
|
this.sources.delete(source);
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
|
-
|
|
60
|
+
/** Executions that a change matching `matches` would wait for, grouped by holder. */
|
|
61
|
+
blockers(matches) {
|
|
62
|
+
const grouped = new Map();
|
|
63
|
+
const scopes = [...this.executions, ...[...this.sources].flatMap((source) => [...source()])];
|
|
64
|
+
for (const scope of scopes) {
|
|
65
|
+
if (!matches(scope))
|
|
66
|
+
continue;
|
|
67
|
+
const blocker = {
|
|
68
|
+
appId: scope.appId,
|
|
69
|
+
projectPath: scope.projectPath,
|
|
70
|
+
...(scope.holder ? { holder: { ...scope.holder } } : {}),
|
|
71
|
+
};
|
|
72
|
+
const key = JSON.stringify(blocker), existing = grouped.get(key);
|
|
73
|
+
if (existing)
|
|
74
|
+
existing.count++;
|
|
75
|
+
else
|
|
76
|
+
grouped.set(key, { ...blocker, count: 1 });
|
|
77
|
+
}
|
|
78
|
+
return [...grouped.values()];
|
|
79
|
+
}
|
|
80
|
+
async mutate(matches, work,
|
|
81
|
+
/** Trusted Host preparation that validates a review before draining existing work. */
|
|
82
|
+
drain) {
|
|
53
83
|
if (this.mutations.size)
|
|
54
84
|
throw new PanelExecutionBusyError("另一个面板修改正在进行,请稍后重试。");
|
|
55
85
|
// No await between the snapshot and installing the admission barrier.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new PanelExecutionBusyError("面板还有正在提交、排队或运行的任务,请结束后再修改。");
|
|
86
|
+
const blockers = this.blockers(matches);
|
|
87
|
+
if (blockers.length && !drain)
|
|
88
|
+
throw new PanelExecutionBusyError("面板还有正在提交、排队或运行的任务,请结束后再修改。", blockers);
|
|
59
89
|
this.mutations.add(matches);
|
|
60
90
|
try {
|
|
91
|
+
if (drain) {
|
|
92
|
+
await drain();
|
|
93
|
+
const remaining = this.blockers(matches);
|
|
94
|
+
if (remaining.length)
|
|
95
|
+
throw new PanelExecutionBusyError("面板还有正在提交、排队或运行的任务,请结束后再修改。", remaining);
|
|
96
|
+
}
|
|
61
97
|
return await work();
|
|
62
98
|
}
|
|
63
99
|
finally {
|
package/dist/panels/http.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
|
39
39
|
install(context: import("./types.js").PanelOperationContext, token: unknown, bind?: boolean, approval?: {
|
|
40
40
|
overwrite?: boolean;
|
|
41
41
|
expectedId?: string;
|
|
42
|
+
releaseExecutions?: (appId: string, matches: (scope: import("./execution-gate.js").PanelExecutionScope) => boolean) => Promise<void>;
|
|
42
43
|
}): Promise<{
|
|
43
44
|
id: string;
|
|
44
45
|
packageDigest: string | undefined;
|
|
@@ -30,6 +30,7 @@ export declare function createPanelManagementHttp(options: PanelManagementHttpOp
|
|
|
30
30
|
install(context: PanelOperationContext, token: unknown, bind?: boolean, approval?: {
|
|
31
31
|
overwrite?: boolean;
|
|
32
32
|
expectedId?: string;
|
|
33
|
+
releaseExecutions?: (appId: string, matches: (scope: import("./execution-gate.js").PanelExecutionScope) => boolean) => Promise<void>;
|
|
33
34
|
}): Promise<{
|
|
34
35
|
id: string;
|
|
35
36
|
packageDigest: string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PanelExecutionScope } from "./execution-gate.js";
|
|
1
2
|
import { type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
|
|
2
3
|
import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelProjectReview, PanelSnapshot, PanelPackageHistory, PanelPackageRestoreReview } from "./types.js";
|
|
3
4
|
export declare class PanelManagementError extends Error {
|
|
@@ -50,6 +51,8 @@ export declare function createPanelManagement(options: PanelManagementOptions):
|
|
|
50
51
|
install(context: PanelOperationContext, token: unknown, bind?: boolean, approval?: {
|
|
51
52
|
overwrite?: boolean;
|
|
52
53
|
expectedId?: string;
|
|
54
|
+
/** Trusted Host callback, never supplied by an HTTP request body. */
|
|
55
|
+
releaseExecutions?: (appId: string, matches: (scope: PanelExecutionScope) => boolean) => Promise<void>;
|
|
53
56
|
}): Promise<{
|
|
54
57
|
id: string;
|
|
55
58
|
packageDigest: string | undefined;
|
|
@@ -3,7 +3,7 @@ import { panelPackageMutationMatches } from "./package-mutation.js";
|
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { lstatSync, mkdirSync, statSync } from "node:fs";
|
|
5
5
|
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
6
|
+
import { SettingsManager, PanelAppAlreadyInstalledError, PanelAppInstallError, PanelAppReviewChangedError, assertSafePanelAppId, discoverGitPanelApps, installReviewedLocalPanelApp, invalidateSkillCache, listInstalledPanelApps, inspectProjectPanelApps, listRetainedPanelAppPackages, resolvePanelAppPackage, parsePanelAppPackagePins, currentPanelAppPackagePins, installedPanelAppUpdatedAt, retainInstalledPanelApp, userHome, panelAppsRoot, previewLocalPanelApp, resolveGitHubPanelAppCommit, resolvePanelAppBindingPolicy, uninstallPanelApp, } from "@cjhyy/code-shell-core";
|
|
7
7
|
import { mutateJsonFile } from "@cjhyy/code-shell-core/internal";
|
|
8
8
|
import { parseGithubUrl } from "@cjhyy/code-shell-core/internal/skills";
|
|
9
9
|
const REVIEW_TTL = 8 * 60_000;
|
|
@@ -177,14 +177,17 @@ export function createPanelManagement(options) {
|
|
|
177
177
|
.update(JSON.stringify({
|
|
178
178
|
app,
|
|
179
179
|
digest,
|
|
180
|
-
...(options.projectPackages
|
|
181
|
-
? { pin: parsePanelAppPackagePins(project ?? projectSettings())[app.id] ?? null }
|
|
182
|
-
: {}),
|
|
180
|
+
...(options.projectPackages ? { pin: selectedPin(app.id, project) } : {}),
|
|
183
181
|
bound: current.boundApps.has(app.id),
|
|
184
182
|
disabled: current.globalDisabledApps.has(app.id),
|
|
185
183
|
}))
|
|
186
184
|
.digest("hex");
|
|
187
185
|
}
|
|
186
|
+
/** The pinned package identity; when it was pinned does not change the selection. */
|
|
187
|
+
function selectedPin(id, project) {
|
|
188
|
+
const pin = parsePanelAppPackagePins(project ?? projectSettings())[id];
|
|
189
|
+
return pin ? { version: pin.version, packageDigest: pin.packageDigest } : null;
|
|
190
|
+
}
|
|
188
191
|
function projectSettings() {
|
|
189
192
|
return new SettingsManager(workspace, "full").getRawForScope("project", workspace, {
|
|
190
193
|
strict: true,
|
|
@@ -291,7 +294,7 @@ export function createPanelManagement(options) {
|
|
|
291
294
|
// Never attach a new revision to stale displayed binding state. File and
|
|
292
295
|
// package inspection above can yield to another device's mutation.
|
|
293
296
|
for (const app of [...panels, ...issues]) {
|
|
294
|
-
const pin = options.projectPackages ?
|
|
297
|
+
const pin = options.projectPackages ? currentPanelAppPackagePins(project)[app.id] : undefined;
|
|
295
298
|
if (projectState(app.id, project) !== projectState(app.id) ||
|
|
296
299
|
current.globalDisabledApps.has(app.id) !== policy().globalDisabledApps.has(app.id) ||
|
|
297
300
|
(pin && (pin.packageDigest !== app.packageDigest || pin.version !== app.version)))
|
|
@@ -484,7 +487,7 @@ export function createPanelManagement(options) {
|
|
|
484
487
|
return value;
|
|
485
488
|
});
|
|
486
489
|
}
|
|
487
|
-
async function mutate(context, change, work) {
|
|
490
|
+
async function mutate(context, change, work, prepare) {
|
|
488
491
|
const expected = generation(context.ownerId);
|
|
489
492
|
const guard = () => assertAuthorized(context, expected);
|
|
490
493
|
const run = async () => {
|
|
@@ -493,7 +496,11 @@ export function createPanelManagement(options) {
|
|
|
493
496
|
await guard();
|
|
494
497
|
return work(guard);
|
|
495
498
|
};
|
|
496
|
-
|
|
499
|
+
const matches = panelPackageMutationMatches({
|
|
500
|
+
...change,
|
|
501
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
502
|
+
});
|
|
503
|
+
return panelExecutionGate.mutate(matches, () => (options.withMutation ? options.withMutation(write) : write()), prepare ? () => prepare(guard, matches) : undefined);
|
|
497
504
|
};
|
|
498
505
|
const next = catalogMutation.then(run, run);
|
|
499
506
|
catalogMutation = next.catch(() => undefined);
|
|
@@ -524,7 +531,20 @@ export function createPanelManagement(options) {
|
|
|
524
531
|
if (bound) {
|
|
525
532
|
if (!app.packageDigest)
|
|
526
533
|
throw new Error("Cannot bind an unverified Panel package");
|
|
527
|
-
pins[app.id]
|
|
534
|
+
const existing = pins[app.id];
|
|
535
|
+
// Rebinding the package a current pin already selects changes nothing, so
|
|
536
|
+
// it must not invalidate other devices' revisions or pending reviews.
|
|
537
|
+
const unchanged = existing?.version === app.version &&
|
|
538
|
+
existing.packageDigest === app.packageDigest &&
|
|
539
|
+
!!currentPanelAppPackagePins({ panelAppPins: { [app.id]: existing } })[app.id];
|
|
540
|
+
if (!unchanged)
|
|
541
|
+
pins[app.id] = {
|
|
542
|
+
version: app.version,
|
|
543
|
+
packageDigest: app.packageDigest,
|
|
544
|
+
// Holds this project only until the next global install or update, so it
|
|
545
|
+
// must be strictly later than the install it was chosen after.
|
|
546
|
+
pinnedAt: new Date(Math.max(now(), (installedPanelAppUpdatedAt(app.id) ?? 0) + 1)).toISOString(),
|
|
547
|
+
};
|
|
528
548
|
}
|
|
529
549
|
else
|
|
530
550
|
delete pins[app.id];
|
|
@@ -735,12 +755,18 @@ export function createPanelManagement(options) {
|
|
|
735
755
|
await assertAuthorized(context, held.generation);
|
|
736
756
|
if (approval?.expectedId && approval.expectedId !== held.public.preview.id)
|
|
737
757
|
invalid();
|
|
758
|
+
if (approval?.releaseExecutions && held.public.kind !== "update")
|
|
759
|
+
invalid();
|
|
738
760
|
if (approval && held.public.kind === "update" && !approval.overwrite)
|
|
739
761
|
throw new PanelManagementError(409, "already_installed", "这个面板已经安装,请确认项目更新。");
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (reviews.get(token) !== held || held.public.expiresAt <= now())
|
|
762
|
+
let admitted = false;
|
|
763
|
+
const assertReviewLease = () => {
|
|
764
|
+
if (!admitted && (reviews.get(token) !== held || held.public.expiresAt <= now()))
|
|
743
765
|
throw new PanelManagementError(409, "review_expired", "预览已失效,请重新检查来源。");
|
|
766
|
+
};
|
|
767
|
+
const validateReview = async (guard) => {
|
|
768
|
+
await assertAuthorized(context, held.generation);
|
|
769
|
+
assertReviewLease();
|
|
744
770
|
const id = held.public.preview.id;
|
|
745
771
|
if (held.public.kind === "update") {
|
|
746
772
|
const current = await selectedApp(id);
|
|
@@ -753,6 +779,11 @@ export function createPanelManagement(options) {
|
|
|
753
779
|
if (projectState(id) !== held.projectState)
|
|
754
780
|
throw new PanelManagementError(409, "conflict", "项目面板配置已改变,请重新预览。");
|
|
755
781
|
await guard();
|
|
782
|
+
assertReviewLease();
|
|
783
|
+
};
|
|
784
|
+
return mutate(context, { appId: held.public.preview.id, kind: held.public.kind }, async (guard) => {
|
|
785
|
+
await validateReview(guard);
|
|
786
|
+
const id = held.public.preview.id;
|
|
756
787
|
const installed = await installReviewedLocalPanelApp(held.source, held.digest, new Date(now()).toISOString(), {
|
|
757
788
|
overwrite: held.public.kind === "update",
|
|
758
789
|
expectedId: id,
|
|
@@ -782,7 +813,24 @@ export function createPanelManagement(options) {
|
|
|
782
813
|
invalidateSkillCache();
|
|
783
814
|
await options.onChanged?.(id, held.public.kind);
|
|
784
815
|
return { id, packageDigest: installed.packageDigest };
|
|
785
|
-
}
|
|
816
|
+
}, approval?.releaseExecutions
|
|
817
|
+
? async (guard, matches) => {
|
|
818
|
+
await validateReview(guard);
|
|
819
|
+
// A local directory/ZIP may have changed since preview. Check its
|
|
820
|
+
// reviewed bytes before cancelling work, then recheck mutable
|
|
821
|
+
// project/catalog authority after the source read.
|
|
822
|
+
const preview = await previewLocalPanelApp(held.source);
|
|
823
|
+
if (preview.id !== held.public.preview.id || preview.reviewToken !== held.digest)
|
|
824
|
+
throw new PanelManagementError(409, "conflict", "来源已改变,请重新检查更新。");
|
|
825
|
+
await validateReview(guard);
|
|
826
|
+
// This admitted operation owns the review while its holders exit.
|
|
827
|
+
// TTL expiry during cancellation must not invalidate that consent;
|
|
828
|
+
// owner revocation and package/project changes are still checked.
|
|
829
|
+
admitted = true;
|
|
830
|
+
reviews.delete(token);
|
|
831
|
+
await approval.releaseExecutions(held.public.preview.id, matches);
|
|
832
|
+
}
|
|
833
|
+
: undefined);
|
|
786
834
|
},
|
|
787
835
|
async binding(context, id, bound, expected) {
|
|
788
836
|
assertId(id);
|
|
@@ -793,11 +841,11 @@ export function createPanelManagement(options) {
|
|
|
793
841
|
const current = await selectedApp(id);
|
|
794
842
|
if (bound && !compatibility(current.app).supported)
|
|
795
843
|
throw new PanelManagementError(400, "unsupported", "这个面板需要当前 Web 环境未提供的能力。");
|
|
796
|
-
//
|
|
797
|
-
//
|
|
844
|
+
// An unpinned (or superseded) selection needs a retained copy before the
|
|
845
|
+
// project can pin it. A current pin must keep its selected version.
|
|
798
846
|
const app = bound &&
|
|
799
847
|
options.projectPackages &&
|
|
800
|
-
!
|
|
848
|
+
!currentPanelAppPackagePins(projectSettings())[current.app.id]
|
|
801
849
|
? await retainInstalledPanelApp(current.app.id, current.app.packageDigest)
|
|
802
850
|
: current.app;
|
|
803
851
|
await guard();
|
|
@@ -4,5 +4,5 @@ export interface PanelPackageMutation {
|
|
|
4
4
|
projectPath: string;
|
|
5
5
|
kind: "install" | "update" | "binding" | "remove";
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/** A catalog install or update supersedes every project's pin, so it reaches every project. */
|
|
8
8
|
export declare function panelPackageMutationMatches(change: PanelPackageMutation): (scope: PanelExecutionScope) => boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolvePanelAppBindingProjectPath } from "@cjhyy/code-shell-core";
|
|
2
2
|
import { panelExecutionProject } from "./execution-gate.js";
|
|
3
|
-
/**
|
|
3
|
+
/** A catalog install or update supersedes every project's pin, so it reaches every project. */
|
|
4
4
|
export function panelPackageMutationMatches(change) {
|
|
5
5
|
const project = panelExecutionProject(resolvePanelAppBindingProjectPath(change.projectPath));
|
|
6
6
|
return (scope) => {
|
|
@@ -11,13 +11,6 @@ export function panelPackageMutationMatches(change) {
|
|
|
11
11
|
const bindingProject = resolvePanelAppBindingProjectPath(scope.projectPath);
|
|
12
12
|
if (panelExecutionProject(bindingProject) === project)
|
|
13
13
|
return true;
|
|
14
|
-
|
|
15
|
-
return false;
|
|
16
|
-
try {
|
|
17
|
-
return !projectPanelAppPackagePins(bindingProject, change.appId)[change.appId];
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return true;
|
|
21
|
-
} // An unreadable project cannot prove that it is insulated.
|
|
14
|
+
return change.kind !== "binding";
|
|
22
15
|
};
|
|
23
16
|
}
|
|
@@ -214,12 +214,21 @@ export class ResourceUploadIngest {
|
|
|
214
214
|
}
|
|
215
215
|
async cleanupExpired() {
|
|
216
216
|
await this.initialize();
|
|
217
|
-
for (const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
217
|
+
for (const key of [...this.records.keys()]) {
|
|
218
|
+
// Another upload may have renewed while cleanup waited on an earlier
|
|
219
|
+
// entry. Only the current expired record can cancel a running finish.
|
|
220
|
+
const record = this.records.get(key);
|
|
221
|
+
if (!record || record.expiresAt > this.options.now())
|
|
222
|
+
continue;
|
|
223
|
+
this.finishes.get(key)?.abort();
|
|
224
|
+
await this.locked(key, async () => {
|
|
225
|
+
// A write already holding this lock may acknowledge a chunk and renew
|
|
226
|
+
// the lease before cleanup enters. Never delete that renewed upload.
|
|
227
|
+
const current = this.records.get(key);
|
|
228
|
+
if (current && current.expiresAt <= this.options.now())
|
|
229
|
+
await this.remove(current);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
223
232
|
}
|
|
224
233
|
async begin(scope, raw) {
|
|
225
234
|
await this.authorize(scope);
|
package/dist/panels/runtime.js
CHANGED
|
@@ -553,6 +553,7 @@ export function createPanelRuntime(options) {
|
|
|
553
553
|
acquireExecution: (owner) => panelExecutionGate.enter({
|
|
554
554
|
appId: owner.appId,
|
|
555
555
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
556
|
+
holder: { kind: "process" },
|
|
556
557
|
}),
|
|
557
558
|
approvalScope: "guest",
|
|
558
559
|
resolvePackageEntry: async (owner, name) => {
|
|
@@ -941,7 +942,11 @@ export function createPanelRuntime(options) {
|
|
|
941
942
|
};
|
|
942
943
|
}
|
|
943
944
|
async function onPanelAction(scope, input) {
|
|
944
|
-
return panelExecutionGate.run({
|
|
945
|
+
return panelExecutionGate.run({
|
|
946
|
+
appId: scope.appId,
|
|
947
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
948
|
+
holder: { kind: "panel-call" },
|
|
949
|
+
}, () => onPanelActionAdmitted(scope, input));
|
|
945
950
|
}
|
|
946
951
|
async function onPanelActionAdmitted(scope, input) {
|
|
947
952
|
const grant = grants.get(scope.instanceId);
|
|
@@ -980,6 +985,7 @@ export function createPanelRuntime(options) {
|
|
|
980
985
|
const releaseExecution = panelExecutionGate.enter({
|
|
981
986
|
appId: grant.app.id,
|
|
982
987
|
projectPath: options.bindingCwd ?? options.cwd,
|
|
988
|
+
holder: { kind: "agent-tool" },
|
|
983
989
|
});
|
|
984
990
|
const requestId = randomBytes(18).toString("base64url");
|
|
985
991
|
const value = await new Promise((done) => {
|
|
@@ -1309,7 +1315,11 @@ export function createPanelRuntime(options) {
|
|
|
1309
1315
|
}
|
|
1310
1316
|
}
|
|
1311
1317
|
async function call(grant, input) {
|
|
1312
|
-
return panelExecutionGate.run({
|
|
1318
|
+
return panelExecutionGate.run({
|
|
1319
|
+
appId: grant.app.id,
|
|
1320
|
+
projectPath: options.bindingCwd ?? options.cwd,
|
|
1321
|
+
holder: { kind: "panel-call" },
|
|
1322
|
+
}, () => callAdmitted(grant, input));
|
|
1313
1323
|
}
|
|
1314
1324
|
async function callAdmitted(grant, input) {
|
|
1315
1325
|
if (!(await authorized(grant)))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { chmod, lstat, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
-
import { formatNetscapeCookies, parseCookieJar, } from "@cjhyy/code-shell-core";
|
|
4
|
+
import { formatNetscapeCookies, normalizeCookieDomain, cookieAppliesToHost, parseCookieJar, } from "@cjhyy/code-shell-core";
|
|
5
5
|
function targetUrl(raw) {
|
|
6
6
|
if (typeof raw !== "string" || raw.length > 2048)
|
|
7
7
|
throw new Error("Cookie selection requires an HTTPS URL");
|
|
@@ -16,16 +16,6 @@ function targetUrl(raw) {
|
|
|
16
16
|
throw new Error("Cookie selection requires an HTTPS URL");
|
|
17
17
|
return url;
|
|
18
18
|
}
|
|
19
|
-
function domain(raw) {
|
|
20
|
-
if (typeof raw !== "string")
|
|
21
|
-
return;
|
|
22
|
-
const value = raw.toLowerCase().replace(/^\./, "");
|
|
23
|
-
if (value.length > 253 ||
|
|
24
|
-
!value.includes(".") ||
|
|
25
|
-
!value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
|
|
26
|
-
return;
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
19
|
function beneath(host, root) {
|
|
30
20
|
return host === root || host.endsWith(`.${root}`);
|
|
31
21
|
}
|
|
@@ -86,7 +76,7 @@ export class PanelTaskCookieService {
|
|
|
86
76
|
]))
|
|
87
77
|
.digest("hex");
|
|
88
78
|
}
|
|
89
|
-
cookies(credential,
|
|
79
|
+
cookies(credential, host) {
|
|
90
80
|
const now = (this.options.now?.() ?? Date.now()) / 1000;
|
|
91
81
|
if (typeof credential.secret !== "string" ||
|
|
92
82
|
Buffer.byteLength(credential.secret) > 4 * 1024 * 1024)
|
|
@@ -106,8 +96,10 @@ export class PanelTaskCookieService {
|
|
|
106
96
|
!cookie.path.startsWith("/") ||
|
|
107
97
|
/[\x00-\x1f\x7f]/.test(cookie.path))))
|
|
108
98
|
return false;
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
// Account eligibility and actual Cookie scope are different fences:
|
|
100
|
+
// a login captured on www.example.com can carry .example.com cookies,
|
|
101
|
+
// but must not export host-only cookies from a sibling login host.
|
|
102
|
+
if (!cookieAppliesToHost(cookie, host))
|
|
111
103
|
return false;
|
|
112
104
|
if (cookie.expirationDate !== undefined &&
|
|
113
105
|
(typeof cookie.expirationDate !== "number" ||
|
|
@@ -120,10 +112,10 @@ export class PanelTaskCookieService {
|
|
|
120
112
|
async snapshot(scope, url) {
|
|
121
113
|
await this.options.authorize(scope);
|
|
122
114
|
const result = (await this.options.credentials(scope)).flatMap((credential) => {
|
|
123
|
-
const root =
|
|
115
|
+
const root = normalizeCookieDomain(credential.meta?.domain);
|
|
124
116
|
if (credential.type !== "cookie" || !root || !beneath(url.hostname.toLowerCase(), root))
|
|
125
117
|
return [];
|
|
126
|
-
const cookies = this.cookies(credential,
|
|
118
|
+
const cookies = this.cookies(credential, url.hostname);
|
|
127
119
|
if (!cookies.length)
|
|
128
120
|
return [];
|
|
129
121
|
return [
|
package/dist/panels/tool-jobs.js
CHANGED
|
@@ -179,11 +179,16 @@ export class PanelToolJobService {
|
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
executionScopes() {
|
|
182
|
+
const held = (scope, label) => ({
|
|
183
|
+
appId: scope.appId,
|
|
184
|
+
projectPath: scope.projectPath,
|
|
185
|
+
holder: { kind: "tool-job", ...(label ? { label } : {}) },
|
|
186
|
+
});
|
|
182
187
|
return [
|
|
183
|
-
...[...this.preparing.values()].map((item) => item.scope),
|
|
188
|
+
...[...this.preparing.values()].map((item) => held(item.scope)),
|
|
184
189
|
...[...this.jobs.values()]
|
|
185
190
|
.filter((job) => !TERMINAL.has(job.status) || this.active.has(job.id))
|
|
186
|
-
.map((job) => job.scope),
|
|
191
|
+
.map((job) => held(job.scope, job.entry.name)),
|
|
187
192
|
];
|
|
188
193
|
}
|
|
189
194
|
activeCount(projectPath) {
|