@cjhyy/code-shell-server 0.9.51 → 0.10.1
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/account/cli.d.ts +23 -0
- package/dist/account/cli.js +249 -0
- package/dist/account/cloud-contract.d.ts +39 -0
- package/dist/account/cloud-contract.js +134 -0
- package/dist/account/cloud-types.d.ts +47 -0
- package/dist/account/cloud-types.js +1 -0
- package/dist/account/feature-service.d.ts +47 -0
- package/dist/account/feature-service.js +184 -0
- package/dist/account/http.d.ts +17 -0
- package/dist/account/http.js +158 -0
- package/dist/account/links.d.ts +13 -0
- package/dist/account/links.js +137 -0
- package/dist/account/manager.d.ts +96 -0
- package/dist/account/manager.js +976 -0
- package/dist/account/native-lock.d.ts +6 -0
- package/dist/account/native-lock.js +26 -0
- package/dist/account/native-store.d.ts +32 -0
- package/dist/account/native-store.js +162 -0
- package/dist/account/profile.d.ts +4 -0
- package/dist/account/profile.js +46 -0
- package/dist/account/store.d.ts +34 -0
- package/dist/account/store.js +118 -0
- package/dist/account/types.d.ts +138 -0
- package/dist/account/types.js +2 -0
- package/dist/desktop-web/http-api.d.ts +4 -0
- package/dist/desktop-web/http-api.js +12 -4
- package/dist/environment-identity.d.ts +1 -1
- package/dist/environment-identity.js +2 -1
- package/dist/headless-worker-env.d.ts +2 -0
- package/dist/headless-worker-env.js +15 -0
- package/dist/hub/approval-lease.js +6 -1
- package/dist/hub/configuration.d.ts +11 -0
- package/dist/hub/configuration.js +34 -1
- package/dist/hub/model-parameter-metadata.d.ts +26 -0
- package/dist/hub/model-parameter-metadata.js +92 -0
- package/dist/hub/run-replay.js +1 -1
- package/dist/hub/session-management.js +1 -1
- package/dist/index.account.d.ts +12 -0
- package/dist/index.account.js +12 -0
- package/dist/index.mobile-remote.d.ts +2 -0
- package/dist/index.mobile-remote.js +1 -0
- package/dist/index.worker.d.ts +1 -0
- package/dist/index.worker.js +1 -0
- package/dist/mobile-remote/account-relay-access.d.ts +59 -0
- package/dist/mobile-remote/account-relay-access.js +174 -0
- package/dist/mobile-remote/approval-clock.d.ts +3 -0
- package/dist/mobile-remote/approval-clock.js +15 -0
- package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
- package/dist/mobile-remote/codex-room-agent.js +17 -1
- package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
- package/dist/mobile-remote/mobile-chat-turn.js +3 -0
- package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
- package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
- package/dist/mobile-remote/mobile-upload-service.js +25 -0
- package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
- package/dist/mobile-remote/remote-host-manager.js +142 -25
- package/dist/mobile-remote/resident-agent.d.ts +14 -0
- package/dist/mobile-remote/resident-agent.js +38 -11
- package/dist/mobile-remote/room-history.d.ts +7 -0
- package/dist/mobile-remote/room-history.js +56 -1
- package/dist/mobile-remote/room-manager.d.ts +216 -9
- package/dist/mobile-remote/room-manager.js +700 -65
- package/dist/mobile-remote/room-process-stop.d.ts +15 -0
- package/dist/mobile-remote/room-process-stop.js +41 -0
- package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
- package/dist/mobile-remote/trusted-device-store.js +54 -1
- package/dist/mobile-remote/turn-queue.d.ts +183 -0
- package/dist/mobile-remote/turn-queue.js +422 -0
- package/dist/mobile-remote/types.d.ts +1 -1
- package/dist/panels/agent-task-host.js +6 -12
- package/dist/panels/agent-task-service.d.ts +2 -0
- package/dist/panels/agent-task-service.js +7 -0
- package/dist/panels/browser-capture.d.ts +1 -15
- package/dist/panels/browser-capture.js +1 -35
- package/dist/panels/directory-picker.d.ts +48 -0
- package/dist/panels/directory-picker.js +247 -0
- package/dist/panels/http.d.ts +15 -15
- package/dist/panels/management-http.d.ts +9 -9
- package/dist/panels/process-service.d.ts +5 -4
- package/dist/panels/process-service.js +54 -16
- package/dist/panels/runtime.js +148 -30
- package/dist/panels/tool-jobs-storage.d.ts +4 -0
- package/dist/panels/tool-jobs-storage.js +111 -34
- package/dist/panels/types.d.ts +1 -97
- package/dist/remote-relay/connector.d.ts +8 -0
- package/dist/remote-relay/connector.js +29 -2
- package/dist/remote-relay/protocol.d.ts +21 -0
- package/dist/remote-relay/protocol.js +31 -0
- package/dist/serve/cli.js +6 -0
- package/dist/serve/headless-server.js +24 -19
- package/dist/sessions-service.d.ts +4 -0
- package/dist/sessions-service.js +14 -1
- package/package.json +10 -6
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
import { opendir } from "node:fs/promises";
|
|
4
|
+
import { basename, isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { PanelManagementError } from "./management.js";
|
|
6
|
+
import { PanelBridgeError } from "./bridge-contract.js";
|
|
7
|
+
export const directoryPickerLifetime = 5 * 60_000;
|
|
8
|
+
const TOKEN = /^[A-Za-z0-9_-]{24}$/;
|
|
9
|
+
const MAX_TOKENS = 1024;
|
|
10
|
+
const MAX_ENTRIES = 200;
|
|
11
|
+
const MAX_SCAN = 4096;
|
|
12
|
+
const MAX_TOKEN_BYTES = 1024 * 1024;
|
|
13
|
+
function unavailable() {
|
|
14
|
+
throw new PanelManagementError(409, "panel_directory_picker", "目录选择已失效,请重新发起。");
|
|
15
|
+
}
|
|
16
|
+
function identity(path) {
|
|
17
|
+
try {
|
|
18
|
+
const info = lstatSync(path, { bigint: true });
|
|
19
|
+
if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(path) !== path)
|
|
20
|
+
unavailable();
|
|
21
|
+
return { path, dev: info.dev, ino: info.ino };
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return unavailable();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function assertIdentity(expected) {
|
|
28
|
+
const actual = identity(expected.path);
|
|
29
|
+
if (actual.dev !== expected.dev || actual.ino !== expected.ino)
|
|
30
|
+
unavailable();
|
|
31
|
+
}
|
|
32
|
+
function visibleName(name) {
|
|
33
|
+
return (name.length > 0 &&
|
|
34
|
+
name.length <= 255 &&
|
|
35
|
+
!name.startsWith(".") &&
|
|
36
|
+
!/[\\/\u0000-\u001f\u007f]/u.test(name));
|
|
37
|
+
}
|
|
38
|
+
/** Used only after a Host bookmark proves a previous choice; a client path is never sufficient. */
|
|
39
|
+
export function capturePanelWorkspaceDirectory(cwd, path) {
|
|
40
|
+
let root;
|
|
41
|
+
try {
|
|
42
|
+
root = realpathSync(cwd);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return unavailable();
|
|
46
|
+
}
|
|
47
|
+
const rel = relative(root, path);
|
|
48
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
|
|
49
|
+
unavailable();
|
|
50
|
+
const parts = rel.split(sep).filter(Boolean);
|
|
51
|
+
if (parts.length > 64)
|
|
52
|
+
unavailable();
|
|
53
|
+
const chain = [identity(root)];
|
|
54
|
+
for (let i = 0; i < parts.length; i++)
|
|
55
|
+
chain.push(identity(join(root, ...parts.slice(0, i + 1))));
|
|
56
|
+
const assertCurrent = () => {
|
|
57
|
+
try {
|
|
58
|
+
if (realpathSync(cwd) !== root)
|
|
59
|
+
unavailable();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
unavailable();
|
|
63
|
+
}
|
|
64
|
+
for (const entry of chain)
|
|
65
|
+
assertIdentity(entry);
|
|
66
|
+
};
|
|
67
|
+
assertCurrent();
|
|
68
|
+
return { path, assertCurrent };
|
|
69
|
+
}
|
|
70
|
+
/** A pending parent-UI choice of an existing project directory, not filesystem authority. */
|
|
71
|
+
export class PanelDirectoryPicker {
|
|
72
|
+
cwd;
|
|
73
|
+
now;
|
|
74
|
+
rootPath;
|
|
75
|
+
rootToken;
|
|
76
|
+
initialToken;
|
|
77
|
+
expiresAt;
|
|
78
|
+
root;
|
|
79
|
+
choices = new Map();
|
|
80
|
+
closed = false;
|
|
81
|
+
reading = false;
|
|
82
|
+
tokenBytes = 0;
|
|
83
|
+
constructor(cwd, now) {
|
|
84
|
+
this.cwd = cwd;
|
|
85
|
+
this.now = now;
|
|
86
|
+
this.rootPath = realpathSync(cwd);
|
|
87
|
+
this.root = identity(this.rootPath);
|
|
88
|
+
this.expiresAt = now() + directoryPickerLifetime;
|
|
89
|
+
this.rootToken = this.add([], [this.root]).token;
|
|
90
|
+
const path = join(this.rootPath, "downloads");
|
|
91
|
+
let child;
|
|
92
|
+
try {
|
|
93
|
+
child = identity(path);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (!(error instanceof PanelManagementError) &&
|
|
97
|
+
error.code !== "ENOENT")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
this.initialToken = child
|
|
101
|
+
? this.add(["downloads"], [this.root, child], this.rootToken).token
|
|
102
|
+
: this.rootToken;
|
|
103
|
+
this.assertCurrent();
|
|
104
|
+
}
|
|
105
|
+
close() {
|
|
106
|
+
this.closed = true;
|
|
107
|
+
this.choices.clear();
|
|
108
|
+
this.tokenBytes = 0;
|
|
109
|
+
}
|
|
110
|
+
assertRoot() {
|
|
111
|
+
try {
|
|
112
|
+
if (realpathSync(this.cwd) !== this.rootPath)
|
|
113
|
+
unavailable();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
unavailable();
|
|
117
|
+
}
|
|
118
|
+
assertIdentity(this.root);
|
|
119
|
+
}
|
|
120
|
+
assertCurrent() {
|
|
121
|
+
if (this.closed || this.now() >= this.expiresAt)
|
|
122
|
+
unavailable();
|
|
123
|
+
this.assertRoot();
|
|
124
|
+
}
|
|
125
|
+
check(choice) {
|
|
126
|
+
this.assertCurrent();
|
|
127
|
+
for (const entry of choice.chain)
|
|
128
|
+
assertIdentity(entry);
|
|
129
|
+
}
|
|
130
|
+
get(token) {
|
|
131
|
+
const choice = typeof token === "string" && TOKEN.test(token) && this.choices.get(token);
|
|
132
|
+
if (!choice)
|
|
133
|
+
unavailable();
|
|
134
|
+
this.check(choice);
|
|
135
|
+
return choice;
|
|
136
|
+
}
|
|
137
|
+
add(parts, chain, parentToken) {
|
|
138
|
+
const existing = [...this.choices.values()].find((item) => item.parts.join("/") === parts.join("/"));
|
|
139
|
+
if (existing) {
|
|
140
|
+
this.check(existing);
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
const bytes = Buffer.byteLength(join(this.rootPath, ...parts)) +
|
|
144
|
+
Buffer.byteLength(parts.join("/")) +
|
|
145
|
+
chain.length * 32 +
|
|
146
|
+
256;
|
|
147
|
+
if (this.choices.size >= MAX_TOKENS ||
|
|
148
|
+
parts.length > 64 ||
|
|
149
|
+
this.tokenBytes + bytes > MAX_TOKEN_BYTES)
|
|
150
|
+
unavailable();
|
|
151
|
+
const choice = {
|
|
152
|
+
token: randomBytes(18).toString("base64url"),
|
|
153
|
+
parts,
|
|
154
|
+
chain,
|
|
155
|
+
...(parentToken ? { parentToken } : {}),
|
|
156
|
+
};
|
|
157
|
+
this.choices.set(choice.token, choice);
|
|
158
|
+
this.tokenBytes += bytes;
|
|
159
|
+
return choice;
|
|
160
|
+
}
|
|
161
|
+
async list(token, authorize) {
|
|
162
|
+
if (this.reading)
|
|
163
|
+
throw new PanelBridgeError("RATE_LIMITED", "目录正在读取,请稍后重试。", 50);
|
|
164
|
+
this.reading = true;
|
|
165
|
+
try {
|
|
166
|
+
const choice = this.get(token);
|
|
167
|
+
await authorize();
|
|
168
|
+
this.check(choice);
|
|
169
|
+
const directories = [];
|
|
170
|
+
let truncated = false;
|
|
171
|
+
{
|
|
172
|
+
const directory = await opendir(join(this.rootPath, ...choice.parts));
|
|
173
|
+
let scanned = 0;
|
|
174
|
+
try {
|
|
175
|
+
this.check(choice);
|
|
176
|
+
for await (const entry of directory) {
|
|
177
|
+
this.check(choice);
|
|
178
|
+
if (++scanned > MAX_SCAN) {
|
|
179
|
+
truncated = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !visibleName(entry.name))
|
|
183
|
+
continue;
|
|
184
|
+
if (directories.length >= MAX_ENTRIES) {
|
|
185
|
+
truncated = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const parts = [...choice.parts, entry.name];
|
|
189
|
+
const path = join(this.rootPath, ...parts);
|
|
190
|
+
let child;
|
|
191
|
+
try {
|
|
192
|
+
child = identity(path);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const item = this.add(parts, [...choice.chain, child], choice.token);
|
|
198
|
+
directories.push({ name: entry.name, token: item.token });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
// for-await closes its directory; the explicit close covers failure before iteration.
|
|
203
|
+
try {
|
|
204
|
+
await directory.close();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* Already closed by the iterator. */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
await authorize();
|
|
212
|
+
this.check(choice);
|
|
213
|
+
for (const item of directories)
|
|
214
|
+
this.get(item.token);
|
|
215
|
+
return {
|
|
216
|
+
directoryToken: choice.token,
|
|
217
|
+
relativePath: choice.parts.join("/"),
|
|
218
|
+
name: choice.parts.at(-1) ?? (basename(this.rootPath) || this.rootPath),
|
|
219
|
+
...(choice.parentToken ? { parentToken: choice.parentToken } : {}),
|
|
220
|
+
rootToken: this.rootToken,
|
|
221
|
+
directories: directories.sort((a, b) => a.name.localeCompare(b.name)),
|
|
222
|
+
truncated,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
this.reading = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
assertListing(listing) {
|
|
230
|
+
this.get(listing.directoryToken);
|
|
231
|
+
for (const item of listing.directories)
|
|
232
|
+
this.get(item.token);
|
|
233
|
+
}
|
|
234
|
+
/** Called synchronously after final authorization, while this exact confirmation is pending. */
|
|
235
|
+
select(token) {
|
|
236
|
+
const choice = this.get(token);
|
|
237
|
+
const path = join(this.rootPath, ...choice.parts);
|
|
238
|
+
const chain = [...choice.chain];
|
|
239
|
+
const assertCurrent = () => {
|
|
240
|
+
this.assertRoot();
|
|
241
|
+
for (const entry of chain)
|
|
242
|
+
assertIdentity(entry);
|
|
243
|
+
};
|
|
244
|
+
assertCurrent();
|
|
245
|
+
return { path, assertCurrent };
|
|
246
|
+
}
|
|
247
|
+
}
|
package/dist/panels/http.d.ts
CHANGED
|
@@ -17,26 +17,26 @@ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
|
17
17
|
authorizePanelDirectory?: PanelDirectoryAuthorizer;
|
|
18
18
|
}): {
|
|
19
19
|
service: {
|
|
20
|
-
snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("
|
|
21
|
-
packageHistory(context: import("
|
|
22
|
-
previewRestore(context: import("
|
|
23
|
-
cancelRestore(context: import("
|
|
20
|
+
snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("@cjhyy/code-shell-web/panels").PanelSnapshot>;
|
|
21
|
+
packageHistory(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelPackageHistory>;
|
|
22
|
+
previewRestore(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelPackageRestoreReview>;
|
|
23
|
+
cancelRestore(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, token: unknown): Promise<{
|
|
24
24
|
cancelled: boolean;
|
|
25
25
|
}>;
|
|
26
|
-
restore(context: import("
|
|
26
|
+
restore(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, token: unknown): Promise<{
|
|
27
27
|
id: string;
|
|
28
28
|
packageDigest: string;
|
|
29
29
|
}>;
|
|
30
|
-
assertAuthorized: (context: import("
|
|
31
|
-
discover(context: import("
|
|
32
|
-
preview: (context: import("
|
|
33
|
-
previewUpdate(context: import("
|
|
34
|
-
previewProjectSource: (context: import("
|
|
30
|
+
assertAuthorized: (context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, expected?: number) => Promise<void>;
|
|
31
|
+
discover(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, input: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelDiscovery>;
|
|
32
|
+
preview: (context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, input: unknown) => Promise<import("@cjhyy/code-shell-web/panels").PanelReview>;
|
|
33
|
+
previewUpdate(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelReview>;
|
|
34
|
+
previewProjectSource: (context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, input: import("@cjhyy/code-shell-core").PanelAppSourceInput, expected?: {
|
|
35
35
|
id: string;
|
|
36
36
|
revision: string;
|
|
37
|
-
}) => Promise<import("
|
|
38
|
-
previewProjectUpdate(context: import("
|
|
39
|
-
install(context: import("
|
|
37
|
+
}) => Promise<import("@cjhyy/code-shell-web/panels").PanelProjectReview>;
|
|
38
|
+
previewProjectUpdate(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelProjectReview>;
|
|
39
|
+
install(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, token: unknown, bind?: boolean, approval?: {
|
|
40
40
|
overwrite?: boolean;
|
|
41
41
|
expectedId?: string;
|
|
42
42
|
releaseExecutions?: (appId: string, matches: (scope: import("./execution-gate.js").PanelExecutionScope) => boolean) => Promise<void>;
|
|
@@ -44,8 +44,8 @@ export declare function createPanelHttp(options: PanelManagementHttpOptions & {
|
|
|
44
44
|
id: string;
|
|
45
45
|
packageDigest: string | undefined;
|
|
46
46
|
}>;
|
|
47
|
-
binding(context: import("
|
|
48
|
-
remove(context: import("
|
|
47
|
+
binding(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelSnapshot>;
|
|
48
|
+
remove(context: import("@cjhyy/code-shell-web/panels").PanelOperationContext, id: unknown, expected: unknown): Promise<{
|
|
49
49
|
removed: true;
|
|
50
50
|
}>;
|
|
51
51
|
cancelOwner(owner: string): void;
|
|
@@ -8,9 +8,9 @@ export interface PanelManagementHttpOptions extends PanelManagementOptions {
|
|
|
8
8
|
/** Call only after the host's same-origin authentication boundary. */
|
|
9
9
|
export declare function createPanelManagementHttp(options: PanelManagementHttpOptions): {
|
|
10
10
|
service: {
|
|
11
|
-
snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("
|
|
12
|
-
packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("
|
|
13
|
-
previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("
|
|
11
|
+
snapshot: (appId?: string, reads?: import("./management.js").PanelSnapshotReads) => Promise<import("@cjhyy/code-shell-web/panels").PanelSnapshot>;
|
|
12
|
+
packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelPackageHistory>;
|
|
13
|
+
previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelPackageRestoreReview>;
|
|
14
14
|
cancelRestore(context: PanelOperationContext, token: unknown): Promise<{
|
|
15
15
|
cancelled: boolean;
|
|
16
16
|
}>;
|
|
@@ -19,14 +19,14 @@ export declare function createPanelManagementHttp(options: PanelManagementHttpOp
|
|
|
19
19
|
packageDigest: string;
|
|
20
20
|
}>;
|
|
21
21
|
assertAuthorized: (context: PanelOperationContext, expected?: number) => Promise<void>;
|
|
22
|
-
discover(context: PanelOperationContext, input: unknown): Promise<import("
|
|
23
|
-
preview: (context: PanelOperationContext, input: unknown) => Promise<import("
|
|
24
|
-
previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("
|
|
22
|
+
discover(context: PanelOperationContext, input: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelDiscovery>;
|
|
23
|
+
preview: (context: PanelOperationContext, input: unknown) => Promise<import("@cjhyy/code-shell-web/panels").PanelReview>;
|
|
24
|
+
previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelReview>;
|
|
25
25
|
previewProjectSource: (context: PanelOperationContext, input: import("@cjhyy/code-shell-core").PanelAppSourceInput, expected?: {
|
|
26
26
|
id: string;
|
|
27
27
|
revision: string;
|
|
28
|
-
}) => Promise<import("
|
|
29
|
-
previewProjectUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("
|
|
28
|
+
}) => Promise<import("@cjhyy/code-shell-web/panels").PanelProjectReview>;
|
|
29
|
+
previewProjectUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelProjectReview>;
|
|
30
30
|
install(context: PanelOperationContext, token: unknown, bind?: boolean, approval?: {
|
|
31
31
|
overwrite?: boolean;
|
|
32
32
|
expectedId?: string;
|
|
@@ -35,7 +35,7 @@ export declare function createPanelManagementHttp(options: PanelManagementHttpOp
|
|
|
35
35
|
id: string;
|
|
36
36
|
packageDigest: string | undefined;
|
|
37
37
|
}>;
|
|
38
|
-
binding(context: PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("
|
|
38
|
+
binding(context: PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("@cjhyy/code-shell-web/panels").PanelSnapshot>;
|
|
39
39
|
remove(context: PanelOperationContext, id: unknown, expected: unknown): Promise<{
|
|
40
40
|
removed: true;
|
|
41
41
|
}>;
|
|
@@ -26,9 +26,9 @@ export interface PanelAppProcessServiceOptions {
|
|
|
26
26
|
/** Rechecked after asynchronous work and immediately before admitting a process. */
|
|
27
27
|
isOwnerAuthorized?(owner: PanelProcessOwner): boolean | Promise<boolean>;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* to
|
|
31
|
-
* app-wide durable hooks.
|
|
29
|
+
* When executable confirmation is required, app scope can delegate durable
|
|
30
|
+
* approval to Host hooks. Guest scope isolates authenticated instances and
|
|
31
|
+
* never uses the app-wide durable hooks.
|
|
32
32
|
*/
|
|
33
33
|
approvalScope?: "app" | "guest";
|
|
34
34
|
confirmExecution(input: {
|
|
@@ -40,7 +40,7 @@ export interface PanelAppProcessServiceOptions {
|
|
|
40
40
|
}): Promise<boolean>;
|
|
41
41
|
isExecutionApproved?(scope: PanelProcessApprovalScope): Promise<boolean>;
|
|
42
42
|
rememberExecutionApproval?(scope: PanelProcessApprovalScope): Promise<void>;
|
|
43
|
-
/**
|
|
43
|
+
/** Hosts can use installed-app process permission and authorized-project binding as consent. */
|
|
44
44
|
allowAuthorizedProcessWithoutPrompt?: boolean;
|
|
45
45
|
/** Resolve a declared entry in this app revision; return a canonical, reviewed file. */
|
|
46
46
|
resolvePackageEntry?(owner: PanelProcessOwner, name: string): Promise<{
|
|
@@ -75,6 +75,7 @@ export declare class PanelAppProcessService {
|
|
|
75
75
|
private readonly retiring;
|
|
76
76
|
private readonly receipts;
|
|
77
77
|
private readonly approvedExecutables;
|
|
78
|
+
private readonly pendingExecutionApprovals;
|
|
78
79
|
private readonly guestEpochs;
|
|
79
80
|
private readonly reservations;
|
|
80
81
|
private closed;
|
|
@@ -169,6 +169,7 @@ export class PanelAppProcessService {
|
|
|
169
169
|
retiring = new Map();
|
|
170
170
|
receipts;
|
|
171
171
|
approvedExecutables = new Set();
|
|
172
|
+
pendingExecutionApprovals = new Map();
|
|
172
173
|
guestEpochs = new Map();
|
|
173
174
|
reservations = new Map();
|
|
174
175
|
closed = false;
|
|
@@ -447,6 +448,7 @@ export class PanelAppProcessService {
|
|
|
447
448
|
// Admission is reserved before the first await, including a user's approval dialog.
|
|
448
449
|
const releaseExecution = this.options.acquireExecution?.(owner);
|
|
449
450
|
let processOwnsExecution = false;
|
|
451
|
+
let pendingApproval;
|
|
450
452
|
this.reservations.set(owner.guestId, reserved + 1);
|
|
451
453
|
try {
|
|
452
454
|
await this.authorize(owner, epoch);
|
|
@@ -462,29 +464,45 @@ export class PanelAppProcessService {
|
|
|
462
464
|
const guestScoped = this.options.approvalScope === "guest";
|
|
463
465
|
const approvalKey = (guestScoped ? `${owner.guestId}\0${epoch}\0` : "") +
|
|
464
466
|
`${scope.appId}\0${scope.revision}\0${scope.executablePath}\0${scope.executableFingerprint}`;
|
|
465
|
-
//
|
|
466
|
-
// bound project
|
|
467
|
+
// Hosts may use installed-app process permission and their authorized,
|
|
468
|
+
// bound project as consent; authorize() still runs before every spawn.
|
|
467
469
|
let approved = this.options.allowAuthorizedProcessWithoutPrompt === true ||
|
|
468
470
|
this.approvedExecutables.has(approvalKey);
|
|
469
471
|
if (!approved && !guestScoped && this.options.isExecutionApproved) {
|
|
470
472
|
approved = await this.options.isExecutionApproved(scope).catch(() => false);
|
|
471
473
|
await this.authorize(owner, epoch);
|
|
472
474
|
}
|
|
475
|
+
approved ||= this.approvedExecutables.has(approvalKey);
|
|
473
476
|
if (!approved) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
477
|
+
let approval = this.pendingExecutionApprovals.get(approvalKey);
|
|
478
|
+
if (!approval) {
|
|
479
|
+
approval = {
|
|
480
|
+
guestId: owner.guestId,
|
|
481
|
+
waiters: 0,
|
|
482
|
+
result: (async () => {
|
|
483
|
+
const allowed = await this.options.confirmExecution({
|
|
484
|
+
guestId: owner.guestId,
|
|
485
|
+
appId: owner.appId,
|
|
486
|
+
appTitle: owner.appTitle,
|
|
487
|
+
executable: executable.name,
|
|
488
|
+
executablePath: executable.path,
|
|
489
|
+
});
|
|
490
|
+
await this.authorize(owner, epoch);
|
|
491
|
+
if (!allowed)
|
|
492
|
+
throw new Error(`User denied running ${executable.name}`);
|
|
493
|
+
if (!guestScoped) {
|
|
494
|
+
await this.options.rememberExecutionApproval?.(scope).catch(() => undefined);
|
|
495
|
+
await this.authorize(owner, epoch);
|
|
496
|
+
}
|
|
497
|
+
})(),
|
|
498
|
+
};
|
|
499
|
+
this.pendingExecutionApprovals.set(approvalKey, approval);
|
|
487
500
|
}
|
|
501
|
+
approval.waiters++;
|
|
502
|
+
pendingApproval = { key: approvalKey, approval };
|
|
503
|
+
await approval.result;
|
|
504
|
+
// Sharing consent does not share a caller's authority or process handles.
|
|
505
|
+
await this.authorize(owner, epoch);
|
|
488
506
|
}
|
|
489
507
|
// A binary replaced while a dialog was open must earn a new approval.
|
|
490
508
|
if ((await executableFingerprint(executable.path)) !== scope.executableFingerprint) {
|
|
@@ -618,6 +636,13 @@ export class PanelAppProcessService {
|
|
|
618
636
|
}),
|
|
619
637
|
};
|
|
620
638
|
this.processes.set(processId, running);
|
|
639
|
+
// A successful launcher may leave children behind (including children
|
|
640
|
+
// retaining stdout). Reap its group on leader exit, before close/lease release.
|
|
641
|
+
child.once("exit", () => {
|
|
642
|
+
const pid = child.pid;
|
|
643
|
+
if (!running.termination && typeof pid === "number" && pid > 1)
|
|
644
|
+
running.termination = killProcessGroup(pid).catch(() => { });
|
|
645
|
+
});
|
|
621
646
|
if (modelTraceOwner && this.options.modelTraces) {
|
|
622
647
|
const channel = new PanelModelTraceChannel({
|
|
623
648
|
service: this.options.modelTraces,
|
|
@@ -668,7 +693,7 @@ export class PanelAppProcessService {
|
|
|
668
693
|
clearTimeout(lifetime);
|
|
669
694
|
if (inputChecks)
|
|
670
695
|
clearInterval(inputChecks);
|
|
671
|
-
//
|
|
696
|
+
// Natural exit and cancellation both wait for the entire process group.
|
|
672
697
|
await running.termination;
|
|
673
698
|
let finalCode = code;
|
|
674
699
|
if (inputChecks && this.processes.get(processId) === running) {
|
|
@@ -704,6 +729,14 @@ export class PanelAppProcessService {
|
|
|
704
729
|
return { processId, executable: executable.name };
|
|
705
730
|
}
|
|
706
731
|
finally {
|
|
732
|
+
if (pendingApproval) {
|
|
733
|
+
const { key, approval } = pendingApproval;
|
|
734
|
+
approval.waiters--;
|
|
735
|
+
// Keep successful consent in flight until a caller has cached it after
|
|
736
|
+
// all spawn checks. Rejections disappear once every waiter has observed them.
|
|
737
|
+
if (!approval.waiters && this.pendingExecutionApprovals.get(key) === approval)
|
|
738
|
+
this.pendingExecutionApprovals.delete(key);
|
|
739
|
+
}
|
|
707
740
|
if (!processOwnsExecution)
|
|
708
741
|
releaseExecution?.();
|
|
709
742
|
const remaining = (this.reservations.get(owner.guestId) ?? 1) - 1;
|
|
@@ -874,6 +907,10 @@ export class PanelAppProcessService {
|
|
|
874
907
|
}
|
|
875
908
|
revokeGuest(guestId) {
|
|
876
909
|
this.guestEpochs.set(guestId, (this.guestEpochs.get(guestId) ?? 0) + 1);
|
|
910
|
+
for (const [key, approval] of this.pendingExecutionApprovals) {
|
|
911
|
+
if (approval.guestId === guestId)
|
|
912
|
+
this.pendingExecutionApprovals.delete(key);
|
|
913
|
+
}
|
|
877
914
|
if (this.options.approvalScope === "guest") {
|
|
878
915
|
for (const key of this.approvedExecutables) {
|
|
879
916
|
if (key.startsWith(`${guestId}\0`))
|
|
@@ -938,6 +975,7 @@ export class PanelAppProcessService {
|
|
|
938
975
|
for (const guestId of guests)
|
|
939
976
|
this.revokeGuest(guestId);
|
|
940
977
|
this.approvedExecutables.clear();
|
|
978
|
+
this.pendingExecutionApprovals.clear();
|
|
941
979
|
this.receipts.close();
|
|
942
980
|
}
|
|
943
981
|
terminate(running) {
|